VYPR
High severityNVD Advisory· Published Aug 22, 2025· Updated Aug 22, 2025

gnark is vulnerable to signature malleability in EdDSA and ECDSA due to missing scalar checks

CVE-2025-57801

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.

PackageAffected versionsPatched versions
github.com/consensys/gnarkGo
< 0.14.00.14.0

Affected products

2
  • gnark/gnarkllm-fuzzy
    Range: <0.14.0
  • Consensys/gnarkv5
    Range: < 0.14.0

Patches

1
0ba6730f0553

Merge commit from fork

https://github.com/Consensys/gnarkThomasPiellardAug 21, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.