diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d8361b..5f3cce22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ This unreleased set is delivered through a stack of fork pull requests. **Every belongs to PR #2 (the base BNB hardening integration) unless it is tagged with another `PR #N`.** Composing PRs: - **PR #2** — base BNB hardening integration. +- **PR #4** — BNB #332 tBTC-relevant hardening backport (stacked on PR #2). ### ⚠️ Compatibility — read before upgrading @@ -195,6 +196,41 @@ rejecting input that an honest caller would previously have produced. after completion (`BNB #276`, `f3aad28`); the EdDSA resharing round-1 party-0 broadcast and EdDSA keygen `NewECPoint`-error path are guarded against nil dereference (`BNB #282`, `BNB 5d0d0f3`). +- **Panic/DoS guards on EC point operations (PR #4):** `ECPoint.ScalarMult`, + `ScalarBaseMult`, `Add`, `SetCurve`, `EightInvEight`, and `isOnCurve` return nil/error on + nil or invalid inputs instead of panicking (`crypto/ecpoint.go`). Signatures unchanged; + honest callers never pass nil. _Provenance: `BNB #332`, PR #4._ +- **Schnorr verifier pre-checks (PR #4):** `Verify`/`VerifyWithSession` reject nil/invalid + public points, zero or out-of-range scalars, a zero challenge, and nil scalar-mult results + before use (`crypto/schnorr/schnorr_proof.go`). Challenge derivation is unchanged; + malicious/degenerate input only. _Provenance: `BNB #332`, PR #4._ +- **DLN verifier canonical `Alpha` check (PR #4):** `Verify` requires each `Alpha` to be + canonically in `(1, N)` instead of accepting values that only landed in range after + reduction mod `N`, and guards nil `h1`/`h2`/`N`/`Alpha`/`T` (`crypto/dlnproof/proof.go`). + Honest provers already produce canonical `Alpha`. _Provenance: `BNB #332`, PR #4._ +- **Paillier FactorProof response bounds (PR #4):** the verifier rejects `W1`, `W2`, + `Sigma`, and `V` outside their absolute bounds (keeping the existing inclusive `Z1`/`Z2` + style) before verification (`crypto/paillier/factor_proof.go`). Honest proofs pass. + _Provenance: `BNB #332`, PR #4._ +- **MtA / range-proof bounds and nil guards (PR #4):** `ProofBob`/`ProofBobWC`/ + `RangeProofAlice` verifiers reject nil moduli/inputs, tighten the `S2`/`T2` upper bounds to + exclusive, and add an explicit nil-result check after `xE.Add(pf.U)` (`crypto/mta/*.go`). + Honest proofs pass. _Provenance: `BNB #332`, PR #4._ +- **VSS share-verification guards (PR #4):** `Verify` rejects nil, zero, and out-of-range + shares and verifier points (and validates each commitment point) before scalar + multiplication (`crypto/vss/feldman_vss.go`). _Provenance: `BNB #332`, PR #4._ +- **ECDSA keygen round-1 modulus-width check (PR #4):** `KGRound1Message.ValidateBasic` + rejects Paillier `N` / `NTilde` that are not exactly 2048 bits, failing fast on malformed + peer messages and mirroring the pre-existing round-2 contract. Honest 2048-bit keys are + unaffected. _Provenance: `BNB #332`, PR #4._ +- **ECDSA signing round-9 decommitment guard fix (PR #4):** corrected the de-commitment + validation from `!ok && len(values) != 4` to `!ok || len(values) != 4` (extracted as + `decommitFour`), closing a soundness/DoS gap where a malformed or oversized de-commitment + could be read as attacker-chosen point coordinates or cause an out-of-range panic + (`ecdsa/signing/round_9.go`). _Provenance: `BNB #332`, PR #4._ +- **ECDSA signing round-4 nil theta-inverse guard (PR #4):** a non-invertible theta + (`ModInverse` returning nil) is rejected with a clean error instead of propagating nil + (`ecdsa/signing/round_4.go`). _Provenance: `BNB #332`, PR #4._ ### Added @@ -209,6 +245,9 @@ rejecting input that an honest caller would previously have produced. - `schnorr.NewZKProofWithSession`, `NewZKVProofWithSession`, `VerifyWithSession` — session- aware Schnorr proof overloads (the original signatures are retained and delegate with a nil session). +- `mta.ErrRangeProofVerify` (PR #4) — sentinel error letting ECDSA signing round 2 attribute + a peer's MtA range-proof rejection to the offending party (`crypto/mta/share_protocol.go`, + `ecdsa/signing/round_2.go`). _Provenance: `BNB #332`, PR #4._ ### Notes diff --git a/crypto/dlnproof/proof.go b/crypto/dlnproof/proof.go index 446e92c7..bf9b8aad 100644 --- a/crypto/dlnproof/proof.go +++ b/crypto/dlnproof/proof.go @@ -58,7 +58,7 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { if p == nil { return false } - if N.Sign() != 1 { + if h1 == nil || h2 == nil || N == nil || N.Sign() != 1 { return false } modN := common.ModInt(N) @@ -82,11 +82,7 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { } } for i := range p.Alpha { - if p.Alpha[i] == nil { - return false - } - a := new(big.Int).Mod(p.Alpha[i], N) - if a.Cmp(one) <= 0 || a.Cmp(N) >= 0 { + if p.Alpha[i] == nil || p.Alpha[i].Cmp(one) <= 0 || p.Alpha[i].Cmp(N) >= 0 { return false } } @@ -94,9 +90,6 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { c := common.SHA512_256i_TAGGED(Session, msg...) cIBI := new(big.Int) for i := 0; i < Iterations; i++ { - if p.Alpha[i] == nil || p.T[i] == nil { - return false - } cI := c.Bit(i) cIBI = cIBI.SetInt64(int64(cI)) h1ExpTi := modN.Exp(h1, p.T[i]) diff --git a/crypto/dlnproof/proof_test.go b/crypto/dlnproof/proof_test.go index c319add7..576f16f7 100644 --- a/crypto/dlnproof/proof_test.go +++ b/crypto/dlnproof/proof_test.go @@ -30,6 +30,71 @@ func TestDLNProofVerifyRejectsOverwideT(t *testing.T) { } } +func TestDLNProofVerifyRejectsOverwideAlpha(t *testing.T) { + proof := &Proof{} + for i := 0; i < Iterations; i++ { + proof.Alpha[i] = big.NewInt(2) + proof.T[i] = big.NewInt(2) + } + proof.Alpha[0] = big.NewInt(25) + + if proof.Verify(big.NewInt(2), big.NewInt(3), big.NewInt(23)) { + t.Fatal("Verify must reject Alpha values outside [2, N)") + } +} + +func TestDLNProofVerifyRejectsNilInputs(t *testing.T) { + proof := &Proof{} + for i := 0; i < Iterations; i++ { + proof.Alpha[i] = big.NewInt(2) + proof.T[i] = big.NewInt(2) + } + + if proof.Verify(nil, big.NewInt(3), big.NewInt(23)) { + t.Fatal("Verify must reject nil h1") + } + if proof.Verify(big.NewInt(2), nil, big.NewInt(23)) { + t.Fatal("Verify must reject nil h2") + } + if proof.Verify(big.NewInt(2), big.NewInt(3), nil) { + t.Fatal("Verify must reject nil N") + } +} + +func TestDLNProofVerifyRejectsNilProofElements(t *testing.T) { + proof := &Proof{} + for i := 0; i < Iterations; i++ { + proof.Alpha[i] = big.NewInt(2) + proof.T[i] = big.NewInt(2) + } + + badAlpha := *proof + badAlpha.Alpha[0] = nil + assertNotPanics(t, func() { + if badAlpha.Verify(big.NewInt(2), big.NewInt(3), big.NewInt(23)) { + t.Fatal("Verify must reject nil Alpha values") + } + }) + + badT := *proof + badT.T[0] = nil + assertNotPanics(t, func() { + if badT.Verify(big.NewInt(2), big.NewInt(3), big.NewInt(23)) { + t.Fatal("Verify must reject nil T values") + } + }) +} + +func assertNotPanics(t *testing.T, f func()) { + t.Helper() + defer func() { + if r := recover(); r != nil { + t.Fatalf("unexpected panic: %v", r) + } + }() + f() +} + func assertPanics(t *testing.T, f func()) { t.Helper() defer func() { diff --git a/crypto/ecpoint.go b/crypto/ecpoint.go index 5e975da5..b7ae8eb3 100644 --- a/crypto/ecpoint.go +++ b/crypto/ecpoint.go @@ -55,13 +55,22 @@ func (p *ECPoint) Y() *big.Int { } func (p *ECPoint) Add(p1 *ECPoint) (*ECPoint, error) { + if p == nil || p1 == nil { + return nil, errors.New("Add: point is nil") + } x, y := p.curve.Add(p.X(), p.Y(), p1.X(), p1.Y()) return NewECPoint(p.curve, x, y) } +// ScalarMult returns p * k. If the input is nil, k is nil, or the curve +// operation returns the identity/off-curve coordinates, ScalarMult returns nil +// instead of panicking. Callers must check the returned pointer before use. func (p *ECPoint) ScalarMult(k *big.Int) *ECPoint { + if p == nil || k == nil { + return nil + } x, y := p.curve.ScalarMult(p.X(), p.Y(), k.Bytes()) - newP, _ := NewECPoint(p.curve, x, y) // it must be on the curve, no need to check. + newP, _ := NewECPoint(p.curve, x, y) return newP } @@ -89,6 +98,9 @@ func (p *ECPoint) Equals(p2 *ECPoint) bool { } func (p *ECPoint) SetCurve(curve elliptic.Curve) *ECPoint { + if p == nil { + return nil + } p.curve = curve return p } @@ -98,17 +110,24 @@ func (p *ECPoint) ValidateBasic() bool { } func (p *ECPoint) EightInvEight() *ECPoint { - return p.ScalarMult(eight).ScalarMult(eightInv) + q := p.ScalarMult(eight) + if q == nil { + return nil + } + return q.ScalarMult(eightInv) } func ScalarBaseMult(curve elliptic.Curve, k *big.Int) *ECPoint { + if curve == nil || k == nil { + return nil + } x, y := curve.ScalarBaseMult(k.Bytes()) - p, _ := NewECPoint(curve, x, y) // it must be on the curve, no need to check. + p, _ := NewECPoint(curve, x, y) return p } func isOnCurve(c elliptic.Curve, x, y *big.Int) bool { - if x == nil || y == nil { + if c == nil || x == nil || y == nil { return false } P := c.Params().P diff --git a/crypto/ecpoint_test.go b/crypto/ecpoint_test.go index 60817c21..cb9f0c29 100644 --- a/crypto/ecpoint_test.go +++ b/crypto/ecpoint_test.go @@ -129,6 +129,17 @@ func TestNewECPointRejectsNonCanonicalCoordinates(t *testing.T) { assert.Error(t, err) } +func TestScalarMultReturnsNilForInvalidInputs(t *testing.T) { + curve := tss.EC() + point := ScalarBaseMult(curve, big.NewInt(1)) + + assert.Nil(t, ScalarBaseMult(curve, big.NewInt(0))) + assert.Nil(t, ScalarBaseMult(curve, nil)) + assert.Nil(t, ScalarBaseMult(nil, big.NewInt(1))) + assert.Nil(t, point.ScalarMult(big.NewInt(0))) + assert.Nil(t, point.ScalarMult(nil)) +} + func TestS256EcpointJsonSerialization(t *testing.T) { ec := btcec.S256() tss.RegisterCurve("secp256k1", ec) diff --git a/crypto/mta/proofs.go b/crypto/mta/proofs.go index 51e6e3a6..0128a9f0 100644 --- a/crypto/mta/proofs.go +++ b/crypto/mta/proofs.go @@ -192,7 +192,8 @@ func ProofBobFromBytes(bzs [][]byte) (*ProofBob, error) { func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2 *big.Int, X *crypto.ECPoint, session ...[]byte) bool { Session := optionalProofSession(session) if pf == nil || pf.ProofBob == nil || - pk == nil || NTilde == nil || h1 == nil || h2 == nil || c1 == nil || c2 == nil { + ec == nil || pk == nil || pk.N == nil || + NTilde == nil || h1 == nil || h2 == nil || c1 == nil || c2 == nil { return false } if X != nil { @@ -208,6 +209,8 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, q3 = new(big.Int).Mul(q, q3) q7 := new(big.Int).Mul(q3, q3) q7 = new(big.Int).Mul(q7, q) + // Honest S2/T2 = e*rho + rho' with e < q, rho < q*NTilde, + // rho' < q^3*NTilde, hence S2/T2 < 2*q^3*NTilde. q3NTilde := new(big.Int).Mul(q3, NTilde) maxS2 := new(big.Int).Lsh(q3NTilde, 1) maxT2 := new(big.Int).Set(maxS2) @@ -275,13 +278,13 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, if pf.S1.Cmp(q3) > 0 { return false } - if pf.S2.Cmp(maxS2) > 0 { + if pf.S2.Cmp(maxS2) >= 0 { return false } if pf.T1.Cmp(q7) > 0 { return false } - if pf.T2.Cmp(maxT2) > 0 { + if pf.T2.Cmp(maxT2) >= 0 { return false } @@ -293,7 +296,7 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, if X == nil { eHash = common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, c1, c2, pf.Z, pf.ZPrm, pf.T, pf.V, pf.W)...) } else { - if !tss.SameCurve(ec, X.Curve()) { + if !X.ValidateBasic() || !tss.SameCurve(ec, X.Curve()) { return false } eHash = common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, X.X(), X.Y(), c1, c2, pf.U.X(), pf.U.Y(), pf.Z, pf.ZPrm, pf.T, pf.V, pf.W)...) @@ -307,8 +310,12 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, if X != nil { s1ModQ := new(big.Int).Mod(pf.S1, ec.Params().N) gS1 := crypto.ScalarBaseMult(ec, s1ModQ) - xEU, err := X.ScalarMult(e).Add(pf.U) - if err != nil || !gS1.Equals(xEU) { + xE := X.ScalarMult(e) + if xE == nil { + return false + } + xEU, err := xE.Add(pf.U) + if err != nil || xEU == nil || !gS1.Equals(xEU) { return false } } diff --git a/crypto/mta/range_proof.go b/crypto/mta/range_proof.go index c7ee82c6..2c5c7fd0 100644 --- a/crypto/mta/range_proof.go +++ b/crypto/mta/range_proof.go @@ -104,7 +104,9 @@ func RangeProofAliceFromBytes(bzs [][]byte) (*RangeProofAlice, error) { func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c *big.Int, session ...[]byte) bool { Session := optionalProofSession(session) - if pf == nil || !pf.ValidateBasic() || pk == nil || NTilde == nil || h1 == nil || h2 == nil || c == nil { + if pf == nil || !pf.ValidateBasic() || ec == nil || + pk == nil || pk.N == nil || + NTilde == nil || h1 == nil || h2 == nil || c == nil { return false } if new(big.Int).GCD(nil, nil, c, pk.N).Cmp(one) != 0 { @@ -114,6 +116,8 @@ func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTi q := ec.Params().N q3 := new(big.Int).Mul(q, q) q3 = new(big.Int).Mul(q, q3) + // Honest S2 = e*rho + gamma with e < q, rho < q*NTilde, + // gamma < q^3*NTilde, hence S2 < 2*q^3*NTilde. q3NTilde := new(big.Int).Mul(q3, NTilde) maxS2 := new(big.Int).Lsh(q3NTilde, 1) @@ -164,7 +168,7 @@ func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTi if pf.S1.Cmp(q3) == 1 { return false } - if pf.S2.Cmp(maxS2) > 0 { + if pf.S2.Cmp(maxS2) >= 0 { return false } diff --git a/crypto/mta/share_protocol.go b/crypto/mta/share_protocol.go index 05fb69f0..fe03eaee 100644 --- a/crypto/mta/share_protocol.go +++ b/crypto/mta/share_protocol.go @@ -16,6 +16,11 @@ import ( "github.com/bnb-chain/tss-lib/crypto/paillier" ) +// ErrRangeProofVerify signals that BobMid/BobMidWC rejected the peer-supplied +// RangeProofAlice. Callers can use errors.Is to preserve peer attribution while +// distinguishing proof rejection from local arithmetic failures. +var ErrRangeProofVerify = errors.New("RangeProofAlice.Verify() returned false") + func AliceInit( ec elliptic.Curve, pkA *paillier.PublicKey, @@ -38,7 +43,7 @@ func BobMid( session ...[]byte, ) (beta, cB, betaPrm *big.Int, piB *ProofBob, err error) { if !pf.Verify(ec, pkA, NTildeB, h1B, h2B, cA, session...) { - err = errors.New("RangeProofAlice.Verify() returned false") + err = ErrRangeProofVerify return } q := ec.Params().N @@ -72,7 +77,7 @@ func BobMidWC( session ...[]byte, ) (beta, cB, betaPrm *big.Int, piB *ProofBobWC, err error) { if !pf.Verify(ec, pkA, NTildeB, h1B, h2B, cA, session...) { - err = errors.New("RangeProofAlice.Verify() returned false") + err = ErrRangeProofVerify return } q := ec.Params().N diff --git a/crypto/paillier/factor_proof.go b/crypto/paillier/factor_proof.go index 2302abf6..5ec95e1a 100644 --- a/crypto/paillier/factor_proof.go +++ b/crypto/paillier/factor_proof.go @@ -106,6 +106,48 @@ func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int, session ...[]byte) (bo } } + limit := big.NewInt(1) + limit.Lsh(limit, PARAM_L+PARAM_E) + limit.Mul(limit, new(big.Int).Sqrt(pkN)) + + if pf.Z1.CmpAbs(limit) > 0 { + return false, fmt.Errorf("fac proof verify: z1 = %x exceeds limit %x", pf.Z1, limit) + } + + if pf.Z2.CmpAbs(limit) > 0 { + return false, fmt.Errorf("fac proof verify: z2 = %x exceeds limit %x", pf.Z2, limit) + } + + // FactorProof responses are signed in Threshold's fork. Keep the existing + // inclusive absolute Z1/Z2 bound style, and reject W1/W2/Sigma/V before + // any modular exponentiation so malformed proofs cannot amplify verifier + // CPU cost with over-wide exponents. + q := new(big.Int).Lsh(big.NewInt(1), PARAM_L) + q3 := new(big.Int).Mul(q, q) + q3.Mul(q3, q) + qN := new(big.Int).Mul(q, N) + qPkNN := new(big.Int).Mul(qN, pkN) + q3N := new(big.Int).Mul(q3, N) + q3PkNN := new(big.Int).Mul(q3N, pkN) + limitW := new(big.Int).Lsh(q3N, 1) + limitV := new(big.Int).Lsh(q3PkNN, 2) + + if pf.W1.CmpAbs(limitW) > 0 { + return false, fmt.Errorf("fac proof verify: w1 = %x exceeds limit %x", pf.W1, limitW) + } + + if pf.W2.CmpAbs(limitW) > 0 { + return false, fmt.Errorf("fac proof verify: w2 = %x exceeds limit %x", pf.W2, limitW) + } + + if pf.Sigma.CmpAbs(qPkNN) > 0 { + return false, fmt.Errorf("fac proof verify: sigma = %x exceeds limit %x", pf.Sigma, qPkNN) + } + + if pf.V.CmpAbs(limitV) > 0 { + return false, fmt.Errorf("fac proof verify: v = %x exceeds limit %x", pf.V, limitV) + } + e := FactorChallenge(N, s, t, pkN, pf.P, pf.Q, pf.A, pf.B, pf.T, pf.Sigma, session...) modN := common.ModInt(N) @@ -132,18 +174,6 @@ func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int, session ...[]byte) (bo return false, fmt.Errorf("fac proof verify: Q^z1*t^v = %x != T*R^e = %x", Qz1tv, TRe) } - limit := big.NewInt(1) - limit.Lsh(limit, PARAM_L+PARAM_E) - limit.Mul(limit, new(big.Int).Sqrt(pkN)) - - if pf.Z1.CmpAbs(limit) > 0 { - return false, fmt.Errorf("fac proof verify: z1 = %x exceeds limit %x", pf.Z1, limit) - } - - if pf.Z2.CmpAbs(limit) > 0 { - return false, fmt.Errorf("fac proof verify: z2 = %x exceeds limit %x", pf.Z2, limit) - } - return true, nil } diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index 5ff8cc28..e8b45ce6 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -213,6 +213,61 @@ func TestFactorProofVerifyRejectsNonZeroInvalidBases(t *testing.T) { } } +func TestFactorProofVerifyRejectsOverwideResponses(t *testing.T) { + facSetUp(t) + + q := new(big.Int).Lsh(big.NewInt(1), PARAM_L) + q3 := new(big.Int).Mul(q, q) + q3.Mul(q3, q) + qN := new(big.Int).Mul(q, auxPrime.N) + qPkNN := new(big.Int).Mul(qN, publicKey.N) + q3N := new(big.Int).Mul(q3, auxPrime.N) + q3PkNN := new(big.Int).Mul(q3N, publicKey.N) + limitW := new(big.Int).Lsh(q3N, 1) + limitV := new(big.Int).Lsh(q3PkNN, 2) + + cases := []struct { + name string + mutate func(proof *FactorProof) + }{ + { + name: "W1", + mutate: func(proof *FactorProof) { + proof.W1 = new(big.Int).Add(limitW, big.NewInt(1)) + }, + }, + { + name: "W2", + mutate: func(proof *FactorProof) { + proof.W2 = new(big.Int).Add(limitW, big.NewInt(1)) + }, + }, + { + name: "Sigma", + mutate: func(proof *FactorProof) { + proof.Sigma = new(big.Int).Add(qPkNN, big.NewInt(1)) + }, + }, + { + name: "V", + mutate: func(proof *FactorProof) { + proof.V = new(big.Int).Add(limitV, big.NewInt(1)) + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + proof := privateKey.FactorProof(auxPrime.N, s, tt) + test.mutate(proof) + + res, err := proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + assert.Error(t, err) + assert.False(t, res, "proof verify result must be false") + }) + } +} + func TestFactorProofVerifyFailBadFactors(t *testing.T) { facSetUp(t) proof := badPrivateKey.FactorProof(auxPrime.N, s, tt) diff --git a/crypto/schnorr/schnorr_proof.go b/crypto/schnorr/schnorr_proof.go index 693a9253..d5a2ccd8 100644 --- a/crypto/schnorr/schnorr_proof.go +++ b/crypto/schnorr/schnorr_proof.go @@ -61,19 +61,28 @@ func (pf *ZKProof) Verify(X *crypto.ECPoint) bool { // VerifyWithSession verifies a Schnorr proof with the session bound into the // Fiat-Shamir challenge. func (pf *ZKProof) VerifyWithSession(session []byte, X *crypto.ECPoint) bool { - if pf == nil || !pf.ValidateBasic() { + if pf == nil || !pf.ValidateBasic() || X == nil || !X.ValidateBasic() { return false } ec := X.Curve() ecParams := ec.Params() q := ecParams.N + if !isValidScalar(pf.T, q) { + return false + } g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) cHash := common.SHA512_256i_TAGGED(session, X.X(), X.Y(), g.X(), g.Y(), pf.Alpha.X(), pf.Alpha.Y()) c := common.RejectionSample(q, cHash) + if c.Sign() == 0 { + return false + } tG := crypto.ScalarBaseMult(ec, pf.T) Xc := X.ScalarMult(c) + if tG == nil || Xc == nil { + return false + } aXc, err := pf.Alpha.Add(Xc) if err != nil { return false @@ -82,7 +91,7 @@ func (pf *ZKProof) VerifyWithSession(session []byte, X *crypto.ECPoint) bool { } func (pf *ZKProof) ValidateBasic() bool { - return pf.T != nil && pf.Alpha != nil + return pf.T != nil && pf.Alpha != nil && pf.Alpha.ValidateBasic() } // NewZKProof constructs a new Schnorr ZK proof of knowledge s_i, l_i such that V_i = R^s_i, g^l_i (GG18Spec Fig. 17) @@ -123,22 +132,35 @@ func (pf *ZKVProof) Verify(V, R *crypto.ECPoint) bool { // VerifyWithSession verifies a Schnorr V proof with the session bound into the // Fiat-Shamir challenge. func (pf *ZKVProof) VerifyWithSession(session []byte, V, R *crypto.ECPoint) bool { - if pf == nil || !pf.ValidateBasic() { + if pf == nil || !pf.ValidateBasic() || + V == nil || R == nil || !V.ValidateBasic() || !R.ValidateBasic() { return false } ec := V.Curve() ecParams := ec.Params() q := ecParams.N + if !isValidScalar(pf.T, q) || !isValidScalar(pf.U, q) { + return false + } g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) cHash := common.SHA512_256i_TAGGED(session, V.X(), V.Y(), R.X(), R.Y(), g.X(), g.Y(), pf.Alpha.X(), pf.Alpha.Y()) c := common.RejectionSample(q, cHash) + if c.Sign() == 0 { + return false + } tR := R.ScalarMult(pf.T) uG := crypto.ScalarBaseMult(ec, pf.U) + if tR == nil || uG == nil { + return false + } tRuG, _ := tR.Add(uG) // already on the curve. Vc := V.ScalarMult(c) + if Vc == nil { + return false + } aVc, err := pf.Alpha.Add(Vc) if err != nil { return false @@ -149,3 +171,7 @@ func (pf *ZKVProof) VerifyWithSession(session []byte, V, R *crypto.ECPoint) bool func (pf *ZKVProof) ValidateBasic() bool { return pf.Alpha != nil && pf.T != nil && pf.U != nil && pf.Alpha.ValidateBasic() } + +func isValidScalar(k, q *big.Int) bool { + return k != nil && k.Sign() > 0 && k.Cmp(q) < 0 +} diff --git a/crypto/schnorr/schnorr_proof_test.go b/crypto/schnorr/schnorr_proof_test.go index 33284f1a..e5040e7d 100644 --- a/crypto/schnorr/schnorr_proof_test.go +++ b/crypto/schnorr/schnorr_proof_test.go @@ -7,6 +7,7 @@ package schnorr_test import ( + "math/big" "testing" "github.com/stretchr/testify/assert" @@ -66,6 +67,22 @@ func TestSchnorrProofVerifyBadX(t *testing.T) { assert.False(t, res, "verify result must be false") } +func TestSchnorrProofVerifyRejectsInvalidInputs(t *testing.T) { + q := tss.EC().Params().N + u := common.GetRandomPositiveInt(q) + X := crypto.ScalarBaseMult(tss.EC(), u) + + proof, _ := NewZKProof(u, X) + + assert.False(t, proof.Verify(nil), "nil public point must fail") + assert.False(t, proof.Verify(crypto.NewECPointNoCurveCheck(tss.EC(), big.NewInt(1), big.NewInt(1))), "off-curve public point must fail") + + proof.T = big.NewInt(0) + assert.NotPanics(t, func() { + assert.False(t, proof.Verify(X), "zero proof scalar must fail") + }) +} + func TestSchnorrVProofVerify(t *testing.T) { q := tss.EC().Params().N k := common.GetRandomPositiveInt(q) @@ -82,6 +99,29 @@ func TestSchnorrVProofVerify(t *testing.T) { assert.True(t, res, "verify result must be true") } +func TestSchnorrVProofVerifyRejectsZeroScalars(t *testing.T) { + q := tss.EC().Params().N + k := common.GetRandomPositiveInt(q) + s := common.GetRandomPositiveInt(q) + l := common.GetRandomPositiveInt(q) + R := crypto.ScalarBaseMult(tss.EC(), k) + Rs := R.ScalarMult(s) + lG := crypto.ScalarBaseMult(tss.EC(), l) + V, _ := Rs.Add(lG) + + proof, _ := NewZKVProof(V, R, s, l) + proof.T = big.NewInt(0) + assert.NotPanics(t, func() { + assert.False(t, proof.Verify(V, R), "zero T must fail") + }) + + proof, _ = NewZKVProof(V, R, s, l) + proof.U = big.NewInt(0) + assert.NotPanics(t, func() { + assert.False(t, proof.Verify(V, R), "zero U must fail") + }) +} + func TestSchnorrVProofVerifySessionBinding(t *testing.T) { q := tss.EC().Params().N k := common.GetRandomPositiveInt(q) diff --git a/crypto/vss/feldman_vss.go b/crypto/vss/feldman_vss.go index 8ba7a045..736e8e4e 100644 --- a/crypto/vss/feldman_vss.go +++ b/crypto/vss/feldman_vss.go @@ -92,23 +92,41 @@ func Create(ec elliptic.Curve, threshold int, secret *big.Int, indexes []*big.In } func (share *Share) Verify(ec elliptic.Curve, threshold int, vs Vs) bool { - if share.Threshold != threshold || vs == nil || len(vs) != threshold+1 { + if share == nil || ec == nil || share.ID == nil || share.Share == nil || + share.Threshold != threshold || vs == nil || len(vs) != threshold+1 { + return false + } + q := ec.Params().N + idModQ := new(big.Int).Mod(share.ID, q) + if idModQ.Sign() == 0 || share.Share.Sign() <= 0 || share.Share.Cmp(q) >= 0 { return false } var err error - modQ := common.ModInt(ec.Params().N) + modQ := common.ModInt(q) v, t := vs[0], one // YRO : we need to have our accumulator outside of the loop + if v == nil || !v.SetCurve(ec).ValidateBasic() { + return false + } for j := 1; j <= threshold; j++ { + if vs[j] == nil || !vs[j].SetCurve(ec).ValidateBasic() { + return false + } // t = k_i^j t = modQ.Mul(t, share.ID) // v = v * v_j^t vjt := vs[j].SetCurve(ec).ScalarMult(t) + if vjt == nil { + return false + } v, err = v.SetCurve(ec).Add(vjt) if err != nil { return false } } sigmaGi := crypto.ScalarBaseMult(ec, share.Share) + if sigmaGi == nil { + return false + } return sigmaGi.Equals(v) } diff --git a/crypto/vss/feldman_vss_test.go b/crypto/vss/feldman_vss_test.go index cbb26ee7..8192891d 100644 --- a/crypto/vss/feldman_vss_test.go +++ b/crypto/vss/feldman_vss_test.go @@ -90,6 +90,58 @@ func TestVerify(t *testing.T) { assert.False(t, shares[0].Verify(tss.EC(), threshold, vs[:threshold])) } +func TestVerifyRejectsMalformedShare(t *testing.T) { + num, threshold := 5, 3 + + secret := common.GetRandomPositiveInt(tss.EC().Params().N) + + ids := make([]*big.Int, 0) + for i := 0; i < num; i++ { + ids = append(ids, common.GetRandomPositiveInt(tss.EC().Params().N)) + } + + vs, shares, err := Create(tss.EC(), threshold, secret, ids) + assert.NoError(t, err) + + cases := []struct { + name string + share *Share + }{ + { + name: "zero share", + share: &Share{ + Threshold: shares[0].Threshold, + ID: shares[0].ID, + Share: big.NewInt(0), + }, + }, + { + name: "overwide share", + share: &Share{ + Threshold: shares[0].Threshold, + ID: shares[0].ID, + Share: new(big.Int).Set(tss.EC().Params().N), + }, + }, + { + name: "zero id", + share: &Share{ + Threshold: shares[0].Threshold, + ID: big.NewInt(0), + Share: shares[0].Share, + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + assert.NotPanics(t, func() { + assert.False(t, test.share.Verify(tss.EC(), threshold, vs)) + }) + }) + } +} + func TestReconstruct(t *testing.T) { num, threshold := 5, 3 diff --git a/ecdsa/keygen/messages.go b/ecdsa/keygen/messages.go index 04e32977..930ed5a7 100644 --- a/ecdsa/keygen/messages.go +++ b/ecdsa/keygen/messages.go @@ -82,6 +82,8 @@ func (m *KGRound1Message) ValidateBasic() bool { common.NonEmptyBytes(m.GetCommitment()) && common.NonEmptyBytes(m.GetPaillierN()) && common.NonEmptyBytes(m.GetNTilde()) && + hasBitLen(m.GetPaillierN(), paillierBitsLen) && + hasBitLen(m.GetNTilde(), paillierBitsLen) && common.NonEmptyBytes(m.GetH1()) && common.NonEmptyBytes(m.GetH2()) && m.GetDlnproof_1().ValidateBasic() && @@ -90,6 +92,10 @@ func (m *KGRound1Message) ValidateBasic() bool { m.GetModproofTilde().ValidateBasic() } +func hasBitLen(value []byte, bits int) bool { + return new(big.Int).SetBytes(value).BitLen() == bits +} + func (m *KGRound1Message) UnmarshalCommitment() *big.Int { return new(big.Int).SetBytes(m.GetCommitment()) } diff --git a/ecdsa/keygen/messages_test.go b/ecdsa/keygen/messages_test.go new file mode 100644 index 00000000..f86f1ffc --- /dev/null +++ b/ecdsa/keygen/messages_test.go @@ -0,0 +1,106 @@ +// Copyright © 2019 Binance +// +// This file is part of Binance. The full Binance copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +package keygen + +import ( + "math/big" + "testing" + + "github.com/bnb-chain/tss-lib/crypto/dlnproof" + "github.com/bnb-chain/tss-lib/crypto/paillier" +) + +func TestKGRound1MessageValidateBasicRequiresExactModulusWidth(t *testing.T) { + msg := validKGRound1MessageForValidation() + if !msg.ValidateBasic() { + t.Fatal("expected baseline message to validate") + } + + msg.PaillierN = big.NewInt(1).Bytes() + if msg.ValidateBasic() { + t.Fatal("expected sub-2048-bit Paillier modulus to fail validation") + } + + msg = validKGRound1MessageForValidation() + msg.NTilde = big.NewInt(1).Bytes() + if msg.ValidateBasic() { + t.Fatal("expected sub-2048-bit NTilde modulus to fail validation") + } + + msg = validKGRound1MessageForValidation() + msg.PaillierN = new(big.Int).Lsh(big.NewInt(1), paillierBitsLen).Bytes() + if msg.ValidateBasic() { + t.Fatal("expected over-2048-bit Paillier modulus to fail validation") + } + + msg = validKGRound1MessageForValidation() + msg.NTilde = new(big.Int).Lsh(big.NewInt(1), paillierBitsLen).Bytes() + if msg.ValidateBasic() { + t.Fatal("expected over-2048-bit NTilde modulus to fail validation") + } + + // 2^(paillierBitsLen-1) - 1 has BitLen == paillierBitsLen - 1 (2047), which + // is the just-below boundary that a `<= paillierBitsLen` mutation of + // hasBitLen would silently let through. + belowByOne := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), paillierBitsLen-1), big.NewInt(1)).Bytes() + + msg = validKGRound1MessageForValidation() + msg.PaillierN = belowByOne + if msg.ValidateBasic() { + t.Fatal("expected 2047-bit Paillier modulus to fail validation") + } + + msg = validKGRound1MessageForValidation() + msg.NTilde = belowByOne + if msg.ValidateBasic() { + t.Fatal("expected 2047-bit NTilde modulus to fail validation") + } +} + +func validKGRound1MessageForValidation() *KGRound1Message { + largeModulus := new(big.Int).Lsh(big.NewInt(1), paillierBitsLen-1).Bytes() + + return &KGRound1Message{ + Commitment: []byte{1}, + PaillierN: largeModulus, + NTilde: largeModulus, + H1: []byte{2}, + H2: []byte{3}, + Dlnproof_1: validDLNProofForValidation(), + Dlnproof_2: validDLNProofForValidation(), + Modproof: validModProofForValidation(), + ModproofTilde: validModProofForValidation(), + } +} + +func validDLNProofForValidation() *KGRound1Message_DLNProof { + alpha := make([][]byte, dlnproof.Iterations) + t := make([][]byte, dlnproof.Iterations) + for i := range alpha { + alpha[i] = []byte{1} + t[i] = []byte{1} + } + return &KGRound1Message_DLNProof{Alpha: alpha, T: t} +} + +func validModProofForValidation() *KGRound1Message_ModProof { + x := make([][]byte, paillier.PARAM_M) + a := make([]bool, paillier.PARAM_M) + b := make([]bool, paillier.PARAM_M) + z := make([][]byte, paillier.PARAM_M) + for i := range x { + x[i] = []byte{1} + z[i] = []byte{1} + } + return &KGRound1Message_ModProof{ + W: []byte{1}, + X: x, + A: a, + B: b, + Z: z, + } +} diff --git a/ecdsa/resharing/messages_test.go b/ecdsa/resharing/messages_test.go index 9f089d3f..2ccddc10 100644 --- a/ecdsa/resharing/messages_test.go +++ b/ecdsa/resharing/messages_test.go @@ -96,6 +96,23 @@ func TestDGRound2Message1ValidateBasicRequiresExactModulusWidth(t *testing.T) { if msg.ValidateBasic() { t.Fatal("expected over-2048-bit NTilde modulus to fail validation") } + + // 2^(paillierBitsLen-1) - 1 has BitLen == paillierBitsLen - 1 (2047), which + // is the just-below boundary that a `<= paillierBitsLen` mutation of + // hasBitLen would silently let through. + belowByOne := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), paillierBitsLen-1), big.NewInt(1)).Bytes() + + msg = validDGRound2Message1ForValidation() + msg.PaillierN = belowByOne + if msg.ValidateBasic() { + t.Fatal("expected 2047-bit Paillier modulus to fail validation") + } + + msg = validDGRound2Message1ForValidation() + msg.NTilde = belowByOne + if msg.ValidateBasic() { + t.Fatal("expected 2047-bit NTilde modulus to fail validation") + } } func validDGRound2Message1ForValidation() *DGRound2Message1 { diff --git a/ecdsa/signing/round_2.go b/ecdsa/signing/round_2.go index 4433ef12..e63a7760 100644 --- a/ecdsa/signing/round_2.go +++ b/ecdsa/signing/round_2.go @@ -32,6 +32,12 @@ func (round *round2) Start() *tss.Error { wg := sync.WaitGroup{} wg.Add((len(round.Parties().IDs()) - 1) * 2) contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) + attributeBobMidErr := func(err error, Pj *tss.PartyID) *tss.Error { + if errors.Is(err, mta.ErrRangeProofVerify) { + return round.WrapError(errorspkg.Wrap(err, "peer RangeProofAlice rejected"), Pj) + } + return round.WrapError(errorspkg.Wrap(err, "BobMid arithmetic failure"), Pj) + } for j, Pj := range round.Parties().IDs() { if j == i { continue @@ -63,7 +69,7 @@ func (round *round2) Start() *tss.Error { round.temp.c1jis[j] = c1ji round.temp.pi1jis[j] = pi1ji if err != nil { - errChs <- round.WrapError(err, Pj) + errChs <- attributeBobMidErr(err, Pj) } }(j, Pj) // Bob_mid_wc @@ -93,7 +99,7 @@ func (round *round2) Start() *tss.Error { round.temp.c2jis[j] = c2ji round.temp.pi2jis[j] = pi2ji if err != nil { - errChs <- round.WrapError(err, Pj) + errChs <- attributeBobMidErr(err, Pj) } }(j, Pj) } diff --git a/ecdsa/signing/round_4.go b/ecdsa/signing/round_4.go index 8955a72e..b8b8467b 100644 --- a/ecdsa/signing/round_4.go +++ b/ecdsa/signing/round_4.go @@ -41,6 +41,9 @@ func (round *round4) Start() *tss.Error { // compute the multiplicative inverse thelta mod q thetaInverse = modN.ModInverse(thetaInverse) + if thetaInverse == nil { + return round.WrapError(errors.New("theta inverse is nil")) + } i := round.PartyID().Index contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) piGamma, err := schnorr.NewZKProofWithSession(contextI, round.temp.gamma, round.temp.pointGamma) diff --git a/ecdsa/signing/round_9.go b/ecdsa/signing/round_9.go index bcd37035..935a7104 100644 --- a/ecdsa/signing/round_9.go +++ b/ecdsa/signing/round_9.go @@ -8,11 +8,25 @@ package signing import ( "errors" + "math/big" "github.com/bnb-chain/tss-lib/crypto/commitments" "github.com/bnb-chain/tss-lib/tss" ) +// decommitFour verifies cmt and returns its four secret values. It rejects +// any commitment whose DeCommit reports failure OR whose length differs from +// four; both conditions must hold because an attacker controls both C and D +// in their own messages and could otherwise commit to a longer payload, then +// have round 9 silently read values[0..3] as attacker-chosen point coordinates. +func decommitFour(cmt commitments.HashCommitDecommit) ([]*big.Int, bool) { + ok, values := cmt.DeCommit() + if !ok || len(values) != 4 { + return nil, false + } + return values, true +} + func (round *round9) Start() *tss.Error { if round.started { return round.WrapError(errors.New("round already started")) @@ -31,9 +45,8 @@ func (round *round9) Start() *tss.Error { r7msg := round.temp.signRound7Messages[j].Content().(*SignRound7Message) r8msg := round.temp.signRound8Messages[j].Content().(*SignRound8Message) cj, dj := r7msg.UnmarshalCommitment(), r8msg.UnmarshalDeCommitment() - cmt := commitments.HashCommitDecommit{C: cj, D: dj} - ok, values := cmt.DeCommit() - if !ok && len(values) != 4 { + values, ok := decommitFour(commitments.HashCommitDecommit{C: cj, D: dj}) + if !ok { return round.WrapError(errors.New("de-commitment for bigVj and bigAj failed"), Pj) } UjX, UjY, TjX, TjY := values[0], values[1], values[2], values[3] diff --git a/ecdsa/signing/round_9_test.go b/ecdsa/signing/round_9_test.go new file mode 100644 index 00000000..1a754b8a --- /dev/null +++ b/ecdsa/signing/round_9_test.go @@ -0,0 +1,56 @@ +// Copyright © 2019 Binance +// +// This file is part of Binance. The full Binance copyright notice, including +// terms governing use, modification, and redistribution, is contained in the +// file LICENSE at the root of the source code distribution tree. + +package signing + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bnb-chain/tss-lib/crypto/commitments" +) + +func TestDecommitFour(t *testing.T) { + secrets := func(n int) []*big.Int { + out := make([]*big.Int, n) + for i := range out { + out[i] = big.NewInt(int64(i + 1)) + } + return out + } + + t.Run("accepts exactly four secrets", func(t *testing.T) { + cmt := commitments.NewHashCommitment(secrets(4)...) + values, ok := decommitFour(commitments.HashCommitDecommit{C: cmt.C, D: cmt.D}) + assert.True(t, ok) + assert.Len(t, values, 4) + }) + + // Without the length guard, an attacker can send a commitment binding to + // more (or fewer) than four secrets and have round 9 silently take + // values[0..3] as their attacker-chosen Uj/Tj coordinates, bypassing the + // U==T integrity check without breaking any hash. + t.Run("rejects three secrets", func(t *testing.T) { + cmt := commitments.NewHashCommitment(secrets(3)...) + _, ok := decommitFour(commitments.HashCommitDecommit{C: cmt.C, D: cmt.D}) + assert.False(t, ok) + }) + + t.Run("rejects six secrets", func(t *testing.T) { + cmt := commitments.NewHashCommitment(secrets(6)...) + _, ok := decommitFour(commitments.HashCommitDecommit{C: cmt.C, D: cmt.D}) + assert.False(t, ok) + }) + + t.Run("rejects mismatched commitment", func(t *testing.T) { + cmt := commitments.NewHashCommitment(secrets(4)...) + corrupted := new(big.Int).Add(cmt.C, big.NewInt(1)) + _, ok := decommitFour(commitments.HashCommitDecommit{C: corrupted, D: cmt.D}) + assert.False(t, ok) + }) +}