gnark is vulnerable to signature malleability in EdDSA and ECDSA due to missing scalar checks
Description
gnark is a zero-knowledge proof system framework. In versions prior to 0.14.0, the Verify function in eddsa.go and ecdsa.go used the S value from a signature without asserting that 0 ≤ S < order, leading to a signature malleability vulnerability. Because gnark’s native EdDSA and ECDSA circuits lack essential constraints, multiple distinct witnesses can satisfy the same public inputs. In protocols where nullifiers or anti-replay checks are derived from R and S, this enables signature malleability and may allow double spending. This issue has been addressed in version 0.14.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Missing S-value range check in gnark's EdDSA and ECDSA Verify functions prior to v0.14.0 enables signature malleability, allowing double spends in protocols that derive anti-replay from (R,S).
What the vulnerability is
CVE-2025-57801 is a signature malleability flaw in gnark, a zero-knowledge proof library. In versions before 0.14.0, the Verify functions in eddsa.go and ecdsa.go accepted an S value from a signature without ensuring that 0 ≤ S < the curve's subgroup order.[1] This missing scalar-range check means that an attacker can add the group order to the valid S, producing a distinct but equally valid signature that still satisfies the verification equation.[2]
How it's exploited
The attack requires only a valid signature (R, S) produced by a legitimate signer. By forging a new signature with S' = S + order (modulo the group order), an attacker creates a second valid witness for the same public inputs (message and public key). No access to the private key is needed; the attacker only needs to observe a single valid signature. The proof-of-concept code in the advisory demonstrates this by adding the curve's scalar field order to the S component, producing a signature that passes gnark's verification circuit in groth16.[2]
Impact
Because multiple distinct witnesses (S and S+order) can satisfy the same circuit's verification, protocols that use (R, S) to derive nullifiers, anti-replay nonces, or spentness checks are vulnerable. An adversary who has seen a valid signature can generate a second, distinct proof that appears to be a separate transaction, leading to double spending or replay attacks.[1][2] The core cryptographic primitive (EdDSA or ECDSA) remains sound when implemented correctly, but the gnark zk-circuit lacked the necessary range constraints that standard implementations require (e.g., RFC 8032 §3.4 for EdDSA).
Mitigation
The issue is fixed in gnark version 0.14.0.[1] The patch adds AssertIsLessOrEqual calls for both S and R values against the curve's modulus or subgroup order, as shown in the merge commit.[4] Users should upgrade to v0.14.0 or later. No workaround is available; any protocol relying on pre-0.14.0 gnark circuits for signature verification should treat the old circuits as malleable.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/consensys/gnarkGo | < 0.14.0 | 0.14.0 |
Affected products
2- Consensys/gnarkv5Range: < 0.14.0
Patches
10ba6730f0553Merge commit from fork
3 files changed · +56 −0
std/signature/ecdsa/ecdsa.go+4 −0 modified@@ -59,6 +59,10 @@ func (pk PublicKey[T, S]) prepareVerification(api frontend.API, params sw_emulat if err != nil { panic(err) } + + scalarApi.AssertIsLessOrEqual(&sig.S, scalarApi.Modulus()) + scalarApi.AssertIsLessOrEqual(&sig.R, scalarApi.Modulus()) + pkpt := sw_emulated.AffinePoint[T](pk) msInv := scalarApi.Div(msg, &sig.S) rsInv := scalarApi.Div(&sig.R, &sig.S)
std/signature/eddsa/eddsa.go+3 −0 modified@@ -53,6 +53,9 @@ func Verify(curve twistededwards.Curve, sig Signature, msg frontend.Variable, pu Y: curve.Params().Base[1], } + // Assert S < GroupSize (see https://datatracker.ietf.org/doc/html/rfc8032#section-3.4) + curve.API().AssertIsLessOrEqual(sig.S, curve.Params().Order) + //[S]G-[H(R,A,M)]*A _A := curve.Neg(pubKey.A) Q := curve.DoubleBaseScalarMul(base, _A, sig.S, hRAM)
std/signature/eddsa/eddsa_test.go+49 −0 modified@@ -42,6 +42,47 @@ func (circuit *eddsaCircuit) Define(api frontend.API) error { return Verify(curve, circuit.Signature, circuit.Message, circuit.PublicKey, &mimc) } +// Forge signature: S → S + order +func forge(id tedwards.ID, sig []byte) ([]byte, error) { + + forged := make([]byte, len(sig)) + copy(forged, sig) + + var offset int + switch id { + case tedwards.BN254: + offset = 32 + case tedwards.BLS12_381: + offset = 32 + case tedwards.BLS12_377: + offset = 32 + case tedwards.BW6_761: + offset = 48 + case tedwards.BLS24_317: + offset = 32 + case tedwards.BLS24_315: + offset = 32 + case tedwards.BW6_633: + offset = 40 + default: + panic("not implemented") + } + + s := new(big.Int).SetBytes(sig[offset:]) + params, err := twistededwards.GetCurveParams(id) + if err != nil { + return nil, err + } + s.Add(s, params.Order) + + sizeS := len(sig) - offset + buf := make([]byte, sizeS) + copy(buf[sizeS-len(s.Bytes()):], s.Bytes()) + + copy(forged[offset:], buf) + return forged, nil +} + func TestEddsa(t *testing.T) { assert := test.NewAssert(t) @@ -110,9 +151,17 @@ func TestEddsa(t *testing.T) { invalidWitness.PublicKey.Assign(conf.curve, pubKey.Bytes()) invalidWitness.Signature.Assign(conf.curve, signature) + var invalidWitnessOverflow eddsaCircuit + invalidWitnessOverflow.Message = msg + invalidWitnessOverflow.PublicKey.Assign(conf.curve, pubKey.Bytes()) + forgedSig, err := forge(conf.curve, signature) + assert.NoError(err, "forging signature") + invalidWitnessOverflow.Signature.Assign(conf.curve, forgedSig) + assert.CheckCircuit(&circuit, test.WithValidAssignment(&validWitness), test.WithInvalidAssignment(&invalidWitness), + test.WithInvalidAssignment(&invalidWitnessOverflow), test.WithCurves(snarkCurve)) }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-95v9-hv42-pwrjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-57801ghsaADVISORY
- github.com/Consensys/gnark/commit/0ba6730f05537a351517998add89a61a0d82716eghsax_refsource_MISCWEB
- github.com/Consensys/gnark/security/advisories/GHSA-95v9-hv42-pwrjghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2025-3912ghsaWEB
News mentions
0No linked articles in our index yet.