From db3fca8d6d3bd244b02cda3f918f1888bb5668e6 Mon Sep 17 00:00:00 2001 From: maclane Date: Sun, 17 May 2026 11:42:42 -0500 Subject: [PATCH 01/24] Integrate BNB tss-lib hardening --- BNB_HARDENING_INTEGRATION.md | 68 +++++++++++++++++ common/hash.go | 50 +++++++++++++ common/hash_utils.go | 7 ++ common/int.go | 10 +++ crypto/dlnproof/proof.go | 17 ++++- crypto/ecpoint.go | 4 + crypto/ecpoint_test.go | 9 +++ crypto/mta/proofs.go | 101 ++++++++++++++++++++++---- crypto/mta/range_proof.go | 52 ++++++++++++- crypto/mta/range_proof_test.go | 59 +++++++++++++++ crypto/mta/share_protocol.go | 29 +++++--- crypto/paillier/factor_proof.go | 15 ++-- crypto/paillier/factor_proof_test.go | 18 +++++ crypto/paillier/mod_proof.go | 17 +++-- crypto/paillier/mod_proof_test.go | 18 +++++ crypto/schnorr/schnorr_proof.go | 36 ++++++++- crypto/schnorr/schnorr_proof_test.go | 31 ++++++++ crypto/vss/feldman_vss.go | 4 +- crypto/vss/feldman_vss_test.go | 7 +- ecdsa/keygen/local_party.go | 2 + ecdsa/keygen/local_party_test.go | 4 +- ecdsa/keygen/round_1.go | 22 ++++-- ecdsa/keygen/round_2.go | 24 +++--- ecdsa/keygen/round_3.go | 11 ++- ecdsa/keygen/rounds.go | 16 ++++ ecdsa/keygen/verifier.go | 12 ++- ecdsa/keygen/verifier_test.go | 29 ++++++++ ecdsa/resharing/local_party.go | 2 + ecdsa/resharing/round_1_old_step_1.go | 10 ++- ecdsa/resharing/round_2_new_step_1.go | 31 +++++--- ecdsa/resharing/round_4_new_step_2.go | 16 ++-- ecdsa/resharing/round_5_new_step_3.go | 6 +- ecdsa/resharing/rounds.go | 17 +++++ ecdsa/signing/finalize.go | 10 ++- ecdsa/signing/local_party.go | 22 ++++-- ecdsa/signing/local_party_test.go | 12 ++- ecdsa/signing/round_1.go | 13 +++- ecdsa/signing/round_2.go | 15 +++- ecdsa/signing/round_3.go | 13 +++- ecdsa/signing/round_4.go | 10 ++- ecdsa/signing/round_5.go | 10 ++- ecdsa/signing/round_6.go | 14 +++- ecdsa/signing/round_7.go | 11 ++- ecdsa/signing/round_8.go | 6 +- ecdsa/signing/round_9.go | 6 +- ecdsa/signing/rounds.go | 25 +++++++ eddsa/keygen/local_party_test.go | 4 +- eddsa/keygen/round_1.go | 6 +- eddsa/keygen/round_2.go | 9 ++- eddsa/resharing/round_1_old_step_1.go | 10 ++- eddsa/resharing/round_2_new_step_1.go | 6 +- eddsa/resharing/round_4_new_step_2.go | 6 +- eddsa/signing/finalize.go | 10 ++- eddsa/signing/local_party.go | 9 ++- eddsa/signing/local_party_test.go | 12 ++- eddsa/signing/round_1.go | 6 +- eddsa/signing/round_2.go | 6 +- eddsa/signing/round_3.go | 16 +++- tss/curve.go | 7 ++ tss/params.go | 20 +++++ tss/party.go | 5 +- 61 files changed, 889 insertions(+), 164 deletions(-) create mode 100644 BNB_HARDENING_INTEGRATION.md diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md new file mode 100644 index 000000000..e3be491f0 --- /dev/null +++ b/BNB_HARDENING_INTEGRATION.md @@ -0,0 +1,68 @@ +# BNB Hardening Integration Report + +## Scope + +- Threshold base: `2e712689cfbeefede15f95a0ec7112227d86f702` +- BNB upstream head compared: `3f677ff761fcf692edb0243a5d812930844d879a` +- Common ancestor: `afbe264b44b63155a864dbe0171040c66e442963` +- Goal: port applicable security and correctness hardening without replacing Threshold's Paillier/NTilde remediation or weakening `ModProof`/`FactorProof`. + +## Ported Or Manually Adapted + +- `3d95e54` / PR `#252`, ECDSA protocol security updates: manually adapted tagged challenges, MtA/range-proof validation, session-context plumbing, and proof boundary checks while preserving Threshold's existing Paillier/NTilde proof model. +- `1a14f3a` / PR `#256`, ECDSA proof session byte: manually adapted proof-session APIs for DLN, Schnorr, MtA, Paillier mod proof, and factor proof. Public callers remain backward compatible through variadic session parameters. +- `ff989bf` / PR `#257`, tagged hash encoding: ported length-delimited tagged hashing as `common.SHA512_256i_TAGGED`. +- `f3aad28` / PR `#276`, nil `String()` panic: ported `BaseParty.String()` nil-round guard. +- `409542e` / PR `#282`, round update correctness: ported the `round.ok` accumulation fix for ECDSA/EdDSA keygen, signing, and resharing rounds, plus the resharing party-0 broadcast nil guard. +- `9acd90b`, `2f294cf`, `6b92e7d`, `c0de534` / PR `#284`, leading-zero message signing: manually adapted for ECDSA and EdDSA with backward-compatible variadic `fullBytesLen` parameters. EdDSA now also hashes the full-length message bytes in round 3. +- `843de68` / PR `#291`, VSS threshold-size validation: ported `len(vs) == threshold+1` verification and added test coverage. +- `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. +- `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. +- `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce` and ECDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. +- `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. +- `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. + +## Already Covered Or Superseded + +- `c84c096` / PR `#323`, modproof checker: Threshold's Paillier proof implementation already had the key Jacobi/non-prime validation from its GHSA-h24c-6p6p-m3vx remediation. Threshold's stronger `ModProof` coverage for both Paillier `N` and `NTilde` was kept. +- `e0e4299`, EdDSA keygen error aggregation: current Threshold code already had the corrected behavior. +- `0629cff`, `773b6af`, `b7b73a0`, `27922e0`, `4c83ace`, `c8136c9`, `8a87b22`, `f67a429`, `002397d`, `28d0622`, `bddf60d`: style, comment, logging, gofmt, minor optimization, or merge-only changes with no security behavior to port. + +## Skipped + +- `faf1884`, `c23246e`: module path bumps to `/v2` and `/v3`; skipped to preserve Threshold compatibility. +- `fbb0ef7`: changes `SignatureData` channels to pointers; skipped as public API churn not required for the hardening. +- `b8d526d`, `8abf1d5`, `6c233c6`, `87f7e12`: dependency and random-source API churn; skipped except where existing Threshold APIs already supported the needed behavior. +- `7113b68`, `d0325a1`, `dca2ac4`: repository metadata, CI, or security-report housekeeping. +- `3709c25`, `7a10240`, `0735081`, `3f677ff` / PR `#328`: broad optional constant-time framework. Not ported in this pass because it adds a new dependency, broad Paillier/MtA rewrites, and is default-disabled upstream. Treat as a separate follow-up security project with benchmarking and side-channel review. +- Merge-only commits `b15a0cf`, `c76a1a5`, `b79b349`, `d6e2aa9`, `ba5b734`: no direct changes to port beyond their constituent commits above. + +## Semantic Differences From BNB + +- Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation was retained. No BNB no-proof escape hatches were introduced. +- Session parameters were added as variadic arguments to preserve existing public call sites. BNB's newer APIs are more breaking. +- Keygen and resharing SSIDs are locally derived and use `Parameters.SessionNonce()` when set. This avoids protobuf/module churn, but callers must provide a unique agreed nonce for keygen/resharing sessions that need cross-session replay resistance. +- ECDSA resharing SSID binding was adapted without adding BNB's newer wire-level SSID message fields. +- Constant-time operations are not included and remain a residual follow-up. + +## Tests + +- `go test ./crypto/... ./ecdsa/keygen ./ecdsa/signing ./eddsa/signing` passed. +- `go test ./eddsa/keygen ./eddsa/resharing` passed after updating EdDSA VSS threshold tests and resharing nil guard. +- `go test ./ecdsa/resharing` passed after the analogous resharing guard. +- `go test ./...` passed. + +Added or updated focused tests cover: + +- DLN, Schnorr, Paillier mod proof, factor proof, and MtA range proof session mismatch/replay failures. +- MtA range-proof malformed ciphertext and proof-value boundary failures. +- VSS `threshold+1` verification/reconstruction behavior. +- Non-canonical EC coordinate rejection. +- ECDSA and EdDSA leading-zero message signing. + +## Residual Risks + +- Applications must call `SetSessionNonce` for keygen/resharing if they need unique SSIDs across otherwise identical party sets. +- The optional constant-time upstream work is not integrated. +- Resharing SSID binding is adapted locally rather than wire-compatible with BNB's latest protocol messages. diff --git a/common/hash.go b/common/hash.go index e26b2fc6d..e48a58006 100644 --- a/common/hash.go +++ b/common/hash.go @@ -93,6 +93,56 @@ func SHA512_256i(in ...*big.Int) *big.Int { return new(big.Int).SetBytes(state.Sum(nil)) } +// SHA512_256i_TAGGED is a domain-separated variant of SHA512_256i. The tag is +// hashed and prepended twice, matching the tagged-hash construction used by BNB +// upstream for proof challenges. +func SHA512_256i_TAGGED(tag []byte, in ...*big.Int) *big.Int { + tagBz := SHA512_256(tag) + state := crypto.SHA512_256.New() + if _, err := state.Write(tagBz); err != nil { + Logger.Errorf("SHA512_256i_TAGGED Write(tag) failed: %v", err) + return nil + } + if _, err := state.Write(tagBz); err != nil { + Logger.Errorf("SHA512_256i_TAGGED Write(tag) failed: %v", err) + return nil + } + + inLen := len(in) + if inLen == 0 { + return nil + } + + bzSize := 0 + inLenBz := make([]byte, 64/8) + binary.LittleEndian.PutUint64(inLenBz, uint64(inLen)) + ptrs := make([][]byte, inLen) + for i, n := range in { + if n == nil { + ptrs[i] = zero.Bytes() + } else { + ptrs[i] = n.Bytes() + } + bzSize += len(ptrs[i]) + } + + dataCap := len(inLenBz) + bzSize + inLen + (inLen * 8) + data := make([]byte, 0, dataCap) + data = append(data, inLenBz...) + for i := range in { + data = append(data, ptrs[i]...) + data = append(data, hashInputDelimiter) + dataLen := make([]byte, 8) + binary.LittleEndian.PutUint64(dataLen, uint64(len(ptrs[i]))) + data = append(data, dataLen...) + } + if _, err := state.Write(data); err != nil { + Logger.Errorf("SHA512_256i_TAGGED Write(data) failed: %v", err) + return nil + } + return new(big.Int).SetBytes(state.Sum(nil)) +} + func SHA512_256iOne(in *big.Int) *big.Int { var data []byte state := crypto.SHA512_256.New() diff --git a/common/hash_utils.go b/common/hash_utils.go index 12a0037dd..1d3a88c3d 100644 --- a/common/hash_utils.go +++ b/common/hash_utils.go @@ -21,6 +21,13 @@ func LiterallyJustMod(q *big.Int, eHash *big.Int) *big.Int { // e' = eHash return e } +// RejectionSample implements the upstream challenge reduction name. For the +// secp256k1 and ed25519 group orders used here this has the same behavior as +// LiterallyJustMod. +func RejectionSample(q *big.Int, eHash *big.Int) *big.Int { + return LiterallyJustMod(q, eHash) +} + // Return a big.Int between 0 and N func HashToN(N *big.Int, in ...*big.Int) *big.Int { bitCnt := N.BitLen() diff --git a/common/int.go b/common/int.go index e6762534d..bfe6e7825 100644 --- a/common/int.go +++ b/common/int.go @@ -100,6 +100,16 @@ func (mi *modInt) i() *big.Int { return (*big.Int)(mi) } +func IsInInterval(b *big.Int, bound *big.Int) bool { + return b != nil && bound != nil && b.Cmp(bound) < 0 && b.Cmp(zero) >= 0 +} + +func AppendBigIntToBytesSlice(commonBytes []byte, appended *big.Int) []byte { + resultBytes := make([]byte, len(commonBytes), len(commonBytes)+len(appended.Bytes())) + copy(resultBytes, commonBytes) + return append(resultBytes, appended.Bytes()...) +} + // Marshal the given bigint into bytes. // with the sign stored in the first byte and the absolute value in the rest. // `nil` or 0 is stored as the byte 0x00. diff --git a/crypto/dlnproof/proof.go b/crypto/dlnproof/proof.go index b5d5f7749..124c41c3d 100644 --- a/crypto/dlnproof/proof.go +++ b/crypto/dlnproof/proof.go @@ -31,7 +31,8 @@ var ( one = big.NewInt(1) ) -func NewDLNProof(h1, h2, x, p, q, N *big.Int) *Proof { +func NewDLNProof(h1, h2, x, p, q, N *big.Int, session ...[]byte) *Proof { + Session := optionalSession(session) pMulQ := new(big.Int).Mul(p, q) modN, modPQ := common.ModInt(N), common.ModInt(pMulQ) a := make([]*big.Int, Iterations) @@ -41,7 +42,7 @@ func NewDLNProof(h1, h2, x, p, q, N *big.Int) *Proof { alpha[i] = modN.Exp(h1, a[i]) } msg := append([]*big.Int{h1, h2, N}, alpha[:]...) - c := common.SHA512_256i(msg...) + c := common.SHA512_256i_TAGGED(Session, msg...) t := [Iterations]*big.Int{} cIBI := new(big.Int) for i := range t { @@ -52,7 +53,8 @@ func NewDLNProof(h1, h2, x, p, q, N *big.Int) *Proof { return &Proof{alpha, t} } -func (p *Proof) Verify(h1, h2, N *big.Int) bool { +func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { + Session := optionalSession(session) if p == nil { return false } @@ -87,7 +89,7 @@ func (p *Proof) Verify(h1, h2, N *big.Int) bool { } } msg := append([]*big.Int{h1, h2, N}, p.Alpha[:]...) - c := common.SHA512_256i(msg...) + 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 { @@ -105,6 +107,13 @@ func (p *Proof) Verify(h1, h2, N *big.Int) bool { return true } +func optionalSession(session [][]byte) []byte { + if len(session) == 0 { + return nil + } + return session[0] +} + func UnmarshalDLNProof(alphas, ts [][]byte) (*Proof, error) { if len1 := len(alphas); len1 != Iterations { return nil, fmt.Errorf("UnmarshalDLNProof expected %d Alphas but received %d", Iterations, len1) diff --git a/crypto/ecpoint.go b/crypto/ecpoint.go index 879dbf489..5e975da5b 100644 --- a/crypto/ecpoint.go +++ b/crypto/ecpoint.go @@ -111,6 +111,10 @@ func isOnCurve(c elliptic.Curve, x, y *big.Int) bool { if x == nil || y == nil { return false } + P := c.Params().P + if x.Sign() < 0 || x.Cmp(P) >= 0 || y.Sign() < 0 || y.Cmp(P) >= 0 { + return false + } return c.IsOnCurve(x, y) } diff --git a/crypto/ecpoint_test.go b/crypto/ecpoint_test.go index b3d518e69..60817c21f 100644 --- a/crypto/ecpoint_test.go +++ b/crypto/ecpoint_test.go @@ -120,6 +120,15 @@ func TestUnFlattenECPoints(t *testing.T) { } } +func TestNewECPointRejectsNonCanonicalCoordinates(t *testing.T) { + curve := tss.EC() + gx, gy := curve.ScalarBaseMult(big.NewInt(1).Bytes()) + nonCanonicalX := new(big.Int).Add(gx, curve.Params().P) + + _, err := NewECPoint(curve, nonCanonicalX, gy) + assert.Error(t, err) +} + 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 4e9baefab..03173ca33 100644 --- a/crypto/mta/proofs.go +++ b/crypto/mta/proofs.go @@ -15,6 +15,7 @@ import ( "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto" "github.com/bnb-chain/tss-lib/crypto/paillier" + "github.com/bnb-chain/tss-lib/tss" ) const ( @@ -35,7 +36,8 @@ type ( // ProveBobWC implements Bob's proof both with or without check "ProveMtawc_Bob" and "ProveMta_Bob" used in the MtA protocol from GG18Spec (9) Figs. 10 & 11. // an absent `X` generates the proof without the X consistency check X = g^x -func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2, x, y, r *big.Int, X *crypto.ECPoint) (*ProofBobWC, error) { +func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2, x, y, r *big.Int, X *crypto.ECPoint, session ...[]byte) (*ProofBobWC, error) { + Session := optionalProofSession(session) if pk == nil || NTilde == nil || h1 == nil || h2 == nil || c1 == nil || c2 == nil || x == nil || y == nil || r == nil { return nil, errors.New("ProveBob() received a nil argument") } @@ -45,6 +47,8 @@ func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c q := ec.Params().N q3 := new(big.Int).Mul(q, q) q3 = new(big.Int).Mul(q, q3) + q7 := new(big.Int).Mul(q3, q3) + q7 = new(big.Int).Mul(q7, q) qNTilde := new(big.Int).Mul(q, NTilde) q3NTilde := new(big.Int).Mul(q3, NTilde) @@ -55,14 +59,14 @@ func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c // 2. rho := common.GetRandomPositiveInt(qNTilde) sigma := common.GetRandomPositiveInt(qNTilde) - tau := common.GetRandomPositiveInt(qNTilde) + tau := common.GetRandomPositiveInt(q3NTilde) // 3. rhoPrm := common.GetRandomPositiveInt(q3NTilde) // 4. beta := common.GetRandomPositiveRelativelyPrimeInt(pk.N) - gamma := common.GetRandomPositiveRelativelyPrimeInt(pk.N) + gamma := common.GetRandomPositiveInt(q7) // 5. u := crypto.NewECPointNoCurveCheck(ec, zero, zero) // initialization suppresses an IDE warning @@ -95,13 +99,15 @@ func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c // 11-12. e' var e *big.Int - { + { // must use RejectionSample + var eHash *big.Int // X is nil if called by ProveBob (Bob's proof "without check") if X == nil { - e = common.HashToN(q, append(pk.AsInts(), c1, c2, z, zPrm, t, v, w)...) + eHash = common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, c1, c2, z, zPrm, t, v, w)...) } else { - e = common.HashToN(q, append(pk.AsInts(), X.X(), X.Y(), c1, c2, u.X(), u.Y(), z, zPrm, t, v, w)...) + eHash = common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, X.X(), X.Y(), c1, c2, u.X(), u.Y(), z, zPrm, t, v, w)...) } + e = common.RejectionSample(q, eHash) } // 13. @@ -133,10 +139,10 @@ func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c } // ProveBob implements Bob's proof "ProveMta_Bob" used in the MtA protocol from GG18Spec (9) Fig. 11. -func ProveBob(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2, x, y, r *big.Int) (*ProofBob, error) { +func ProveBob(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2, x, y, r *big.Int, session ...[]byte) (*ProofBob, error) { // the Bob proof ("with check") contains the ProofBob "without check"; this method extracts and returns it // X is supplied as nil to exclude it from the proof hash - pf, err := ProveBobWC(ec, pk, NTilde, h1, h2, c1, c2, x, y, r, nil) + pf, err := ProveBobWC(ec, pk, NTilde, h1, h2, c1, c2, x, y, r, nil, session...) if err != nil { return nil, err } @@ -183,15 +189,53 @@ func ProofBobFromBytes(bzs [][]byte) (*ProofBob, error) { // ProveBobWC.Verify implements verification of Bob's proof with check "VerifyMtawc_Bob" used in the MtA protocol from GG18Spec (9) Fig. 10. // an absent `X` verifies a proof generated without the X consistency check X = g^x -func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2 *big.Int, X *crypto.ECPoint) bool { - if pk == nil || NTilde == nil || h1 == nil || h2 == nil || c1 == nil || c2 == nil { +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 || !pf.ProofBob.ValidateBasic() || + (X != nil && (pf.U == nil || !pf.U.ValidateBasic())) || + pk == nil || NTilde == nil || h1 == nil || h2 == nil || c1 == nil || c2 == nil { return false } q := ec.Params().N q3 := new(big.Int).Mul(q, q) q3 = new(big.Int).Mul(q, q3) + q7 := new(big.Int).Mul(q3, q3) + q7 = new(big.Int).Mul(q7, q) + if !common.IsInInterval(pf.Z, NTilde) { + return false + } + if !common.IsInInterval(pf.ZPrm, NTilde) { + return false + } + if !common.IsInInterval(pf.T, NTilde) { + return false + } + if !common.IsInInterval(pf.V, pk.NSquare()) { + return false + } + if !common.IsInInterval(pf.W, NTilde) { + return false + } + if !common.IsInInterval(pf.S, pk.N) { + return false + } + if new(big.Int).GCD(nil, nil, pf.Z, NTilde).Cmp(one) != 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.ZPrm, NTilde).Cmp(one) != 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.T, NTilde).Cmp(one) != 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.V, pk.NSquare()).Cmp(one) != 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.W, NTilde).Cmp(one) != 0 { + return false + } gcd := big.NewInt(0) if pf.S.Cmp(zero) == 0 { return false @@ -205,21 +249,41 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, if gcd.GCD(nil, nil, pf.V, pk.N).Cmp(one) != 0 { return false } + if pf.S1.Cmp(q) == -1 { + return false + } + if pf.S2.Cmp(q) == -1 { + return false + } + if pf.T1.Cmp(q) == -1 { + return false + } + if pf.T2.Cmp(q) == -1 { + return false + } // 3. if pf.S1.Cmp(q3) > 0 { return false } + if pf.T1.Cmp(q7) > 0 { + return false + } // 1-2. e' var e *big.Int - { + { // must use RejectionSample + var eHash *big.Int // X is nil if called on a ProveBob (Bob's proof "without check") if X == nil { - e = common.HashToN(q, append(pk.AsInts(), c1, c2, pf.Z, pf.ZPrm, pf.T, pf.V, pf.W)...) + 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 { - e = common.HashToN(q, append(pk.AsInts(), X.X(), X.Y(), c1, c2, pf.U.X(), pf.U.Y(), pf.Z, pf.ZPrm, pf.T, pf.V, pf.W)...) + if !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)...) } + e = common.RejectionSample(q, eHash) } var left, right *big.Int // for the following conditionals @@ -278,12 +342,19 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, } // ProveBob.Verify implements verification of Bob's proof without check "VerifyMta_Bob" used in the MtA protocol from GG18Spec (9) Fig. 11. -func (pf *ProofBob) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2 *big.Int) bool { +func (pf *ProofBob) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c2 *big.Int, session ...[]byte) bool { if pf == nil { return false } pfWC := &ProofBobWC{ProofBob: pf, U: nil} - return pfWC.Verify(ec, pk, NTilde, h1, h2, c1, c2, nil) + return pfWC.Verify(ec, pk, NTilde, h1, h2, c1, c2, nil, session...) +} + +func optionalProofSession(session [][]byte) []byte { + if len(session) == 0 { + return nil + } + return session[0] } func (pf *ProofBob) ValidateBasic() bool { diff --git a/crypto/mta/range_proof.go b/crypto/mta/range_proof.go index 0cff6703d..f4bbc4668 100644 --- a/crypto/mta/range_proof.go +++ b/crypto/mta/range_proof.go @@ -32,7 +32,8 @@ type ( ) // ProveRangeAlice implements Alice's range proof used in the MtA and MtAwc protocols from GG18Spec (9) Fig. 9. -func ProveRangeAlice(ec elliptic.Curve, pk *paillier.PublicKey, c, NTilde, h1, h2, m, r *big.Int) (*RangeProofAlice, error) { +func ProveRangeAlice(ec elliptic.Curve, pk *paillier.PublicKey, c, NTilde, h1, h2, m, r *big.Int, session ...[]byte) (*RangeProofAlice, error) { + Session := optionalProofSession(session) if pk == nil || NTilde == nil || h1 == nil || h2 == nil || c == nil || m == nil || r == nil { return nil, errors.New("ProveRangeAlice constructor received nil value(s)") } @@ -69,7 +70,8 @@ func ProveRangeAlice(ec elliptic.Curve, pk *paillier.PublicKey, c, NTilde, h1, h w = modNTilde.Mul(w, modNTilde.Exp(h2, gamma)) // 8-9. e' - e := common.HashToN(q, append(pk.AsInts(), c, z, u, w)...) + eHash := common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, c, z, u, w)...) + e := common.RejectionSample(q, eHash) modN := common.ModInt(pk.N) s := modN.Exp(r, e) @@ -100,22 +102,64 @@ func RangeProofAliceFromBytes(bzs [][]byte) (*RangeProofAlice, error) { }, nil } -func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c *big.Int) bool { +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 { return false } + if new(big.Int).GCD(nil, nil, c, pk.N).Cmp(one) != 0 { + return false + } q := ec.Params().N q3 := new(big.Int).Mul(q, q) q3 = new(big.Int).Mul(q, q3) + if !common.IsInInterval(pf.Z, NTilde) { + return false + } + if !common.IsInInterval(pf.U, pk.NSquare()) { + return false + } + if !common.IsInInterval(pf.W, NTilde) { + return false + } + if !common.IsInInterval(pf.S, pk.N) { + return false + } + if new(big.Int).GCD(nil, nil, pf.Z, NTilde).Cmp(one) != 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.U, pk.NSquare()).Cmp(one) != 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.W, NTilde).Cmp(one) != 0 { + return false + } + if pf.S1.Cmp(q) == -1 { + return false + } + if pf.S2.Cmp(q) == -1 { + return false + } + if pf.S.Cmp(one) == 0 { + return false + } + if pf.Z.Cmp(one) == 0 { + return false + } + if pf.S1.Cmp(pf.S2) == 0 { + return false + } + // 3. if pf.S1.Cmp(q3) == 1 { return false } // 1-2. e' - e := common.HashToN(q, append(pk.AsInts(), c, pf.Z, pf.U, pf.W)...) + eHash := common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, c, pf.Z, pf.U, pf.W)...) + e := common.RejectionSample(q, eHash) var products *big.Int // for the following conditionals minusE := new(big.Int).Sub(zero, e) diff --git a/crypto/mta/range_proof_test.go b/crypto/mta/range_proof_test.go index b8cac1e38..53e920df8 100644 --- a/crypto/mta/range_proof_test.go +++ b/crypto/mta/range_proof_test.go @@ -47,3 +47,62 @@ func TestProveRangeAlice(t *testing.T) { ok := proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, c) assert.True(t, ok, "proof must verify") } + +func TestProveRangeAliceSessionBinding(t *testing.T) { + q := tss.EC().Params().N + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + sk, pk, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + m := common.GetRandomPositiveInt(q) + c, r, err := sk.EncryptAndReturnRandomness(m) + assert.NoError(t, err) + + primes := [2]*big.Int{common.GetRandomPrimeInt(testSafePrimeBits), common.GetRandomPrimeInt(testSafePrimeBits)} + NTildei, h1i, h2i, err := crypto.GenerateNTildei(primes) + assert.NoError(t, err) + + session := []byte("range-proof-session-a") + proof, err := ProveRangeAlice(tss.EC(), pk, c, NTildei, h1i, h2i, m, r, session) + assert.NoError(t, err) + assert.True(t, proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, c, session), "proof must verify with the original session") + assert.False(t, proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, c, []byte("range-proof-session-b")), "proof must not replay across sessions") + assert.False(t, proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "session-bound proof must not verify without its session") +} + +func TestRangeProofAliceRejectsMalformedInputs(t *testing.T) { + q := tss.EC().Params().N + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + sk, pk, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + m := common.GetRandomPositiveInt(q) + c, r, err := sk.EncryptAndReturnRandomness(m) + assert.NoError(t, err) + + primes := [2]*big.Int{common.GetRandomPrimeInt(testSafePrimeBits), common.GetRandomPrimeInt(testSafePrimeBits)} + NTildei, h1i, h2i, err := crypto.GenerateNTildei(primes) + assert.NoError(t, err) + proof, err := ProveRangeAlice(tss.EC(), pk, c, NTildei, h1i, h2i, m, r) + assert.NoError(t, err) + + assert.False(t, proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, pk.N), "ciphertext must be coprime to Paillier N") + + badS1 := *proof + badS1.S1 = new(big.Int).Sub(q, big.NewInt(1)) + assert.False(t, badS1.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "S1 below q must fail") + + badS := *proof + badS.S = big.NewInt(1) + assert.False(t, badS.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "S equal to one must fail") + + badZ := *proof + badZ.Z = big.NewInt(1) + assert.False(t, badZ.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "Z equal to one must fail") +} diff --git a/crypto/mta/share_protocol.go b/crypto/mta/share_protocol.go index 4f2885827..05fb69f09 100644 --- a/crypto/mta/share_protocol.go +++ b/crypto/mta/share_protocol.go @@ -20,12 +20,13 @@ func AliceInit( ec elliptic.Curve, pkA *paillier.PublicKey, a, NTildeB, h1B, h2B *big.Int, + session ...[]byte, ) (cA *big.Int, pf *RangeProofAlice, err error) { cA, rA, err := pkA.EncryptAndReturnRandomness(a) if err != nil { return nil, nil, err } - pf, err = ProveRangeAlice(ec, pkA, cA, NTildeB, h1B, h2B, a, rA) + pf, err = ProveRangeAlice(ec, pkA, cA, NTildeB, h1B, h2B, a, rA, session...) return cA, pf, err } @@ -34,13 +35,17 @@ func BobMid( pkA *paillier.PublicKey, pf *RangeProofAlice, b, cA, NTildeA, h1A, h2A, NTildeB, h1B, h2B *big.Int, + session ...[]byte, ) (beta, cB, betaPrm *big.Int, piB *ProofBob, err error) { - if !pf.Verify(ec, pkA, NTildeB, h1B, h2B, cA) { + if !pf.Verify(ec, pkA, NTildeB, h1B, h2B, cA, session...) { err = errors.New("RangeProofAlice.Verify() returned false") return } q := ec.Params().N - betaPrm = common.GetRandomPositiveInt(pkA.N) + q5 := new(big.Int).Mul(q, q) + q5 = new(big.Int).Mul(q5, q5) + q5 = new(big.Int).Mul(q5, q) + betaPrm = common.GetRandomPositiveInt(q5) cBetaPrm, cRand, err := pkA.EncryptAndReturnRandomness(betaPrm) if err != nil { return @@ -54,7 +59,7 @@ func BobMid( return } beta = common.ModInt(q).Sub(zero, betaPrm) - piB, err = ProveBob(ec, pkA, NTildeA, h1A, h2A, cA, cB, b, betaPrm, cRand) + piB, err = ProveBob(ec, pkA, NTildeA, h1A, h2A, cA, cB, b, betaPrm, cRand, session...) return } @@ -64,13 +69,17 @@ func BobMidWC( pf *RangeProofAlice, b, cA, NTildeA, h1A, h2A, NTildeB, h1B, h2B *big.Int, B *crypto.ECPoint, + session ...[]byte, ) (beta, cB, betaPrm *big.Int, piB *ProofBobWC, err error) { - if !pf.Verify(ec, pkA, NTildeB, h1B, h2B, cA) { + if !pf.Verify(ec, pkA, NTildeB, h1B, h2B, cA, session...) { err = errors.New("RangeProofAlice.Verify() returned false") return } q := ec.Params().N - betaPrm = common.GetRandomPositiveInt(pkA.N) + q5 := new(big.Int).Mul(q, q) + q5 = new(big.Int).Mul(q5, q5) + q5 = new(big.Int).Mul(q5, q) + betaPrm = common.GetRandomPositiveInt(q5) cBetaPrm, cRand, err := pkA.EncryptAndReturnRandomness(betaPrm) if err != nil { return @@ -84,7 +93,7 @@ func BobMidWC( return } beta = common.ModInt(q).Sub(zero, betaPrm) - piB, err = ProveBobWC(ec, pkA, NTildeA, h1A, h2A, cA, cB, b, betaPrm, cRand, B) + piB, err = ProveBobWC(ec, pkA, NTildeA, h1A, h2A, cA, cB, b, betaPrm, cRand, B, session...) return } @@ -94,8 +103,9 @@ func AliceEnd( pf *ProofBob, h1A, h2A, cA, cB, NTildeA *big.Int, sk *paillier.PrivateKey, + session ...[]byte, ) (*big.Int, error) { - if !pf.Verify(ec, pkA, NTildeA, h1A, h2A, cA, cB) { + if !pf.Verify(ec, pkA, NTildeA, h1A, h2A, cA, cB, session...) { return nil, errors.New("ProofBob.Verify() returned false") } alphaPrm, err := sk.Decrypt(cB) @@ -113,8 +123,9 @@ func AliceEndWC( B *crypto.ECPoint, cA, cB, NTildeA, h1A, h2A *big.Int, sk *paillier.PrivateKey, + session ...[]byte, ) (*big.Int, error) { - if !pf.Verify(ec, pkA, NTildeA, h1A, h2A, cA, cB, B) { + if !pf.Verify(ec, pkA, NTildeA, h1A, h2A, cA, cB, B, session...) { return nil, errors.New("ProofBobWC.Verify() returned false") } alphaPrm, err := sk.Decrypt(cB) diff --git a/crypto/paillier/factor_proof.go b/crypto/paillier/factor_proof.go index b376b7388..ef8f82530 100644 --- a/crypto/paillier/factor_proof.go +++ b/crypto/paillier/factor_proof.go @@ -34,7 +34,7 @@ type ( // Canetti, R., Gennaro, R., Goldfeder, S., Makriyannis, N., Peled, U.: // UC Non-Interactive, Proactive, Threshold ECDSA with Identifiable Aborts. // In: Cryptology ePrint Archive 2021/060 -func (privateKey *PrivateKey) FactorProof(N, s, t *big.Int) *FactorProof { +func (privateKey *PrivateKey) FactorProof(N, s, t *big.Int, session ...[]byte) *FactorProof { N0 := privateKey.PublicKey.N p, q := privateKey.GetPQ() @@ -67,7 +67,7 @@ func (privateKey *PrivateKey) FactorProof(N, s, t *big.Int) *FactorProof { // the last message with respect to e and communicates the entire transcript as the proof. Later, the Verifier // accepts the proof if it is a valid transcript of the underlying Σ-protocol and e is well-formed (verified by // querying the oracle as the Prover should have). - e := FactorChallenge(N, s, t, N0, P, Q, A, B, T, sigma) + e := FactorChallenge(N, s, t, N0, P, Q, A, B, T, sigma, session...) sigmaH := new(big.Int) sigmaH.Mul(v, p) @@ -82,7 +82,7 @@ func (privateKey *PrivateKey) FactorProof(N, s, t *big.Int) *FactorProof { return &FactorProof{P, Q, A, B, T, sigma, z1, z2, w1, w2, vv} } -func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int) (bool, error) { +func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int, session ...[]byte) (bool, error) { if common.AnyIsNil(pkN, N, s, t) { return false, fmt.Errorf("fac proof verify: nil bigint present in args") } @@ -90,7 +90,7 @@ func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int) (bool, error) { return false, fmt.Errorf("fac proof verify: nil bigint present in proof") } - e := FactorChallenge(N, s, t, pkN, pf.P, pf.Q, pf.A, pf.B, pf.T, pf.Sigma) + e := FactorChallenge(N, s, t, pkN, pf.P, pf.Q, pf.A, pf.B, pf.T, pf.Sigma, session...) modN := common.ModInt(N) @@ -131,12 +131,17 @@ func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int) (bool, error) { return true, nil } -func FactorChallenge(N, s, t, pkN, P, Q, A, B, T, sigma *big.Int) *big.Int { +func FactorChallenge(N, s, t, pkN, P, Q, A, B, T, sigma *big.Int, session ...[]byte) *big.Int { q := big.NewInt(1) q = q.Lsh(q, 256) // q = 2^256 qMinus1 := new(big.Int).Sub(q, big.NewInt(1)) // q-1 qDoubleMinus1 := new(big.Int).Add(q, qMinus1) // q+q-1 = 2q-1 + if len(session) > 0 { + eHash := common.SHA512_256i_TAGGED(session[0], N, s, t, pkN, P, Q, A, B, T, sigma) + return common.RejectionSample(q, eHash) + } + // 2. Verifier replies with e <- +-q // The q here is not the secret factor q, but rather the order of secp256k1, // or in practical terms 2^256 as the value h does not involve elliptic curve operations diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index 1dbb240ca..57178dfac 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -91,6 +91,24 @@ func TestFactorProofVerify(t *testing.T) { assert.True(t, res, "proof verify result must be true") } +func TestFactorProofSessionBinding(t *testing.T) { + facSetUp(t) + session := []byte("factor-proof-session-a") + proof := privateKey.FactorProof(auxPrime.N, s, tt, session) + + res, err := proof.FactorVerify(publicKey.N, auxPrime.N, s, tt, session) + assert.NoError(t, err) + assert.True(t, res, "proof verify result must be true") + + res, err = proof.FactorVerify(publicKey.N, auxPrime.N, s, tt, []byte("factor-proof-session-b")) + assert.Error(t, err) + assert.False(t, res, "proof verify result must be false") + + res, err = proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + assert.Error(t, err) + assert.False(t, res, "session-bound proof must not verify without its session") +} + func TestFactorProofVerifyFail1(t *testing.T) { facSetUp(t) badN := new(big.Int).Mul(publicKey.N, big.NewInt(3)) diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go index 4c6822a58..b50b6c21e 100644 --- a/crypto/paillier/mod_proof.go +++ b/crypto/paillier/mod_proof.go @@ -25,7 +25,7 @@ type ( // Canetti, R., Gennaro, R., Goldfeder, S., Makriyannis, N., Peled, U.: // UC Non-Interactive, Proactive, Threshold ECDSA with Identifiable Aborts. // In: Cryptology ePrint Archive 2021/060 -func (privateKey *PrivateKey) ModProof() *ModProof { +func (privateKey *PrivateKey) ModProof(session ...[]byte) *ModProof { N := privateKey.PublicKey.N phiN := privateKey.PhiN p, q := privateKey.GetPQ() @@ -35,7 +35,7 @@ func (privateKey *PrivateKey) ModProof() *ModProof { w = common.GetRandomPositiveInt(N) } - y := ModChallenge(N, w) + y := ModChallenge(N, w, session...) var x [PARAM_M]*big.Int var a [PARAM_M]bool @@ -67,7 +67,7 @@ func (privateKey *PrivateKey) ModProof() *ModProof { // – N is an odd composite number. // – z_i^N = y_i for every i ∈ [m] // – x_i^4 = (-1)^a_i * w^b_i * y_i mod N and a_i, b_i ∈ {0, 1} for every i ∈ [m]. -func (pf ModProof) ModVerify(N *big.Int) (bool, error) { +func (pf ModProof) ModVerify(N *big.Int, session ...[]byte) (bool, error) { if common.AnyIsNil(pf.W) || common.AnyIsNil(pf.X[:]...) || common.AnyIsNil(pf.Z[:]...) { return false, fmt.Errorf("mod proof verify: nil inputs in proof") } @@ -91,7 +91,7 @@ func (pf ModProof) ModVerify(N *big.Int) (bool, error) { return false, fmt.Errorf("mod proof verify: w %d exceeds N %d", pf.W, N) } - y := ModChallenge(N, pf.W) + y := ModChallenge(N, pf.W, session...) for i, yi := range y { if !common.Lt(pf.X[i], N) { @@ -125,11 +125,16 @@ func (pf ModProof) ModVerify(N *big.Int) (bool, error) { } // Standard Fiat-Shamir transform -func ModChallenge(N, w *big.Int) [PARAM_M]*big.Int { +func ModChallenge(N, w *big.Int, session ...[]byte) [PARAM_M]*big.Int { var y [PARAM_M]*big.Int for i := range y { - y[i] = common.HashToN(N, w, big.NewInt(int64(i))) + if len(session) == 0 { + y[i] = common.HashToN(N, w, big.NewInt(int64(i))) + continue + } + ei := common.SHA512_256i_TAGGED(session[0], append([]*big.Int{w, N}, y[:i]...)...) + y[i] = common.RejectionSample(N, ei) } return y diff --git a/crypto/paillier/mod_proof_test.go b/crypto/paillier/mod_proof_test.go index 737f53184..c0030d56e 100644 --- a/crypto/paillier/mod_proof_test.go +++ b/crypto/paillier/mod_proof_test.go @@ -30,6 +30,24 @@ func TestModProofVerify(t *testing.T) { assert.True(t, res, "proof verify result must be true") } +func TestModProofSessionBinding(t *testing.T) { + modSetUp(t) + session := []byte("mod-proof-session-a") + proof := privateKey.ModProof(session) + + res, err := proof.ModVerify(publicKey.N, session) + assert.NoError(t, err) + assert.True(t, res, "proof verify result must be true") + + res, err = proof.ModVerify(publicKey.N, []byte("mod-proof-session-b")) + assert.Error(t, err) + assert.False(t, res, "proof verify result must be false") + + res, err = proof.ModVerify(publicKey.N) + assert.Error(t, err) + assert.False(t, res, "session-bound proof must not verify without its session") +} + func TestModProofVerifyFail(t *testing.T) { modSetUp(t) proof := privateKey.ModProof() diff --git a/crypto/schnorr/schnorr_proof.go b/crypto/schnorr/schnorr_proof.go index 89f798c50..693a92531 100644 --- a/crypto/schnorr/schnorr_proof.go +++ b/crypto/schnorr/schnorr_proof.go @@ -28,6 +28,12 @@ type ( // NewZKProof constructs a new Schnorr ZK proof of knowledge of the discrete logarithm (GG18Spec Fig. 16) func NewZKProof(x *big.Int, X *crypto.ECPoint) (*ZKProof, error) { + return NewZKProofWithSession(nil, x, X) +} + +// NewZKProofWithSession constructs a Schnorr proof with the session bound into +// the Fiat-Shamir challenge. +func NewZKProofWithSession(session []byte, x *big.Int, X *crypto.ECPoint) (*ZKProof, error) { if x == nil || X == nil || !X.ValidateBasic() { return nil, errors.New("ZKProof constructor received nil or invalid value(s)") } @@ -39,7 +45,8 @@ func NewZKProof(x *big.Int, X *crypto.ECPoint) (*ZKProof, error) { a := common.GetRandomPositiveInt(q) alpha := crypto.ScalarBaseMult(ec, a) - c := common.HashToN(q, X.X(), X.Y(), g.X(), g.Y(), alpha.X(), alpha.Y()) + cHash := common.SHA512_256i_TAGGED(session, X.X(), X.Y(), g.X(), g.Y(), alpha.X(), alpha.Y()) + c := common.RejectionSample(q, cHash) t := new(big.Int).Mul(c, x) t = common.ModInt(q).Add(a, t) @@ -48,6 +55,12 @@ func NewZKProof(x *big.Int, X *crypto.ECPoint) (*ZKProof, error) { // NewZKProof verifies a new Schnorr ZK proof of knowledge of the discrete logarithm (GG18Spec Fig. 16) func (pf *ZKProof) Verify(X *crypto.ECPoint) bool { + return pf.VerifyWithSession(nil, X) +} + +// 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() { return false } @@ -56,7 +69,8 @@ func (pf *ZKProof) Verify(X *crypto.ECPoint) bool { q := ecParams.N g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) - c := common.HashToN(q, X.X(), X.Y(), g.X(), g.Y(), pf.Alpha.X(), pf.Alpha.Y()) + 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) tG := crypto.ScalarBaseMult(ec, pf.T) Xc := X.ScalarMult(c) @@ -73,6 +87,12 @@ func (pf *ZKProof) ValidateBasic() bool { // 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) func NewZKVProof(V, R *crypto.ECPoint, s, l *big.Int) (*ZKVProof, error) { + return NewZKVProofWithSession(nil, V, R, s, l) +} + +// NewZKVProofWithSession constructs a Schnorr V proof with the session bound +// into the Fiat-Shamir challenge. +func NewZKVProofWithSession(session []byte, V, R *crypto.ECPoint, s, l *big.Int) (*ZKVProof, error) { if V == nil || R == nil || s == nil || l == nil || !V.ValidateBasic() || !R.ValidateBasic() { return nil, errors.New("ZKVProof constructor received nil value(s)") } @@ -86,7 +106,8 @@ func NewZKVProof(V, R *crypto.ECPoint, s, l *big.Int) (*ZKVProof, error) { bG := crypto.ScalarBaseMult(ec, b) alpha, _ := aR.Add(bG) // already on the curve. - c := common.HashToN(q, V.X(), V.Y(), R.X(), R.Y(), g.X(), g.Y(), alpha.X(), alpha.Y()) + cHash := common.SHA512_256i_TAGGED(session, V.X(), V.Y(), R.X(), R.Y(), g.X(), g.Y(), alpha.X(), alpha.Y()) + c := common.RejectionSample(q, cHash) modQ := common.ModInt(q) t := modQ.Add(a, new(big.Int).Mul(c, s)) @@ -96,6 +117,12 @@ func NewZKVProof(V, R *crypto.ECPoint, s, l *big.Int) (*ZKVProof, error) { } func (pf *ZKVProof) Verify(V, R *crypto.ECPoint) bool { + return pf.VerifyWithSession(nil, V, R) +} + +// 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() { return false } @@ -104,7 +131,8 @@ func (pf *ZKVProof) Verify(V, R *crypto.ECPoint) bool { q := ecParams.N g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) - c := common.HashToN(q, V.X(), V.Y(), R.X(), R.Y(), g.X(), g.Y(), pf.Alpha.X(), pf.Alpha.Y()) + 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) tR := R.ScalarMult(pf.T) uG := crypto.ScalarBaseMult(ec, pf.U) diff --git a/crypto/schnorr/schnorr_proof_test.go b/crypto/schnorr/schnorr_proof_test.go index c81fed4de..33284f1af 100644 --- a/crypto/schnorr/schnorr_proof_test.go +++ b/crypto/schnorr/schnorr_proof_test.go @@ -40,6 +40,19 @@ func TestSchnorrProofVerify(t *testing.T) { assert.True(t, res, "verify result must be true") } +func TestSchnorrProofVerifySessionBinding(t *testing.T) { + q := tss.EC().Params().N + u := common.GetRandomPositiveInt(q) + X := crypto.ScalarBaseMult(tss.EC(), u) + + session := []byte("schnorr-session-a") + proof, _ := NewZKProofWithSession(session, u, X) + + assert.True(t, proof.VerifyWithSession(session, X), "verify result must be true with the original session") + assert.False(t, proof.VerifyWithSession([]byte("schnorr-session-b"), X), "proof must not replay across sessions") + assert.False(t, proof.Verify(X), "session-bound proof must not verify without its session") +} + func TestSchnorrProofVerifyBadX(t *testing.T) { q := tss.EC().Params().N u := common.GetRandomPositiveInt(q) @@ -69,6 +82,24 @@ func TestSchnorrVProofVerify(t *testing.T) { assert.True(t, res, "verify result must be true") } +func TestSchnorrVProofVerifySessionBinding(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) // k_-1 * G + Rs := R.ScalarMult(s) + lG := crypto.ScalarBaseMult(tss.EC(), l) + V, _ := Rs.Add(lG) + + session := []byte("schnorr-v-session-a") + proof, _ := NewZKVProofWithSession(session, V, R, s, l) + + assert.True(t, proof.VerifyWithSession(session, V, R), "verify result must be true with the original session") + assert.False(t, proof.VerifyWithSession([]byte("schnorr-v-session-b"), V, R), "proof must not replay across sessions") + assert.False(t, proof.Verify(V, R), "session-bound proof must not verify without its session") +} + func TestSchnorrVProofVerifyBadPartialV(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 52573075b..07fc81137 100644 --- a/crypto/vss/feldman_vss.go +++ b/crypto/vss/feldman_vss.go @@ -92,7 +92,7 @@ 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 { + if share.Threshold != threshold || vs == nil || len(vs) != threshold+1 { return false } var err error @@ -113,7 +113,7 @@ func (share *Share) Verify(ec elliptic.Curve, threshold int, vs Vs) bool { } func (shares Shares) ReConstruct(ec elliptic.Curve) (secret *big.Int, err error) { - if shares != nil && shares[0].Threshold > len(shares) { + if shares != nil && shares[0].Threshold+1 > len(shares) { return nil, ErrNumSharesBelowThreshold } modN := common.ModInt(ec.Params().N) diff --git a/crypto/vss/feldman_vss_test.go b/crypto/vss/feldman_vss_test.go index d6c0284d2..cbb26ee75 100644 --- a/crypto/vss/feldman_vss_test.go +++ b/crypto/vss/feldman_vss_test.go @@ -87,6 +87,7 @@ func TestVerify(t *testing.T) { for i := 0; i < num; i++ { assert.True(t, shares[i].Verify(tss.EC(), threshold, vs)) } + assert.False(t, shares[0].Verify(tss.EC(), threshold, vs[:threshold])) } func TestReconstruct(t *testing.T) { @@ -102,15 +103,17 @@ func TestReconstruct(t *testing.T) { _, shares, err := Create(tss.EC(), threshold, secret, ids) assert.NoError(t, err) - secret2, err2 := shares[:threshold-1].ReConstruct(tss.EC()) + secret2, err2 := shares[:threshold].ReConstruct(tss.EC()) assert.Error(t, err2) // not enough shares to satisfy the threshold assert.Nil(t, secret2) - secret3, err3 := shares[:threshold].ReConstruct(tss.EC()) + secret3, err3 := shares[:threshold+1].ReConstruct(tss.EC()) assert.NoError(t, err3) assert.NotZero(t, secret3) + assert.Zero(t, secret.Cmp(secret3)) secret4, err4 := shares[:num].ReConstruct(tss.EC()) assert.NoError(t, err4) assert.NotZero(t, secret4) + assert.Zero(t, secret.Cmp(secret4)) } diff --git a/ecdsa/keygen/local_party.go b/ecdsa/keygen/local_party.go index 714384202..a5b8e9689 100644 --- a/ecdsa/keygen/local_party.go +++ b/ecdsa/keygen/local_party.go @@ -53,6 +53,8 @@ type ( shares vss.Shares deCommitPolyG cmt.HashDeCommitment skTilde *paillier.PrivateKey + ssid []byte + ssidNonce *big.Int } ) diff --git a/ecdsa/keygen/local_party_test.go b/ecdsa/keygen/local_party_test.go index dcbbee9de..f5abdcc1a 100644 --- a/ecdsa/keygen/local_party_test.go +++ b/ecdsa/keygen/local_party_test.go @@ -277,9 +277,9 @@ keygen: // fails if threshold cannot be satisfied (bad share) { - badShares := pShares[:threshold] + badShares := pShares[:threshold+1] badShares[len(badShares)-1].Share.Set(big.NewInt(0)) - uj, err := pShares[:threshold].ReConstruct(tss.S256()) + uj, err := pShares[:threshold+1].ReConstruct(tss.S256()) assert.NoError(t, err) assert.NotEqual(t, parties[j].temp.ui, uj) BigXjX, BigXjY := tss.EC().ScalarBaseMult(uj.Bytes()) diff --git a/ecdsa/keygen/round_1.go b/ecdsa/keygen/round_1.go index 0b5471233..6a25e2986 100644 --- a/ecdsa/keygen/round_1.go +++ b/ecdsa/keygen/round_1.go @@ -84,6 +84,14 @@ func (round *round1) Start() *tss.Error { round.save.NTildej[i] = preParams.NTildei round.save.H1j[i], round.save.H2j[i] = preParams.H1i, preParams.H2i + if nonce := round.Params().SessionNonce(); nonce != nil { + round.temp.ssidNonce = new(big.Int).Set(nonce) + } else { + round.temp.ssidNonce = new(big.Int).SetUint64(0) + } + round.temp.ssid = round.getSSID() + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + // generate the dlnproofs for keygen h1i, h2i, alpha, beta, p, q, NTildei := preParams.H1i, @@ -93,10 +101,10 @@ func (round *round1) Start() *tss.Error { preParams.P, preParams.Q, preParams.NTildei - dlnProof1 := dlnproof.NewDLNProof(h1i, h2i, alpha, p, q, NTildei) - dlnProof2 := dlnproof.NewDLNProof(h2i, h1i, beta, p, q, NTildei) + dlnProof1 := dlnproof.NewDLNProof(h1i, h2i, alpha, p, q, NTildei, round.temp.ssid) + dlnProof2 := dlnproof.NewDLNProof(h2i, h1i, beta, p, q, NTildei, round.temp.ssid) - modProof := preParams.PaillierSK.ModProof() + modProof := preParams.PaillierSK.ModProof(contextI) // NTildei = (2p+1) * (2q+1) // phi(NTildei) = ((2p+1) - 1) * ((2q+1) - 1) = 2p * 2q @@ -109,7 +117,7 @@ func (round *round1) Start() *tss.Error { pkTilde := &paillier.PublicKey{N: NTildei} skTilde := &paillier.PrivateKey{PublicKey: *pkTilde, LambdaN: lambdaNTilde, PhiN: phiNTilde} - modProofTilde := skTilde.ModProof() + modProofTilde := skTilde.ModProof(contextI) // for this P: SAVE // - shareID @@ -158,17 +166,19 @@ func (round *round1) CanAccept(msg tss.ParsedMessage) bool { } func (round *round1) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.kgRound1Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } // vss check is in round 2 round.ok[j] = true } - return true, nil + return ret, nil } func (round *round1) NextRound() tss.Round { diff --git a/ecdsa/keygen/round_2.go b/ecdsa/keygen/round_2.go index 99540be5f..d8ebf5fee 100644 --- a/ecdsa/keygen/round_2.go +++ b/ecdsa/keygen/round_2.go @@ -9,6 +9,7 @@ package keygen import ( "encoding/hex" "errors" + "math/big" "sync" "github.com/bnb-chain/tss-lib/common" @@ -71,31 +72,32 @@ func (round *round2) Start() *tss.Error { wg.Add(4) _j := j _msg := msg + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) verifier.VerifyDLNProof1(r1msg, H1j, H2j, NTildej, func(isValid bool) { if !isValid { dlnProof1FailCulprits[_j] = _msg.GetFrom() } wg.Done() - }) + }, round.temp.ssid) verifier.VerifyDLNProof2(r1msg, H2j, H1j, NTildej, func(isValid bool) { if !isValid { dlnProof2FailCulprits[_j] = _msg.GetFrom() } wg.Done() - }) + }, round.temp.ssid) verifier.VerifyModProof(r1msg, paillierPKj.N, func(isValid bool) { if !isValid { modProofFailCulprits[_j] = _msg.GetFrom() } wg.Done() - }) + }, contextJ) verifier.VerifyModProofTilde(r1msg, NTildej, func(isValid bool) { if !isValid { modProofTildeFailCulprits[_j] = _msg.GetFrom() } wg.Done() - }) + }, contextJ) } wg.Wait() for _, culprit := range append(dlnProof1FailCulprits, dlnProof2FailCulprits...) { @@ -128,6 +130,7 @@ func (round *round2) Start() *tss.Error { // 5. p2p send share ij to Pj shares := round.temp.shares + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) for j, Pj := range round.Parties().IDs() { // do not send to this Pj, but store for round 3 if j == i { @@ -135,8 +138,8 @@ func (round *round2) Start() *tss.Error { continue } H1j, H2j, NTildej := round.save.H1j[j], round.save.H2j[j], round.save.NTildej[j] - facProof := round.save.LocalPreParams.PaillierSK.FactorProof(NTildej, H1j, H2j) - facProofTilde := round.temp.skTilde.FactorProof(NTildej, H1j, H2j) + facProof := round.save.LocalPreParams.PaillierSK.FactorProof(NTildej, H1j, H2j, contextI) + facProofTilde := round.temp.skTilde.FactorProof(NTildej, H1j, H2j, contextI) r2msg1 := NewKGRound2Message1(Pj, round.PartyID(), shares[j], facProof, facProofTilde) round.out <- r2msg1 @@ -161,21 +164,24 @@ func (round *round2) CanAccept(msg tss.ParsedMessage) bool { } func (round *round2) Update() (bool, *tss.Error) { + ret := true // guard - VERIFY de-commit for all Pj for j, msg := range round.temp.kgRound2Message1s { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } msg2 := round.temp.kgRound2Message2s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round2) NextRound() tss.Round { diff --git a/ecdsa/keygen/round_3.go b/ecdsa/keygen/round_3.go index a0312f979..b25b9d195 100644 --- a/ecdsa/keygen/round_3.go +++ b/ecdsa/keygen/round_3.go @@ -65,6 +65,7 @@ func (round *round3) Start() *tss.Error { if j == PIdx { continue } + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) // 6-8. go func(j int, ch chan<- vssOut) { // 4-9. @@ -96,7 +97,7 @@ func (round *round3) Start() *tss.Error { pkN := round.save.PaillierPKs[j].N NTilde := round.save.LocalPreParams.NTildei H1i, H2i := round.save.LocalPreParams.H1i, round.save.LocalPreParams.H2i - ok, err = FacProof.FactorVerify(pkN, NTilde, H1i, H2i) + ok, err = FacProof.FactorVerify(pkN, NTilde, H1i, H2i, contextJ) if err != nil { ch <- vssOut{err, nil} } @@ -105,7 +106,7 @@ func (round *round3) Start() *tss.Error { } FacProofTilde := r2msg1.UnmarshalFactorProofTilde() NTildej := round.save.NTildej[j] - ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i) + ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i, contextJ) if err != nil { ch <- vssOut{err, nil} } @@ -216,17 +217,19 @@ func (round *round3) CanAccept(msg tss.ParsedMessage) bool { } func (round *round3) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.kgRound3Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } // proof check is in round 4 round.ok[j] = true } - return true, nil + return ret, nil } func (round *round3) NextRound() tss.Round { diff --git a/ecdsa/keygen/rounds.go b/ecdsa/keygen/rounds.go index 313184abd..0e7c7a707 100644 --- a/ecdsa/keygen/rounds.go +++ b/ecdsa/keygen/rounds.go @@ -7,6 +7,9 @@ package keygen import ( + "math/big" + + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/tss" ) @@ -94,3 +97,16 @@ func (round *base) resetOK() { round.ok[j] = false } } + +func (round *base) getSSID() []byte { + ssidList := []*big.Int{ + round.EC().Params().P, + round.EC().Params().N, + round.EC().Params().Gx, + round.EC().Params().Gy, + } + ssidList = append(ssidList, round.Parties().IDs().Keys()...) + ssidList = append(ssidList, big.NewInt(int64(round.number))) + ssidList = append(ssidList, round.temp.ssidNonce) + return common.SHA512_256i(ssidList...).Bytes() +} diff --git a/ecdsa/keygen/verifier.go b/ecdsa/keygen/verifier.go index 9224c0e5c..3675e747e 100644 --- a/ecdsa/keygen/verifier.go +++ b/ecdsa/keygen/verifier.go @@ -44,6 +44,7 @@ func (pv *ProofVerifier) VerifyDLNProof1( m dlnMessage, h1, h2, n *big.Int, onDone func(bool), + session ...[]byte, ) { pv.semaphore <- struct{}{} go func() { @@ -55,7 +56,7 @@ func (pv *ProofVerifier) VerifyDLNProof1( return } - onDone(dlnProof.Verify(h1, h2, n)) + onDone(dlnProof.Verify(h1, h2, n, session...)) }() } @@ -63,6 +64,7 @@ func (pv *ProofVerifier) VerifyDLNProof2( m dlnMessage, h1, h2, n *big.Int, onDone func(bool), + session ...[]byte, ) { pv.semaphore <- struct{}{} go func() { @@ -74,7 +76,7 @@ func (pv *ProofVerifier) VerifyDLNProof2( return } - onDone(dlnProof.Verify(h1, h2, n)) + onDone(dlnProof.Verify(h1, h2, n, session...)) }() } @@ -82,6 +84,7 @@ func (pv *ProofVerifier) VerifyModProof( m modMessage, N *big.Int, onDone func(bool), + session ...[]byte, ) { pv.semaphore <- struct{}{} go func() { @@ -93,7 +96,7 @@ func (pv *ProofVerifier) VerifyModProof( return } - ok, err2 := modProof.ModVerify(N) + ok, err2 := modProof.ModVerify(N, session...) if err2 != nil { onDone(false) return @@ -106,6 +109,7 @@ func (pv *ProofVerifier) VerifyModProofTilde( m modMessage, N *big.Int, onDone func(bool), + session ...[]byte, ) { pv.semaphore <- struct{}{} go func() { @@ -117,7 +121,7 @@ func (pv *ProofVerifier) VerifyModProofTilde( return } - ok, err2 := modProof.ModVerify(N) + ok, err2 := modProof.ModVerify(N, session...) if err2 != nil { onDone(false) return diff --git a/ecdsa/keygen/verifier_test.go b/ecdsa/keygen/verifier_test.go index ba037688d..83cd4af33 100644 --- a/ecdsa/keygen/verifier_test.go +++ b/ecdsa/keygen/verifier_test.go @@ -38,6 +38,35 @@ func BenchmarkDlnProof_Verify(b *testing.B) { } } +func TestDLNProofSessionBinding(t *testing.T) { + localPartySaveData, _, err := LoadKeygenTestFixtures(1) + if err != nil { + t.Fatal(err) + } + + params := localPartySaveData[0].LocalPreParams + session := []byte("dln-session-a") + proof := dlnproof.NewDLNProof( + params.H1i, + params.H2i, + params.Alpha, + params.P, + params.Q, + params.NTildei, + session, + ) + + if !proof.Verify(params.H1i, params.H2i, params.NTildei, session) { + t.Fatal("expected positive verification with the original session") + } + if proof.Verify(params.H1i, params.H2i, params.NTildei, []byte("dln-session-b")) { + t.Fatal("expected negative verification with a different session") + } + if proof.Verify(params.H1i, params.H2i, params.NTildei) { + t.Fatal("expected negative verification without the proof session") + } +} + func BenchmarkDlnVerifier_VerifyProof1(b *testing.B) { preParams, alpha, tt := prepareProofB(b) message := &KGRound1Message{ diff --git a/ecdsa/resharing/local_party.go b/ecdsa/resharing/local_party.go index 34a29d479..4cb735edc 100644 --- a/ecdsa/resharing/local_party.go +++ b/ecdsa/resharing/local_party.go @@ -61,6 +61,8 @@ type ( newXi *big.Int newKs []*big.Int newBigXjs []*crypto.ECPoint // Xj to save in round 5 + ssid []byte + ssidNonce *big.Int } ) diff --git a/ecdsa/resharing/round_1_old_step_1.go b/ecdsa/resharing/round_1_old_step_1.go index 6358512da..9fa6f5cee 100644 --- a/ecdsa/resharing/round_1_old_step_1.go +++ b/ecdsa/resharing/round_1_old_step_1.go @@ -89,16 +89,22 @@ func (round *round1) Update() (bool, *tss.Error) { if !round.ReSharingParameters.IsNewCommittee() { return true, nil } + ret := true // accept messages from old -> new committee for j, msg := range round.temp.dgRound1Messages { if round.oldOK[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.oldOK[j] = true + if round.temp.dgRound1Messages[0] == nil { + ret = false + continue + } // save the ecdsa pub received from the old committee r1msg := round.temp.dgRound1Messages[0].Content().(*DGRound1Message) candidate, err := r1msg.UnmarshalECDSAPub(round.Params().EC()) @@ -112,7 +118,7 @@ func (round *round1) Update() (bool, *tss.Error) { } round.save.ECDSAPub = candidate } - return true, nil + return ret, nil } func (round *round1) NextRound() tss.Round { diff --git a/ecdsa/resharing/round_2_new_step_1.go b/ecdsa/resharing/round_2_new_step_1.go index c7c206007..cdb62eb5f 100644 --- a/ecdsa/resharing/round_2_new_step_1.go +++ b/ecdsa/resharing/round_2_new_step_1.go @@ -10,6 +10,7 @@ import ( "errors" "math/big" + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto/dlnproof" "github.com/bnb-chain/tss-lib/crypto/paillier" "github.com/bnb-chain/tss-lib/ecdsa/keygen" @@ -24,6 +25,12 @@ func (round *round2) Start() *tss.Error { round.started = true round.resetOK() // resets both round.oldOK and round.newOK round.allOldOK() + if nonce := round.Params().SessionNonce(); nonce != nil { + round.temp.ssidNonce = new(big.Int).Set(nonce) + } else { + round.temp.ssidNonce = new(big.Int).SetUint64(0) + } + round.temp.ssid = round.getSSID() if !round.ReSharingParams().IsNewCommittee() { return nil @@ -69,10 +76,11 @@ func (round *round2) Start() *tss.Error { preParams.P, preParams.Q, preParams.NTildei - dlnProof1 := dlnproof.NewDLNProof(h1i, h2i, alpha, p, q, NTildei) - dlnProof2 := dlnproof.NewDLNProof(h2i, h1i, beta, p, q, NTildei) + dlnProof1 := dlnproof.NewDLNProof(h1i, h2i, alpha, p, q, NTildei, round.temp.ssid) + dlnProof2 := dlnproof.NewDLNProof(h2i, h1i, beta, p, q, NTildei, round.temp.ssid) - modProof := preParams.PaillierSK.ModProof() + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + modProof := preParams.PaillierSK.ModProof(contextI) // NTildei = (2p+1) * (2q+1) // phi(NTildei) = ((2p+1) - 1) * ((2q+1) - 1) = 2p * 2q @@ -85,7 +93,7 @@ func (round *round2) Start() *tss.Error { pkTilde := &paillier.PublicKey{N: NTildei} skTilde := &paillier.PrivateKey{PublicKey: *pkTilde, LambdaN: lambdaNTilde, PhiN: phiNTilde} - modProofTilde := skTilde.ModProof() + modProofTilde := skTilde.ModProof(contextI) paillierPf := preParams.PaillierSK.Proof(Pi.KeyInt(), round.save.ECDSAPub) r2msg2, err := NewDGRound2Message1( @@ -132,6 +140,7 @@ func (round *round2) CanAccept(msg tss.ParsedMessage) bool { } func (round *round2) Update() (bool, *tss.Error) { + ret := true if round.ReSharingParams().IsOldCommittee() && round.ReSharingParameters.IsNewCommittee() { // accept messages from new -> old committee for j, msg1 := range round.temp.dgRound2Message2s { @@ -139,12 +148,14 @@ func (round *round2) Update() (bool, *tss.Error) { continue } if msg1 == nil || !round.CanAccept(msg1) { - return false, nil + ret = false + continue } // accept message from new -> committee msg2 := round.temp.dgRound2Message1s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.newOK[j] = true } @@ -155,7 +166,8 @@ func (round *round2) Update() (bool, *tss.Error) { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.newOK[j] = true } @@ -166,14 +178,15 @@ func (round *round2) Update() (bool, *tss.Error) { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.newOK[j] = true } } else { return false, round.WrapError(errors.New("this party is not in the old or the new committee"), round.PartyID()) } - return true, nil + return ret, nil } func (round *round2) NextRound() tss.Round { diff --git a/ecdsa/resharing/round_4_new_step_2.go b/ecdsa/resharing/round_4_new_step_2.go index 74e220e84..ee74483f6 100644 --- a/ecdsa/resharing/round_4_new_step_2.go +++ b/ecdsa/resharing/round_4_new_step_2.go @@ -83,34 +83,35 @@ func (round *round4) Start() *tss.Error { }(j, msg, r2msg1) _j := j _msg := msg + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) verifier.VerifyDLNProof1(r2msg1, H1j, H2j, NTildej, func(isValid bool) { if !isValid { dlnProof1FailCulprits[_j] = _msg.GetFrom() common.Logger.Warningf("dln proof 1 verify failed for party %s", _msg.GetFrom()) } wg.Done() - }) + }, round.temp.ssid) verifier.VerifyDLNProof2(r2msg1, H2j, H1j, NTildej, func(isValid bool) { if !isValid { dlnProof2FailCulprits[_j] = _msg.GetFrom() common.Logger.Warningf("dln proof 2 verify failed for party %s", _msg.GetFrom()) } wg.Done() - }) + }, round.temp.ssid) verifier.VerifyModProof(r2msg1, paiPK.N, func(isValid bool) { if !isValid { modProofFailCulprits[_j] = _msg.GetFrom() common.Logger.Warningf("mod proof verify failed for party %s", _msg.GetFrom()) } wg.Done() - }) + }, contextJ) verifier.VerifyModProofTilde(r2msg1, NTildej, func(isValid bool) { if !isValid { - modProofFailCulprits[_j] = _msg.GetFrom() + modProofTildeFailCulprits[_j] = _msg.GetFrom() common.Logger.Warningf("mod proof tilde verify failed for party %s", _msg.GetFrom()) } wg.Done() - }) + }, contextJ) } wg.Wait() for _, culprit := range append(append(paiProofCulprits, dlnProof1FailCulprits...), dlnProof2FailCulprits...) { @@ -217,6 +218,7 @@ func (round *round4) Start() *tss.Error { return round.WrapError(errors2.Wrapf(err, "newBigXj.Add(Vc[c].ScalarMult(z))"), paiProofCulprits...) } + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) for j, Pj := range round.NewParties().IDs() { if common.Eq(Pi.KeyInt(), Pj.KeyInt()) { @@ -225,8 +227,8 @@ func (round *round4) Start() *tss.Error { // Add factor proofs H1j, H2j, NTildej := round.save.H1j[j], round.save.H2j[j], round.save.NTildej[j] - facProof := round.save.LocalPreParams.PaillierSK.FactorProof(NTildej, H1j, H2j) - facProofTilde := round.temp.skTilde.FactorProof(NTildej, H1j, H2j) + facProof := round.save.LocalPreParams.PaillierSK.FactorProof(NTildej, H1j, H2j, contextI) + facProofTilde := round.temp.skTilde.FactorProof(NTildej, H1j, H2j, contextI) r4msg1 := NewDGRound4Message1(Pj, Pi, facProof, facProofTilde) round.out <- r4msg1 diff --git a/ecdsa/resharing/round_5_new_step_3.go b/ecdsa/resharing/round_5_new_step_3.go index 5d01bd64e..2fc14d9ff 100644 --- a/ecdsa/resharing/round_5_new_step_3.go +++ b/ecdsa/resharing/round_5_new_step_3.go @@ -8,6 +8,7 @@ package resharing import ( "errors" + "math/big" "github.com/hashicorp/go-multierror" @@ -49,6 +50,7 @@ func (round *round5) Start() *tss.Error { if common.Eq(Pi.KeyInt(), Pj.KeyInt()) { continue } + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) go func(j int, ch chan<- proofOut) { r4msg1 := round.temp.dgRound4Message1s[j].Content().(*DGRound4Message1) @@ -59,7 +61,7 @@ func (round *round5) Start() *tss.Error { pkN := pk.N NTilde := round.save.LocalPreParams.NTildei H1i, H2i := round.save.LocalPreParams.H1i, round.save.LocalPreParams.H2i - ok, err := FacProof.FactorVerify(pkN, NTilde, H1i, H2i) + ok, err := FacProof.FactorVerify(pkN, NTilde, H1i, H2i, contextJ) if err != nil { ch <- proofOut{err} } @@ -68,7 +70,7 @@ func (round *round5) Start() *tss.Error { } FacProofTilde := r4msg1.UnmarshalFactorProofTilde() NTildej := round.save.NTildej[j] - ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i) + ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i, contextJ) if err != nil { ch <- proofOut{err} } diff --git a/ecdsa/resharing/rounds.go b/ecdsa/resharing/rounds.go index 20b2f5e7c..2981290bc 100644 --- a/ecdsa/resharing/rounds.go +++ b/ecdsa/resharing/rounds.go @@ -7,6 +7,9 @@ package resharing import ( + "math/big" + + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/bnb-chain/tss-lib/tss" ) @@ -137,3 +140,17 @@ func (round *base) allNewOK() { round.newOK[j] = true } } + +func (round *base) getSSID() []byte { + ssidList := []*big.Int{ + round.EC().Params().P, + round.EC().Params().N, + round.EC().Params().Gx, + round.EC().Params().Gy, + } + ssidList = append(ssidList, round.OldParties().IDs().Keys()...) + ssidList = append(ssidList, round.NewParties().IDs().Keys()...) + ssidList = append(ssidList, big.NewInt(int64(round.number))) + ssidList = append(ssidList, round.temp.ssidNonce) + return common.SHA512_256i(ssidList...).Bytes() +} diff --git a/ecdsa/signing/finalize.go b/ecdsa/signing/finalize.go index b2fc670f2..797ba38e2 100644 --- a/ecdsa/signing/finalize.go +++ b/ecdsa/signing/finalize.go @@ -61,14 +61,20 @@ func (round *finalization) Start() *tss.Error { round.data.S = padToLengthBytesInPlace(sumS.Bytes(), bitSizeInBytes) round.data.Signature = append(round.data.R, round.data.S...) round.data.SignatureRecovery = []byte{byte(recid)} - round.data.M = round.temp.m.Bytes() + if round.temp.fullBytesLen == 0 { + round.data.M = round.temp.m.Bytes() + } else { + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + round.data.M = mBytes + } pk := ecdsa.PublicKey{ Curve: round.Params().EC(), X: round.key.ECDSAPub.X(), Y: round.key.ECDSAPub.Y(), } - ok := ecdsa.Verify(&pk, round.temp.m.Bytes(), round.temp.rx, sumS) + ok := ecdsa.Verify(&pk, round.data.M, round.temp.rx, sumS) if !ok { return round.WrapError(fmt.Errorf("signature verification failed")) } diff --git a/ecdsa/signing/local_party.go b/ecdsa/signing/local_party.go index ae202590a..5dbee793d 100644 --- a/ecdsa/signing/local_party.go +++ b/ecdsa/signing/local_party.go @@ -63,10 +63,11 @@ type ( sigma, keyDerivationDelta, gamma *big.Int - cis []*big.Int - bigWs []*crypto.ECPoint - pointGamma *crypto.ECPoint - deCommit cmt.HashDeCommitment + fullBytesLen int + cis []*big.Int + bigWs []*crypto.ECPoint + pointGamma *crypto.ECPoint + deCommit cmt.HashDeCommitment // round 2 betas, // return value of Bob_mid @@ -91,6 +92,9 @@ type ( Ui, Ti *crypto.ECPoint DTelda cmt.HashDeCommitment + + ssid []byte + ssidNonce *big.Int } ) @@ -99,8 +103,10 @@ func NewLocalParty( params *tss.Parameters, key keygen.LocalPartySaveData, out chan<- tss.Message, - end chan<- common.SignatureData) tss.Party { - return NewLocalPartyWithKDD(msg, params, key, nil, out, end) + end chan<- common.SignatureData, + fullBytesLen ...int, +) tss.Party { + return NewLocalPartyWithKDD(msg, params, key, nil, out, end, fullBytesLen...) } // NewLocalPartyWithKDD returns a party with key derivation delta for HD support @@ -111,6 +117,7 @@ func NewLocalPartyWithKDD( keyDerivationDelta *big.Int, out chan<- tss.Message, end chan<- common.SignatureData, + fullBytesLen ...int, ) tss.Party { partyCount := len(params.Parties().IDs()) p := &LocalParty{ @@ -136,6 +143,9 @@ func NewLocalPartyWithKDD( // temp data init p.temp.keyDerivationDelta = keyDerivationDelta p.temp.m = msg + if len(fullBytesLen) > 0 { + p.temp.fullBytesLen = fullBytesLen[0] + } p.temp.cis = make([]*big.Int, partyCount) p.temp.bigWs = make([]*crypto.ECPoint, partyCount) p.temp.betas = make([]*big.Int, partyCount) diff --git a/ecdsa/signing/local_party_test.go b/ecdsa/signing/local_party_test.go index 51b838737..672f6efdf 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -8,6 +8,7 @@ package signing import ( "crypto/ecdsa" + "encoding/hex" "fmt" "math/big" "runtime" @@ -56,11 +57,15 @@ func TestE2EConcurrent(t *testing.T) { updater := test.SharedPartyUpdater + msgData, err := hex.DecodeString("00f163ee51bcaeff9cdff5e0e3c1a646abd19885fffbab0b3b4236e0cf95c9f5") + assert.NoError(t, err) + msgInt := new(big.Int).SetBytes(msgData) + // init the parties for i := 0; i < len(signPIDs); i++ { params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[i], len(signPIDs), threshold) - P := NewLocalParty(big.NewInt(42), params, keys[i], outCh, endCh).(*LocalParty) + P := NewLocalParty(msgInt, params, keys[i], outCh, endCh, len(msgData)).(*LocalParty) parties = append(parties, P) go func(P *LocalParty) { if err := P.Start(); err != nil { @@ -95,7 +100,7 @@ signing: go updater(parties[dest[0].Index], msg, errCh) } - case <-endCh: + case sig := <-endCh: atomic.AddInt32(&ended, 1) if atomic.LoadInt32(&ended) == int32(len(signPIDs)) { t.Logf("Done. Received signature data from %d participants", ended) @@ -120,8 +125,9 @@ signing: X: pkX, Y: pkY, } - ok := ecdsa.Verify(&pk, big.NewInt(42).Bytes(), R.X(), sumS) + ok := ecdsa.Verify(&pk, msgData, R.X(), sumS) assert.True(t, ok, "ecdsa verify must pass") + assert.Equal(t, msgData, sig.M) t.Log("ECDSA signing test done.") // END ECDSA verify diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index 920930db2..7229e3d69 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -45,6 +45,16 @@ func (round *round1) Start() *tss.Error { round.number = 1 round.started = true round.resetOK() + if nonce := round.Params().SessionNonce(); nonce != nil { + round.temp.ssidNonce = new(big.Int).Set(nonce) + } else { + round.temp.ssidNonce = new(big.Int).Set(round.temp.m) + } + ssid, err := round.getSSID() + if err != nil { + return round.WrapError(err) + } + round.temp.ssid = ssid k := common.GetRandomPositiveInt(round.Params().EC().Params().N) gamma := common.GetRandomPositiveInt(round.Params().EC().Params().N) @@ -63,7 +73,8 @@ func (round *round1) Start() *tss.Error { if j == i { continue } - cA, pi, err := mta.AliceInit(round.Params().EC(), round.key.PaillierPKs[i], k, round.key.NTildej[j], round.key.H1j[j], round.key.H2j[j]) + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + cA, pi, err := mta.AliceInit(round.Params().EC(), round.key.PaillierPKs[i], k, round.key.NTildej[j], round.key.H1j[j], round.key.H2j[j], contextJ) if err != nil { return round.WrapError(fmt.Errorf("failed to init mta: %v", err)) } diff --git a/ecdsa/signing/round_2.go b/ecdsa/signing/round_2.go index 79702e0dd..7d6a07a54 100644 --- a/ecdsa/signing/round_2.go +++ b/ecdsa/signing/round_2.go @@ -8,10 +8,12 @@ package signing import ( "errors" + "math/big" "sync" errorspkg "github.com/pkg/errors" + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto/mta" "github.com/bnb-chain/tss-lib/tss" ) @@ -30,6 +32,7 @@ func (round *round2) Start() *tss.Error { errChs := make(chan *tss.Error, (len(round.Parties().IDs())-1)*2) wg := sync.WaitGroup{} wg.Add((len(round.Parties().IDs()) - 1) * 2) + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) for j, Pj := range round.Parties().IDs() { if j == i { continue @@ -54,7 +57,8 @@ func (round *round2) Start() *tss.Error { round.key.H2j[j], round.key.NTildej[i], round.key.H1j[i], - round.key.H2j[i]) + round.key.H2j[i], + contextI) // should be thread safe as these are pre-allocated round.temp.betas[j] = beta round.temp.c1jis[j] = c1ji @@ -84,7 +88,8 @@ func (round *round2) Start() *tss.Error { round.key.NTildej[i], round.key.H1j[i], round.key.H2j[i], - round.temp.bigWs[i]) + round.temp.bigWs[i], + contextI) round.temp.vs[j] = v round.temp.c2jis[j] = c2ji round.temp.pi2jis[j] = pi2ji @@ -116,16 +121,18 @@ func (round *round2) Start() *tss.Error { } func (round *round2) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound2Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round2) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_3.go b/ecdsa/signing/round_3.go index 87c4f8b47..2d941110b 100644 --- a/ecdsa/signing/round_3.go +++ b/ecdsa/signing/round_3.go @@ -38,6 +38,7 @@ func (round *round3) Start() *tss.Error { if j == i { continue } + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) // Alice_end go func(j int, Pj *tss.PartyID) { defer wg.Done() @@ -56,7 +57,8 @@ func (round *round3) Start() *tss.Error { round.temp.cis[j], new(big.Int).SetBytes(r2msg.GetC1()), round.key.NTildej[i], - round.key.PaillierSK) + round.key.PaillierSK, + contextJ) alphas[j] = alphaIj if err != nil { errChs <- round.WrapError(err, Pj) @@ -81,7 +83,8 @@ func (round *round3) Start() *tss.Error { round.key.NTildej[i], round.key.H1j[i], round.key.H2j[i], - round.key.PaillierSK) + round.key.PaillierSK, + contextJ) us[j] = uIj if err != nil { errChs <- round.WrapError(err, Pj) @@ -122,16 +125,18 @@ func (round *round3) Start() *tss.Error { } func (round *round3) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound3Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round3) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_4.go b/ecdsa/signing/round_4.go index 9048ff009..019d7a58a 100644 --- a/ecdsa/signing/round_4.go +++ b/ecdsa/signing/round_4.go @@ -41,7 +41,9 @@ func (round *round4) Start() *tss.Error { // compute the multiplicative inverse thelta mod q thetaInverse = modN.ModInverse(thetaInverse) - piGamma, err := schnorr.NewZKProof(round.temp.gamma, round.temp.pointGamma) + i := round.PartyID().Index + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + piGamma, err := schnorr.NewZKProofWithSession(contextI, round.temp.gamma, round.temp.pointGamma) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(gamma, bigGamma)")) } @@ -54,16 +56,18 @@ func (round *round4) Start() *tss.Error { } func (round *round4) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound4Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round4) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_5.go b/ecdsa/signing/round_5.go index bcaefa5a6..f3dec630d 100644 --- a/ecdsa/signing/round_5.go +++ b/ecdsa/signing/round_5.go @@ -8,6 +8,7 @@ package signing import ( "errors" + "math/big" errors2 "github.com/pkg/errors" @@ -46,7 +47,8 @@ func (round *round5) Start() *tss.Error { if err != nil { return round.WrapError(errors.New("failed to unmarshal bigGamma proof"), Pj) } - ok = proof.Verify(bigGammaJPoint) + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + ok = proof.VerifyWithSession(contextJ, bigGammaJPoint) if !ok { return round.WrapError(errors.New("failed to prove bigGamma"), Pj) } @@ -96,16 +98,18 @@ func (round *round5) Start() *tss.Error { } func (round *round5) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound5Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round5) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_6.go b/ecdsa/signing/round_6.go index de9306548..db1f92c62 100644 --- a/ecdsa/signing/round_6.go +++ b/ecdsa/signing/round_6.go @@ -8,9 +8,11 @@ package signing import ( "errors" + "math/big" errors2 "github.com/pkg/errors" + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto/schnorr" "github.com/bnb-chain/tss-lib/tss" ) @@ -23,11 +25,13 @@ func (round *round6) Start() *tss.Error { round.started = true round.resetOK() - piAi, err := schnorr.NewZKProof(round.temp.roi, round.temp.bigAi) + i := round.PartyID().Index + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + piAi, err := schnorr.NewZKProofWithSession(contextI, round.temp.roi, round.temp.bigAi) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(roi, bigAi)")) } - piV, err := schnorr.NewZKVProof(round.temp.bigVi, round.temp.bigR, round.temp.si, round.temp.li) + piV, err := schnorr.NewZKVProofWithSession(contextI, round.temp.bigVi, round.temp.bigR, round.temp.si, round.temp.li) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKVProof(bigVi, bigR, si, li)")) } @@ -39,16 +43,18 @@ func (round *round6) Start() *tss.Error { } func (round *round6) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound6Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round6) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_7.go b/ecdsa/signing/round_7.go index 3242b64b9..876da1ff8 100644 --- a/ecdsa/signing/round_7.go +++ b/ecdsa/signing/round_7.go @@ -51,12 +51,13 @@ func (round *round7) Start() *tss.Error { return round.WrapError(errors2.Wrapf(err, "NewECPoint(bigAj)"), Pj) } bigAjs[j] = bigAj + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) pijA, err := r6msg.UnmarshalZKProof(round.Params().EC()) - if err != nil || !pijA.Verify(bigAj) { + if err != nil || !pijA.VerifyWithSession(contextJ, bigAj) { return round.WrapError(errors.New("schnorr verify for Aj failed"), Pj) } pijV, err := r6msg.UnmarshalZKVProof(round.Params().EC()) - if err != nil || !pijV.Verify(bigVj, round.temp.bigR) { + if err != nil || !pijV.VerifyWithSession(contextJ, bigVj, round.temp.bigR) { return round.WrapError(errors.New("vverify for Vj failed"), Pj) } } @@ -92,16 +93,18 @@ func (round *round7) Start() *tss.Error { } func (round *round7) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound7Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round7) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_8.go b/ecdsa/signing/round_8.go index 361a490bf..c57813b6a 100644 --- a/ecdsa/signing/round_8.go +++ b/ecdsa/signing/round_8.go @@ -28,16 +28,18 @@ func (round *round8) Start() *tss.Error { } func (round *round8) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound8Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round8) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/round_9.go b/ecdsa/signing/round_9.go index dcc7c083d..bcd37035a 100644 --- a/ecdsa/signing/round_9.go +++ b/ecdsa/signing/round_9.go @@ -51,16 +51,18 @@ func (round *round9) Start() *tss.Error { } func (round *round9) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound9Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round9) CanAccept(msg tss.ParsedMessage) bool { diff --git a/ecdsa/signing/rounds.go b/ecdsa/signing/rounds.go index b546b6568..418b95364 100644 --- a/ecdsa/signing/rounds.go +++ b/ecdsa/signing/rounds.go @@ -7,7 +7,10 @@ package signing import ( + "math/big" + "github.com/bnb-chain/tss-lib/common" + "github.com/bnb-chain/tss-lib/crypto" "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/bnb-chain/tss-lib/tss" ) @@ -121,3 +124,25 @@ func (round *base) resetOK() { round.ok[j] = false } } + +func (round *base) getSSID() ([]byte, error) { + ssidList := []*big.Int{ + round.EC().Params().P, + round.EC().Params().N, + round.EC().Params().B, + round.EC().Params().Gx, + round.EC().Params().Gy, + } + ssidList = append(ssidList, round.Parties().IDs().Keys()...) + bigXjList, err := crypto.FlattenECPoints(round.key.BigXj) + if err != nil { + return nil, err + } + ssidList = append(ssidList, bigXjList...) + ssidList = append(ssidList, round.key.NTildej...) + ssidList = append(ssidList, round.key.H1j...) + ssidList = append(ssidList, round.key.H2j...) + ssidList = append(ssidList, big.NewInt(int64(round.number))) + ssidList = append(ssidList, round.temp.ssidNonce) + return common.SHA512_256i(ssidList...).Bytes(), nil +} diff --git a/eddsa/keygen/local_party_test.go b/eddsa/keygen/local_party_test.go index 0a5adf1d2..9eda7338c 100644 --- a/eddsa/keygen/local_party_test.go +++ b/eddsa/keygen/local_party_test.go @@ -147,9 +147,9 @@ keygen: // fails if threshold cannot be satisfied (bad share) { - badShares := pShares[:threshold] + badShares := pShares[:threshold+1] badShares[len(badShares)-1].Share.Set(big.NewInt(0)) - uj, err := pShares[:threshold].ReConstruct(tss.Edwards()) + uj, err := pShares[:threshold+1].ReConstruct(tss.Edwards()) assert.NoError(t, err) assert.NotEqual(t, parties[j].temp.ui, uj) BigXjX, BigXjY := tss.Edwards().ScalarBaseMult(uj.Bytes()) diff --git a/eddsa/keygen/round_1.go b/eddsa/keygen/round_1.go index a799d27c3..5861db5a7 100644 --- a/eddsa/keygen/round_1.go +++ b/eddsa/keygen/round_1.go @@ -89,17 +89,19 @@ func (round *round1) CanAccept(msg tss.ParsedMessage) bool { } func (round *round1) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.kgRound1Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } // vss check is in round 2 round.ok[j] = true } - return true, nil + return ret, nil } func (round *round1) NextRound() tss.Round { diff --git a/eddsa/keygen/round_2.go b/eddsa/keygen/round_2.go index 0db0d1b5c..32ce3f327 100644 --- a/eddsa/keygen/round_2.go +++ b/eddsa/keygen/round_2.go @@ -69,21 +69,24 @@ func (round *round2) CanAccept(msg tss.ParsedMessage) bool { } func (round *round2) Update() (bool, *tss.Error) { + ret := true // guard - VERIFY de-commit for all Pj for j, msg := range round.temp.kgRound2Message1s { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } msg2 := round.temp.kgRound2Message2s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round2) NextRound() tss.Round { diff --git a/eddsa/resharing/round_1_old_step_1.go b/eddsa/resharing/round_1_old_step_1.go index 5b5588cb4..4e1b80f9d 100644 --- a/eddsa/resharing/round_1_old_step_1.go +++ b/eddsa/resharing/round_1_old_step_1.go @@ -89,16 +89,22 @@ func (round *round1) Update() (bool, *tss.Error) { if !round.ReSharingParameters.IsNewCommittee() { return true, nil } + ret := true // accept messages from old -> new committee for j, msg := range round.temp.dgRound1Messages { if round.oldOK[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.oldOK[j] = true + if round.temp.dgRound1Messages[0] == nil { + ret = false + continue + } // save the eddsa pub received from the old committee r1msg := round.temp.dgRound1Messages[0].Content().(*DGRound1Message) candidate, err := r1msg.UnmarshalEDDSAPub(round.Params().EC()) @@ -112,7 +118,7 @@ func (round *round1) Update() (bool, *tss.Error) { } round.save.EDDSAPub = candidate } - return true, nil + return ret, nil } func (round *round1) NextRound() tss.Round { diff --git a/eddsa/resharing/round_2_new_step_1.go b/eddsa/resharing/round_2_new_step_1.go index 2a61d7f4f..86b698e76 100644 --- a/eddsa/resharing/round_2_new_step_1.go +++ b/eddsa/resharing/round_2_new_step_1.go @@ -50,18 +50,20 @@ func (round *round2) Update() (bool, *tss.Error) { return true, nil } + ret := true // accept messages from new -> old committee for j, msg := range round.temp.dgRound2Messages { if round.newOK[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.newOK[j] = true } - return true, nil + return ret, nil } func (round *round2) NextRound() tss.Round { diff --git a/eddsa/resharing/round_4_new_step_2.go b/eddsa/resharing/round_4_new_step_2.go index 18acfe4df..ffcb52b9f 100644 --- a/eddsa/resharing/round_4_new_step_2.go +++ b/eddsa/resharing/round_4_new_step_2.go @@ -140,17 +140,19 @@ func (round *round4) CanAccept(msg tss.ParsedMessage) bool { } func (round *round4) Update() (bool, *tss.Error) { + ret := true // accept messages from new -> old&new committees for j, msg := range round.temp.dgRound4Messages { if round.newOK[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.newOK[j] = true } - return true, nil + return ret, nil } func (round *round4) NextRound() tss.Round { diff --git a/eddsa/signing/finalize.go b/eddsa/signing/finalize.go index 25c28b4ca..d383355ab 100644 --- a/eddsa/signing/finalize.go +++ b/eddsa/signing/finalize.go @@ -43,7 +43,13 @@ func (round *finalization) Start() *tss.Error { round.data.Signature = append(bigIntToEncodedBytes(round.temp.r)[:], sumS[:]...) round.data.R = round.temp.r.Bytes() round.data.S = s.Bytes() - round.data.M = round.temp.m.Bytes() + if round.temp.fullBytesLen == 0 { + round.data.M = round.temp.m.Bytes() + } else { + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + round.data.M = mBytes + } pk := edwards.PublicKey{ Curve: round.Params().EC(), @@ -51,7 +57,7 @@ func (round *finalization) Start() *tss.Error { Y: round.key.EDDSAPub.Y(), } - ok := edwards.Verify(&pk, round.temp.m.Bytes(), round.temp.r, s) + ok := edwards.Verify(&pk, round.data.M, round.temp.r, s) if !ok { return round.WrapError(fmt.Errorf("signature verification failed")) } diff --git a/eddsa/signing/local_party.go b/eddsa/signing/local_party.go index 56aa5f1c0..0c5acfb40 100644 --- a/eddsa/signing/local_party.go +++ b/eddsa/signing/local_party.go @@ -50,8 +50,9 @@ type ( wi, m, ri *big.Int - pointRi *crypto.ECPoint - deCommit cmt.HashDeCommitment + fullBytesLen int + pointRi *crypto.ECPoint + deCommit cmt.HashDeCommitment // round 2 cjs []*big.Int @@ -68,6 +69,7 @@ func NewLocalParty( key keygen.LocalPartySaveData, out chan<- tss.Message, end chan<- common.SignatureData, + fullBytesLen ...int, ) tss.Party { partyCount := len(params.Parties().IDs()) p := &LocalParty{ @@ -86,6 +88,9 @@ func NewLocalParty( // temp data init p.temp.m = msg + if len(fullBytesLen) > 0 { + p.temp.fullBytesLen = fullBytesLen[0] + } p.temp.cjs = make([]*big.Int, partyCount) return p } diff --git a/eddsa/signing/local_party_test.go b/eddsa/signing/local_party_test.go index 35a18eee4..44ecf1d91 100644 --- a/eddsa/signing/local_party_test.go +++ b/eddsa/signing/local_party_test.go @@ -7,6 +7,7 @@ package signing import ( + "encoding/hex" "fmt" "math/big" "sync/atomic" @@ -59,12 +60,14 @@ func TestE2EConcurrent(t *testing.T) { updater := test.SharedPartyUpdater - msg := big.NewInt(200) + msgData, err := hex.DecodeString("00f163ee51bcaeff9cdff5e0e3c1a646abd19885fffbab0b3b4236e0cf95c9f5") + assert.NoError(t, err) + msg := new(big.Int).SetBytes(msgData) // init the parties for i := 0; i < len(signPIDs); i++ { params := tss.NewParameters(tss.Edwards(), p2pCtx, signPIDs[i], len(signPIDs), threshold) - P := NewLocalParty(msg, params, keys[i], outCh, endCh).(*LocalParty) + P := NewLocalParty(msg, params, keys[i], outCh, endCh, len(msgData)).(*LocalParty) parties = append(parties, P) go func(P *LocalParty) { if err := P.Start(); err != nil { @@ -98,7 +101,7 @@ signing: go updater(parties[dest[0].Index], msg, errCh) } - case <-endCh: + case sig := <-endCh: atomic.AddInt32(&ended, 1) if atomic.LoadInt32(&ended) == int32(len(signPIDs)) { t.Logf("Done. Received signature data from %d participants", ended) @@ -132,8 +135,9 @@ signing: println("new sig error, ", err.Error()) } - ok := edwards.Verify(&pk, msg.Bytes(), newSig.R, newSig.S) + ok := edwards.Verify(&pk, msgData, newSig.R, newSig.S) assert.True(t, ok, "eddsa verify must pass") + assert.Equal(t, msgData, sig.M) t.Log("EDDSA signing test done.") // END EDDSA verify diff --git a/eddsa/signing/round_1.go b/eddsa/signing/round_1.go index 7af1d8078..9c262c781 100644 --- a/eddsa/signing/round_1.go +++ b/eddsa/signing/round_1.go @@ -56,16 +56,18 @@ func (round *round1) Start() *tss.Error { } func (round *round1) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound1Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round1) CanAccept(msg tss.ParsedMessage) bool { diff --git a/eddsa/signing/round_2.go b/eddsa/signing/round_2.go index 6aa89657b..47dd7b503 100644 --- a/eddsa/signing/round_2.go +++ b/eddsa/signing/round_2.go @@ -53,16 +53,18 @@ func (round *round2) CanAccept(msg tss.ParsedMessage) bool { } func (round *round2) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound2Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round2) NextRound() tss.Round { diff --git a/eddsa/signing/round_3.go b/eddsa/signing/round_3.go index cbcd103fc..f67e48466 100644 --- a/eddsa/signing/round_3.go +++ b/eddsa/signing/round_3.go @@ -50,10 +50,10 @@ func (round *round3) Start() *tss.Error { } Rj, err := crypto.NewECPoint(round.Params().EC(), coordinates[0], coordinates[1]) - Rj = Rj.EightInvEight() if err != nil { return round.WrapError(errors.Wrapf(err, "NewECPoint(Rj)"), Pj) } + Rj = Rj.EightInvEight() proof, err := r2msg.UnmarshalZKProof(round.Params().EC()) if err != nil { return round.WrapError(errors.New("failed to unmarshal Rj proof"), Pj) @@ -77,7 +77,13 @@ func (round *round3) Start() *tss.Error { h.Reset() h.Write(encodedR[:]) h.Write(encodedPubKey[:]) - h.Write(round.temp.m.Bytes()) + if round.temp.fullBytesLen == 0 { + h.Write(round.temp.m.Bytes()) + } else { + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + h.Write(mBytes) + } var lambda [64]byte h.Sum(lambda[:0]) @@ -101,16 +107,18 @@ func (round *round3) Start() *tss.Error { } func (round *round3) Update() (bool, *tss.Error) { + ret := true for j, msg := range round.temp.signRound3Messages { if round.ok[j] { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round3) CanAccept(msg tss.ParsedMessage) bool { diff --git a/tss/curve.go b/tss/curve.go index 4349c701b..d5bcdc82e 100644 --- a/tss/curve.go +++ b/tss/curve.go @@ -60,6 +60,13 @@ func GetCurveName(curve elliptic.Curve) (CurveName, bool) { return "", false } +// SameCurve returns true if both curves are registered under the same name. +func SameCurve(lhs, rhs elliptic.Curve) bool { + lName, lOk := GetCurveName(lhs) + rName, rOk := GetCurveName(rhs) + return lOk && rOk && lName == rName +} + // EC returns the current elliptic curve in use. The default is secp256k1 func EC() elliptic.Curve { return ec diff --git a/tss/params.go b/tss/params.go index 8bf741486..89764ec54 100644 --- a/tss/params.go +++ b/tss/params.go @@ -8,6 +8,7 @@ package tss import ( "crypto/elliptic" + "math/big" "runtime" "time" ) @@ -21,6 +22,10 @@ type ( threshold int concurrency int safePrimeGenTimeout time.Duration + // sessionNonce provides per-session SSID uniqueness for GG20 + // proof binding. Signing falls back to the message hash; keygen + // and resharing require callers to coordinate a shared nonce. + sessionNonce *big.Int } ReSharingParameters struct { @@ -85,6 +90,21 @@ func (params *Parameters) SetSafePrimeGenTimeout(timeout time.Duration) { params.safePrimeGenTimeout = timeout } +// SessionNonce returns the optional per-session nonce used in proof challenges. +func (params *Parameters) SessionNonce() *big.Int { + return params.sessionNonce +} + +// SetSessionNonce sets a per-session nonce that all parties in a protocol run +// must agree on. +func (params *Parameters) SetSessionNonce(nonce *big.Int) { + if nonce == nil { + params.sessionNonce = nil + return + } + params.sessionNonce = new(big.Int).Set(nonce) +} + // ----- // // Exported, used in `tss` client diff --git a/tss/party.go b/tss/party.go index 583a59b39..f7e3d2463 100644 --- a/tss/party.go +++ b/tss/party.go @@ -79,7 +79,10 @@ func (p *BaseParty) ValidateMessage(msg ParsedMessage) (bool, *Error) { } func (p *BaseParty) String() string { - return fmt.Sprintf("round: %d", p.round().RoundNumber()) + if rnd := p.round(); rnd != nil { + return fmt.Sprintf("round: %d", rnd.RoundNumber()) + } + return "No more rounds" } // ----- From 4758b7947d2c20606edcd8b2770bd9fe72330e36 Mon Sep 17 00:00:00 2001 From: maclane Date: Sun, 17 May 2026 14:16:49 -0500 Subject: [PATCH 02/24] Address hardening review findings --- BNB_HARDENING_INTEGRATION.md | 17 ++++++++++---- common/hash.go | 4 ---- common/hash_utils.go | 7 +++--- crypto/mta/share_protocol_test.go | 30 +++++++++++++++++++++++ crypto/paillier/factor_proof.go | 16 +++++++++++++ crypto/paillier/factor_proof_test.go | 13 ++++++++++ ecdsa/keygen/round_3.go | 4 ++++ ecdsa/resharing/local_party_test.go | 34 ++++++++++++++++++++++++--- ecdsa/resharing/round_3_old_step_2.go | 9 ++++--- ecdsa/resharing/round_4_new_step_2.go | 12 ++++++---- ecdsa/resharing/round_5_new_step_3.go | 10 ++++++-- ecdsa/signing/local_party_test.go | 4 ++-- ecdsa/signing/round_1.go | 9 ++++--- eddsa/keygen/local_party.go | 2 ++ eddsa/keygen/round_1.go | 11 +++++++++ eddsa/keygen/round_2.go | 5 +++- eddsa/keygen/round_3.go | 3 ++- eddsa/keygen/rounds.go | 11 +++++++++ eddsa/resharing/local_party_test.go | 28 ++++++++++++++++++++-- eddsa/resharing/round_3_old_step_2.go | 9 ++++--- eddsa/signing/local_party.go | 3 +++ eddsa/signing/local_party_test.go | 4 ++-- eddsa/signing/round_1.go | 12 ++++++++++ eddsa/signing/round_2.go | 5 +++- eddsa/signing/round_3.go | 5 +++- eddsa/signing/rounds.go | 17 ++++++++++++++ 26 files changed, 245 insertions(+), 39 deletions(-) diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index e3be491f0..6ec395cd8 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -7,19 +7,23 @@ - Common ancestor: `afbe264b44b63155a864dbe0171040c66e442963` - Goal: port applicable security and correctness hardening without replacing Threshold's Paillier/NTilde remediation or weakening `ModProof`/`FactorProof`. +## Compatibility Notice + +This is a protocol/wire compatibility break for proof transcripts. Proofs whose Fiat-Shamir challenges now use tagged hashing or session context will not verify across mixed old/new versions, even where the Go API remains source-compatible through variadic arguments. Operators should roll this out as a coordinated protocol upgrade rather than mixing parties from before and after this PR in the same keygen, signing, or resharing ceremony. + ## Ported Or Manually Adapted - `3d95e54` / PR `#252`, ECDSA protocol security updates: manually adapted tagged challenges, MtA/range-proof validation, session-context plumbing, and proof boundary checks while preserving Threshold's existing Paillier/NTilde proof model. -- `1a14f3a` / PR `#256`, ECDSA proof session byte: manually adapted proof-session APIs for DLN, Schnorr, MtA, Paillier mod proof, and factor proof. Public callers remain backward compatible through variadic session parameters. +- `1a14f3a` / PR `#256`, ECDSA proof session byte: manually adapted proof-session APIs for DLN, Schnorr, MtA, Paillier mod proof, and factor proof. Public callers remain source-compatible through variadic session parameters, but generated proof transcripts are not wire-compatible with old versions. - `ff989bf` / PR `#257`, tagged hash encoding: ported length-delimited tagged hashing as `common.SHA512_256i_TAGGED`. - `f3aad28` / PR `#276`, nil `String()` panic: ported `BaseParty.String()` nil-round guard. -- `409542e` / PR `#282`, round update correctness: ported the `round.ok` accumulation fix for ECDSA/EdDSA keygen, signing, and resharing rounds, plus the resharing party-0 broadcast nil guard. +- `409542e` / PR `#282`, round update correctness: ported the `round.ok` accumulation fix for all non-terminal ECDSA/EdDSA keygen, signing, and resharing rounds, plus the resharing party-0 broadcast nil guard. - `9acd90b`, `2f294cf`, `6b92e7d`, `c0de534` / PR `#284`, leading-zero message signing: manually adapted for ECDSA and EdDSA with backward-compatible variadic `fullBytesLen` parameters. EdDSA now also hashes the full-length message bytes in round 3. - `843de68` / PR `#291`, VSS threshold-size validation: ported `len(vs) == threshold+1` verification and added test coverage. - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. -- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce` and ECDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce` and ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. @@ -41,9 +45,10 @@ ## Semantic Differences From BNB - Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation was retained. No BNB no-proof escape hatches were introduced. -- Session parameters were added as variadic arguments to preserve existing public call sites. BNB's newer APIs are more breaking. +- Session parameters were added as variadic arguments to preserve existing public call sites. This is API source-compatible for callers, but not wire-compatible for proof transcripts. - Keygen and resharing SSIDs are locally derived and use `Parameters.SessionNonce()` when set. This avoids protobuf/module churn, but callers must provide a unique agreed nonce for keygen/resharing sessions that need cross-session replay resistance. - ECDSA resharing SSID binding was adapted without adding BNB's newer wire-level SSID message fields. +- `common.RejectionSample` keeps BNB's function name for porting clarity, but this implementation is modular reduction rather than a looping rejection sampler. - Constant-time operations are not included and remain a residual follow-up. ## Tests @@ -51,12 +56,16 @@ - `go test ./crypto/... ./ecdsa/keygen ./ecdsa/signing ./eddsa/signing` passed. - `go test ./eddsa/keygen ./eddsa/resharing` passed after updating EdDSA VSS threshold tests and resharing nil guard. - `go test ./ecdsa/resharing` passed after the analogous resharing guard. +- `go test ./common ./crypto/paillier ./crypto/mta ./ecdsa/keygen ./ecdsa/resharing ./ecdsa/signing ./eddsa/keygen ./eddsa/signing ./eddsa/resharing` passed after review fixes. - `go test ./...` passed. +- `go vet ./...` passed. Added or updated focused tests cover: - DLN, Schnorr, Paillier mod proof, factor proof, and MtA range proof session mismatch/replay failures. +- Non-invertible malformed factor-proof bases returning errors instead of panicking. - MtA range-proof malformed ciphertext and proof-value boundary failures. +- ProofBobWC malformed lower-bound, zero-value, and curve-mismatch failures. - VSS `threshold+1` verification/reconstruction behavior. - Non-canonical EC coordinate rejection. - ECDSA and EdDSA leading-zero message signing. diff --git a/common/hash.go b/common/hash.go index e48a58006..75764ea7f 100644 --- a/common/hash.go +++ b/common/hash.go @@ -109,10 +109,6 @@ func SHA512_256i_TAGGED(tag []byte, in ...*big.Int) *big.Int { } inLen := len(in) - if inLen == 0 { - return nil - } - bzSize := 0 inLenBz := make([]byte, 64/8) binary.LittleEndian.PutUint64(inLenBz, uint64(inLen)) diff --git a/common/hash_utils.go b/common/hash_utils.go index 1d3a88c3d..ea89acd8b 100644 --- a/common/hash_utils.go +++ b/common/hash_utils.go @@ -21,9 +21,10 @@ func LiterallyJustMod(q *big.Int, eHash *big.Int) *big.Int { // e' = eHash return e } -// RejectionSample implements the upstream challenge reduction name. For the -// secp256k1 and ed25519 group orders used here this has the same behavior as -// LiterallyJustMod. +// RejectionSample preserves the upstream challenge-reduction function name. +// This implementation reduces the hash modulo q rather than looping with fresh +// hash material, so callers must only use it where modular-reduction bias is +// acceptable for the proof challenge. func RejectionSample(q *big.Int, eHash *big.Int) *big.Int { return LiterallyJustMod(q, eHash) } diff --git a/crypto/mta/share_protocol_test.go b/crypto/mta/share_protocol_test.go index 313163e16..a71ba9259 100644 --- a/crypto/mta/share_protocol_test.go +++ b/crypto/mta/share_protocol_test.go @@ -84,6 +84,18 @@ func TestShareProtocolWC(t *testing.T) { assert.NoError(t, err) _, cB, betaPrm, pfB, err := BobMidWC(tss.EC(), pk, pf, b, cA, NTildei, h1i, h2i, NTildej, h1j, h2j, gBPoint) assert.NoError(t, err) + assert.True(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint)) + + badS1 := cloneProofBobWC(pfB) + badS1.S1 = new(big.Int).Sub(q, big.NewInt(1)) + assert.False(t, badS1.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "S1 below q must fail") + + badV := cloneProofBobWC(pfB) + badV.V = big.NewInt(0) + assert.False(t, badV.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "V equal to zero must fail") + + wrongCurveX := crypto.NewECPointNoCurveCheck(tss.Edwards(), gBPoint.X(), gBPoint.Y()) + assert.False(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, wrongCurveX), "X on a different curve must fail") alpha, err := AliceEndWC(tss.EC(), pk, pfB, gBPoint, cA, cB, NTildei, h1i, h2i, sk) assert.NoError(t, err) @@ -94,3 +106,21 @@ func TestShareProtocolWC(t *testing.T) { aTimesBPlusBetaModQ := new(big.Int).Mod(aTimesBPlusBeta, q) assert.Equal(t, 0, alpha.Cmp(aTimesBPlusBetaModQ)) } + +func cloneProofBobWC(pf *ProofBobWC) *ProofBobWC { + return &ProofBobWC{ + ProofBob: &ProofBob{ + Z: new(big.Int).Set(pf.Z), + ZPrm: new(big.Int).Set(pf.ZPrm), + T: new(big.Int).Set(pf.T), + V: new(big.Int).Set(pf.V), + W: new(big.Int).Set(pf.W), + S: new(big.Int).Set(pf.S), + S1: new(big.Int).Set(pf.S1), + S2: new(big.Int).Set(pf.S2), + T1: new(big.Int).Set(pf.T1), + T2: new(big.Int).Set(pf.T2), + }, + U: pf.U, + } +} diff --git a/crypto/paillier/factor_proof.go b/crypto/paillier/factor_proof.go index ef8f82530..009008d7a 100644 --- a/crypto/paillier/factor_proof.go +++ b/crypto/paillier/factor_proof.go @@ -89,6 +89,22 @@ func (pf FactorProof) FactorVerify(pkN, N, s, t *big.Int, session ...[]byte) (bo if common.AnyIsNil(pf.P, pf.Q, pf.A, pf.B, pf.T, pf.Sigma, pf.Z1, pf.Z2, pf.W1, pf.W2, pf.V) { return false, fmt.Errorf("fac proof verify: nil bigint present in proof") } + if N.Sign() <= 0 { + return false, fmt.Errorf("fac proof verify: invalid modulus %x", N) + } + for name, base := range map[string]*big.Int{ + "s": s, + "t": t, + "P": pf.P, + "Q": pf.Q, + "A": pf.A, + "B": pf.B, + "T": pf.T, + } { + if !common.IsInInterval(base, N) || !common.Coprime(base, N) { + return false, fmt.Errorf("fac proof verify: base %s = %x is not invertible modulo N", name, base) + } + } e := FactorChallenge(N, s, t, pkN, pf.P, pf.Q, pf.A, pf.B, pf.T, pf.Sigma, session...) diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index 57178dfac..6bd79b694 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -135,6 +135,19 @@ func TestFactorProofVerifyFail3(t *testing.T) { assert.False(t, res, "proof verify result must be false") } +func TestFactorProofVerifyRejectsNonInvertibleBase(t *testing.T) { + facSetUp(t) + proof := privateKey.FactorProof(auxPrime.N, s, tt) + proof.Q = big.NewInt(0) + proof.Z1 = big.NewInt(-1) + + assert.NotPanics(t, func() { + 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/ecdsa/keygen/round_3.go b/ecdsa/keygen/round_3.go index b25b9d195..79cc543c5 100644 --- a/ecdsa/keygen/round_3.go +++ b/ecdsa/keygen/round_3.go @@ -100,18 +100,22 @@ func (round *round3) Start() *tss.Error { ok, err = FacProof.FactorVerify(pkN, NTilde, H1i, H2i, contextJ) if err != nil { ch <- vssOut{err, nil} + return } if !ok { ch <- vssOut{errors.New("factor proof verify failed"), nil} + return } FacProofTilde := r2msg1.UnmarshalFactorProofTilde() NTildej := round.save.NTildej[j] ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i, contextJ) if err != nil { ch <- vssOut{err, nil} + return } if !ok { ch <- vssOut{errors.New("factor proof verify failed"), nil} + return } // (9) handled above ch <- vssOut{nil, PjVs} diff --git a/ecdsa/resharing/local_party_test.go b/ecdsa/resharing/local_party_test.go index 8607e200c..390527c2d 100644 --- a/ecdsa/resharing/local_party_test.go +++ b/ecdsa/resharing/local_party_test.go @@ -10,6 +10,7 @@ import ( "crypto/ecdsa" "fmt" "math/big" + "reflect" "runtime" "sync/atomic" "testing" @@ -169,6 +170,12 @@ signing: signErrCh := make(chan *tss.Error, len(signPIDs)) signOutCh := make(chan tss.Message, len(signPIDs)) signEndCh := make(chan common.SignatureData, len(signPIDs)) + signResultCh := make(chan signatureDataParts, len(signPIDs)) + go func() { + for i := 0; i < len(signPIDs); i++ { + signResultCh <- recvSignatureDataParts(signEndCh) + } + }() for j, signPID := range signPIDs { params := tss.NewParameters(tss.S256(), signP2pCtx, signPID, len(signPIDs), newThreshold) @@ -206,7 +213,7 @@ signing: go updater(signParties[dest[0].Index], msg, signErrCh) } - case signData := <-signEndCh: + case signData := <-signResultCh: atomic.AddInt32(&signEnded, 1) if atomic.LoadInt32(&signEnded) == int32(len(signPIDs)) { t.Logf("Signing done. Received sign data from %d participants", signEnded) @@ -219,8 +226,8 @@ signing: Y: pkY, } ok := ecdsa.Verify(&pk, big.NewInt(42).Bytes(), - new(big.Int).SetBytes(signData.R), - new(big.Int).SetBytes(signData.S)) + new(big.Int).SetBytes(signData.r), + new(big.Int).SetBytes(signData.s)) assert.True(t, ok, "ecdsa verify must pass") t.Log("ECDSA signing test done.") @@ -231,3 +238,24 @@ signing: } } } + +type signatureDataParts struct { + signature []byte + r []byte + s []byte +} + +func recvSignatureDataParts(ch <-chan common.SignatureData) signatureDataParts { + _, value, ok := reflect.Select([]reflect.SelectCase{{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(ch), + }}) + if !ok { + return signatureDataParts{} + } + return signatureDataParts{ + signature: append([]byte(nil), value.FieldByName("Signature").Bytes()...), + r: append([]byte(nil), value.FieldByName("R").Bytes()...), + s: append([]byte(nil), value.FieldByName("S").Bytes()...), + } +} diff --git a/ecdsa/resharing/round_3_old_step_2.go b/ecdsa/resharing/round_3_old_step_2.go index fcaf755a9..f1a8b99a7 100644 --- a/ecdsa/resharing/round_3_old_step_2.go +++ b/ecdsa/resharing/round_3_old_step_2.go @@ -62,21 +62,24 @@ func (round *round3) Update() (bool, *tss.Error) { if !round.ReSharingParams().IsNewCommittee() { return true, nil } + ret := true // accept messages from old -> new committee for j, msg1 := range round.temp.dgRound3Message1s { if round.oldOK[j] { continue } if msg1 == nil || !round.CanAccept(msg1) { - return false, nil + ret = false + continue } msg2 := round.temp.dgRound3Message2s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.oldOK[j] = true } - return true, nil + return ret, nil } func (round *round3) NextRound() tss.Round { diff --git a/ecdsa/resharing/round_4_new_step_2.go b/ecdsa/resharing/round_4_new_step_2.go index ee74483f6..550b88ac3 100644 --- a/ecdsa/resharing/round_4_new_step_2.go +++ b/ecdsa/resharing/round_4_new_step_2.go @@ -257,6 +257,7 @@ func (round *round4) CanAccept(msg tss.ParsedMessage) bool { } func (round *round4) Update() (bool, *tss.Error) { + ret := true if round.ReSharingParameters.IsNewCommittee() { // accept messages from new -> everyone for j, msg1 := range round.temp.dgRound4Message2s { @@ -264,12 +265,14 @@ func (round *round4) Update() (bool, *tss.Error) { continue } if msg1 == nil || !round.CanAccept(msg1) { - return false, nil + ret = false + continue } // accept message from new -> new committee msg2 := round.temp.dgRound4Message1s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.newOK[j] = true } @@ -280,14 +283,15 @@ func (round *round4) Update() (bool, *tss.Error) { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.newOK[j] = true } } else { return false, round.WrapError(errors.New("this party is not in the old or the new committee"), round.PartyID()) } - return true, nil + return ret, nil } func (round *round4) NextRound() tss.Round { diff --git a/ecdsa/resharing/round_5_new_step_3.go b/ecdsa/resharing/round_5_new_step_3.go index 2fc14d9ff..4225f9829 100644 --- a/ecdsa/resharing/round_5_new_step_3.go +++ b/ecdsa/resharing/round_5_new_step_3.go @@ -64,18 +64,22 @@ func (round *round5) Start() *tss.Error { ok, err := FacProof.FactorVerify(pkN, NTilde, H1i, H2i, contextJ) if err != nil { ch <- proofOut{err} + return } if !ok { ch <- proofOut{errors.New("factor proof verify failed")} + return } FacProofTilde := r4msg1.UnmarshalFactorProofTilde() NTildej := round.save.NTildej[j] ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i, contextJ) if err != nil { ch <- proofOut{err} + return } if !ok { ch <- proofOut{errors.New("factor proof verify failed")} + return } // (9) handled above ch <- proofOut{nil} @@ -121,6 +125,7 @@ func (round *round5) CanAccept(msg tss.ParsedMessage) bool { } func (round *round5) Update() (bool, *tss.Error) { + ret := true if round.ReSharingParameters.IsNewCommittee() || round.ReSharingParams().IsOldCommittee() { // accept messages from new -> everyone for j, msg := range round.temp.dgRound5Messages { @@ -128,14 +133,15 @@ func (round *round5) Update() (bool, *tss.Error) { continue } if msg == nil || !round.CanAccept(msg) { - return false, nil + ret = false + continue } round.newOK[j] = true } } else { return false, round.WrapError(errors.New("this party is not in the old or the new committee"), round.PartyID()) } - return true, nil + return ret, nil } func (round *round5) NextRound() tss.Round { diff --git a/ecdsa/signing/local_party_test.go b/ecdsa/signing/local_party_test.go index 672f6efdf..a923f7a06 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -100,7 +100,7 @@ signing: go updater(parties[dest[0].Index], msg, errCh) } - case sig := <-endCh: + case <-endCh: atomic.AddInt32(&ended, 1) if atomic.LoadInt32(&ended) == int32(len(signPIDs)) { t.Logf("Done. Received signature data from %d participants", ended) @@ -127,7 +127,7 @@ signing: } ok := ecdsa.Verify(&pk, msgData, R.X(), sumS) assert.True(t, ok, "ecdsa verify must pass") - assert.Equal(t, msgData, sig.M) + assert.Equal(t, msgData, parties[0].data.M) t.Log("ECDSA signing test done.") // END ECDSA verify diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index 7229e3d69..ea029e1fd 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -91,20 +91,23 @@ func (round *round1) Start() *tss.Error { } func (round *round1) Update() (bool, *tss.Error) { + ret := true for j, msg1 := range round.temp.signRound1Message1s { if round.ok[j] { continue } if msg1 == nil || !round.CanAccept(msg1) { - return false, nil + ret = false + continue } msg2 := round.temp.signRound1Message2s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.ok[j] = true } - return true, nil + return ret, nil } func (round *round1) CanAccept(msg tss.ParsedMessage) bool { diff --git a/eddsa/keygen/local_party.go b/eddsa/keygen/local_party.go index 8d94f5d4a..7e3c59389 100644 --- a/eddsa/keygen/local_party.go +++ b/eddsa/keygen/local_party.go @@ -51,6 +51,8 @@ type ( vs vss.Vs shares vss.Shares deCommitPolyG cmt.HashDeCommitment + ssid []byte + ssidNonce *big.Int } ) diff --git a/eddsa/keygen/round_1.go b/eddsa/keygen/round_1.go index 5861db5a7..634f06f20 100644 --- a/eddsa/keygen/round_1.go +++ b/eddsa/keygen/round_1.go @@ -38,6 +38,17 @@ func (round *round1) Start() *tss.Error { Pi := round.PartyID() i := Pi.Index + if nonce := round.Params().SessionNonce(); nonce != nil { + round.temp.ssidNonce = new(big.Int).Set(nonce) + } else { + round.temp.ssidNonce = new(big.Int).SetUint64(0) + } + ssid, err := round.getSSID() + if err != nil { + return round.WrapError(err) + } + round.temp.ssid = ssid + // 1. calculate "partial" key share ui ui := common.GetRandomPositiveInt(round.Params().EC().Params().N) round.temp.ui = ui diff --git a/eddsa/keygen/round_2.go b/eddsa/keygen/round_2.go index 32ce3f327..ccdb0fdb1 100644 --- a/eddsa/keygen/round_2.go +++ b/eddsa/keygen/round_2.go @@ -8,9 +8,11 @@ package keygen import ( "errors" + "math/big" errors2 "github.com/pkg/errors" + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto/schnorr" "github.com/bnb-chain/tss-lib/tss" ) @@ -45,7 +47,8 @@ func (round *round2) Start() *tss.Error { } // 5. compute Schnorr prove - pii, err := schnorr.NewZKProof(round.temp.ui, round.temp.vs[0]) + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(i))) + pii, err := schnorr.NewZKProofWithSession(contextI, round.temp.ui, round.temp.vs[0]) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(ui, vi0)")) } diff --git a/eddsa/keygen/round_3.go b/eddsa/keygen/round_3.go index 7a82f8831..a0231cb57 100644 --- a/eddsa/keygen/round_3.go +++ b/eddsa/keygen/round_3.go @@ -65,6 +65,7 @@ func (round *round3) Start() *tss.Error { if j == PIdx { continue } + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(j))) // 6-9. go func(j int, ch chan<- vssOut) { // 4-10. @@ -92,7 +93,7 @@ func (round *round3) Start() *tss.Error { ch <- vssOut{errors.New("failed to unmarshal schnorr proof"), nil} return } - ok = proof.Verify(PjVs[0]) + ok = proof.VerifyWithSession(contextJ, PjVs[0]) if !ok { ch <- vssOut{errors.New("failed to prove schnorr proof"), nil} return diff --git a/eddsa/keygen/rounds.go b/eddsa/keygen/rounds.go index f87f47a4b..f67d47aaa 100644 --- a/eddsa/keygen/rounds.go +++ b/eddsa/keygen/rounds.go @@ -7,6 +7,9 @@ package keygen import ( + "math/big" + + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/tss" ) @@ -82,3 +85,11 @@ func (round *base) resetOK() { round.ok[j] = false } } + +func (round *base) getSSID() ([]byte, error) { + ssidList := []*big.Int{round.Params().EC().Params().P, round.Params().EC().Params().N, round.Params().EC().Params().Gx, round.Params().EC().Params().Gy} + ssidList = append(ssidList, round.Parties().IDs().Keys()...) + ssidList = append(ssidList, big.NewInt(int64(round.number))) + ssidList = append(ssidList, round.temp.ssidNonce) + return common.SHA512_256i(ssidList...).Bytes(), nil +} diff --git a/eddsa/resharing/local_party_test.go b/eddsa/resharing/local_party_test.go index 4aa8f85cf..e92048a0a 100644 --- a/eddsa/resharing/local_party_test.go +++ b/eddsa/resharing/local_party_test.go @@ -8,6 +8,7 @@ package resharing_test import ( "math/big" + "reflect" "sync/atomic" "testing" @@ -162,6 +163,12 @@ signing: signErrCh := make(chan *tss.Error, len(signPIDs)) signOutCh := make(chan tss.Message, len(signPIDs)) signEndCh := make(chan common.SignatureData, len(signPIDs)) + signResultCh := make(chan signatureDataParts, len(signPIDs)) + go func() { + for i := 0; i < len(signPIDs); i++ { + signResultCh <- recvSignatureDataParts(signEndCh) + } + }() for j, signPID := range signPIDs { params := tss.NewParameters(tss.Edwards(), signP2pCtx, signPID, len(signPIDs), newThreshold) @@ -198,7 +205,7 @@ signing: go updater(signParties[dest[0].Index], msg, signErrCh) } - case signData := <-signEndCh: + case signData := <-signResultCh: atomic.AddInt32(&signEnded, 1) if atomic.LoadInt32(&signEnded) == int32(len(signPIDs)) { t.Logf("Signing done. Received sign data from %d participants", signEnded) @@ -211,7 +218,7 @@ signing: Y: pkY, } - newSig, err := edwards.ParseSignature(signData.Signature) + newSig, err := edwards.ParseSignature(signData.signature) if err != nil { println("new sig error, ", err.Error()) } @@ -228,3 +235,20 @@ signing: } } } + +type signatureDataParts struct { + signature []byte +} + +func recvSignatureDataParts(ch <-chan common.SignatureData) signatureDataParts { + _, value, ok := reflect.Select([]reflect.SelectCase{{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(ch), + }}) + if !ok { + return signatureDataParts{} + } + return signatureDataParts{ + signature: append([]byte(nil), value.FieldByName("Signature").Bytes()...), + } +} diff --git a/eddsa/resharing/round_3_old_step_2.go b/eddsa/resharing/round_3_old_step_2.go index 21e441065..dbb26bf62 100644 --- a/eddsa/resharing/round_3_old_step_2.go +++ b/eddsa/resharing/round_3_old_step_2.go @@ -64,21 +64,24 @@ func (round *round3) Update() (bool, *tss.Error) { return true, nil } + ret := true // accept messages from old -> new committee for j, msg1 := range round.temp.dgRound3Message1s { if round.oldOK[j] { continue } if msg1 == nil || !round.CanAccept(msg1) { - return false, nil + ret = false + continue } msg2 := round.temp.dgRound3Message2s[j] if msg2 == nil || !round.CanAccept(msg2) { - return false, nil + ret = false + continue } round.oldOK[j] = true } - return true, nil + return ret, nil } func (round *round3) NextRound() tss.Round { diff --git a/eddsa/signing/local_party.go b/eddsa/signing/local_party.go index 0c5acfb40..6bcd272d8 100644 --- a/eddsa/signing/local_party.go +++ b/eddsa/signing/local_party.go @@ -60,6 +60,9 @@ type ( // round 3 r *big.Int + + ssid []byte + ssidNonce *big.Int } ) diff --git a/eddsa/signing/local_party_test.go b/eddsa/signing/local_party_test.go index 44ecf1d91..a87b2c6be 100644 --- a/eddsa/signing/local_party_test.go +++ b/eddsa/signing/local_party_test.go @@ -101,7 +101,7 @@ signing: go updater(parties[dest[0].Index], msg, errCh) } - case sig := <-endCh: + case <-endCh: atomic.AddInt32(&ended, 1) if atomic.LoadInt32(&ended) == int32(len(signPIDs)) { t.Logf("Done. Received signature data from %d participants", ended) @@ -137,7 +137,7 @@ signing: ok := edwards.Verify(&pk, msgData, newSig.R, newSig.S) assert.True(t, ok, "eddsa verify must pass") - assert.Equal(t, msgData, sig.M) + assert.Equal(t, msgData, parties[0].data.M) t.Log("EDDSA signing test done.") // END EDDSA verify diff --git a/eddsa/signing/round_1.go b/eddsa/signing/round_1.go index 9c262c781..dfdb1bb3e 100644 --- a/eddsa/signing/round_1.go +++ b/eddsa/signing/round_1.go @@ -9,6 +9,7 @@ package signing import ( "errors" "fmt" + "math/big" "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto" @@ -32,6 +33,17 @@ func (round *round1) Start() *tss.Error { round.started = true round.resetOK() + if nonce := round.Params().SessionNonce(); nonce != nil { + round.temp.ssidNonce = new(big.Int).Set(nonce) + } else { + round.temp.ssidNonce = new(big.Int).Set(round.temp.m) + } + ssid, err := round.getSSID() + if err != nil { + return round.WrapError(err) + } + round.temp.ssid = ssid + // 1. select ri ri := common.GetRandomPositiveInt(round.Params().EC().Params().N) diff --git a/eddsa/signing/round_2.go b/eddsa/signing/round_2.go index 47dd7b503..3078c5874 100644 --- a/eddsa/signing/round_2.go +++ b/eddsa/signing/round_2.go @@ -8,9 +8,11 @@ package signing import ( "errors" + "math/big" errors2 "github.com/pkg/errors" + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto/schnorr" "github.com/bnb-chain/tss-lib/tss" ) @@ -32,7 +34,8 @@ func (round *round2) Start() *tss.Error { } // 2. compute Schnorr prove - pir, err := schnorr.NewZKProof(round.temp.ri, round.temp.pointRi) + contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(i))) + pir, err := schnorr.NewZKProofWithSession(contextI, round.temp.ri, round.temp.pointRi) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(ri, pointRi)")) } diff --git a/eddsa/signing/round_3.go b/eddsa/signing/round_3.go index f67e48466..c421a21cf 100644 --- a/eddsa/signing/round_3.go +++ b/eddsa/signing/round_3.go @@ -8,10 +8,12 @@ package signing import ( "crypto/sha512" + "math/big" "github.com/agl/ed25519/edwards25519" "github.com/pkg/errors" + "github.com/bnb-chain/tss-lib/common" "github.com/bnb-chain/tss-lib/crypto" "github.com/bnb-chain/tss-lib/crypto/commitments" "github.com/bnb-chain/tss-lib/tss" @@ -38,6 +40,7 @@ func (round *round3) Start() *tss.Error { continue } + contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(j))) msg := round.temp.signRound2Messages[j] r2msg := msg.Content().(*SignRound2Message) cmtDeCmt := commitments.HashCommitDecommit{C: round.temp.cjs[j], D: r2msg.UnmarshalDeCommitment()} @@ -58,7 +61,7 @@ func (round *round3) Start() *tss.Error { if err != nil { return round.WrapError(errors.New("failed to unmarshal Rj proof"), Pj) } - ok = proof.Verify(Rj) + ok = proof.VerifyWithSession(contextJ, Rj) if !ok { return round.WrapError(errors.New("failed to prove Rj"), Pj) } diff --git a/eddsa/signing/rounds.go b/eddsa/signing/rounds.go index 57adcd8d7..e5b38adda 100644 --- a/eddsa/signing/rounds.go +++ b/eddsa/signing/rounds.go @@ -7,7 +7,11 @@ package signing import ( + "errors" + "math/big" + "github.com/bnb-chain/tss-lib/common" + "github.com/bnb-chain/tss-lib/crypto" "github.com/bnb-chain/tss-lib/eddsa/keygen" "github.com/bnb-chain/tss-lib/tss" ) @@ -97,3 +101,16 @@ func (round *base) resetOK() { round.ok[j] = false } } + +func (round *base) getSSID() ([]byte, error) { + ssidList := []*big.Int{round.Params().EC().Params().P, round.Params().EC().Params().N, round.Params().EC().Params().Gx, round.Params().EC().Params().Gy} + ssidList = append(ssidList, round.Parties().IDs().Keys()...) + bigXjList, err := crypto.FlattenECPoints(round.key.BigXj) + if err != nil { + return nil, errors.New("read BigXj failed") + } + ssidList = append(ssidList, bigXjList...) + ssidList = append(ssidList, big.NewInt(int64(round.number))) + ssidList = append(ssidList, round.temp.ssidNonce) + return common.SHA512_256i(ssidList...).Bytes(), nil +} From f2e1b3f62b1b8537ffd1ae98b36ce67547b5d306 Mon Sep 17 00:00:00 2001 From: maclane Date: Sun, 17 May 2026 16:18:07 -0500 Subject: [PATCH 03/24] Address Gemini review cleanup --- BNB_HARDENING_INTEGRATION.md | 2 + common/int.go | 9 ++++ crypto/mta/share_protocol_test.go | 63 +++++++++++++++++++++++++++ ecdsa/keygen/round_1.go | 2 +- ecdsa/keygen/round_2.go | 5 +-- ecdsa/keygen/round_3.go | 2 +- ecdsa/resharing/round_2_new_step_1.go | 2 +- ecdsa/resharing/round_4_new_step_2.go | 4 +- ecdsa/resharing/round_5_new_step_3.go | 3 +- ecdsa/signing/round_1.go | 4 +- ecdsa/signing/round_2.go | 3 +- ecdsa/signing/round_3.go | 2 +- ecdsa/signing/round_4.go | 2 +- ecdsa/signing/round_5.go | 3 +- ecdsa/signing/round_6.go | 3 +- ecdsa/signing/round_7.go | 2 +- ecdsa/signing/rounds.go | 13 ++++++ eddsa/keygen/round_2.go | 3 +- eddsa/keygen/round_3.go | 2 +- eddsa/signing/round_1.go | 2 +- eddsa/signing/round_2.go | 3 +- eddsa/signing/round_3.go | 3 +- eddsa/signing/rounds.go | 13 ++++++ 23 files changed, 121 insertions(+), 29 deletions(-) diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index 6ec395cd8..f12e52835 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -26,6 +26,7 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce` and ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. +- Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID, and signing default SSID nonces are derived from full message bytes when `fullBytesLen` is provided. ## Already Covered Or Superseded @@ -66,6 +67,7 @@ Added or updated focused tests cover: - Non-invertible malformed factor-proof bases returning errors instead of panicking. - MtA range-proof malformed ciphertext and proof-value boundary failures. - ProofBobWC malformed lower-bound, zero-value, and curve-mismatch failures. +- ProofBob and ProofBobWC session mismatch/replay failures. - VSS `threshold+1` verification/reconstruction behavior. - Non-canonical EC coordinate rejection. - ECDSA and EdDSA leading-zero message signing. diff --git a/common/int.go b/common/int.go index bfe6e7825..ad487c517 100644 --- a/common/int.go +++ b/common/int.go @@ -7,6 +7,7 @@ package common import ( + "encoding/binary" "math/big" ) @@ -110,6 +111,14 @@ func AppendBigIntToBytesSlice(commonBytes []byte, appended *big.Int) []byte { return append(resultBytes, appended.Bytes()...) } +func AppendUint64ToBytesSlice(commonBytes []byte, appended uint64) []byte { + resultBytes := make([]byte, len(commonBytes), len(commonBytes)+8) + copy(resultBytes, commonBytes) + idxBytes := make([]byte, 8) + binary.BigEndian.PutUint64(idxBytes, appended) + return append(resultBytes, idxBytes...) +} + // Marshal the given bigint into bytes. // with the sign stored in the first byte and the absolute value in the rest. // `nil` or 0 is stored as the byte 0x00. diff --git a/crypto/mta/share_protocol_test.go b/crypto/mta/share_protocol_test.go index a71ba9259..abddb19ba 100644 --- a/crypto/mta/share_protocol_test.go +++ b/crypto/mta/share_protocol_test.go @@ -59,6 +59,36 @@ func TestShareProtocol(t *testing.T) { assert.Equal(t, 0, alpha.Cmp(aTimesBPlusBetaModQ)) } +func TestProofBobSessionBinding(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + sk, pk, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + q := tss.EC().Params().N + a := common.GetRandomPositiveInt(q) + b := common.GetRandomPositiveInt(q) + + NTildei, h1i, h2i, err := keygen.LoadNTildeH1H2FromTestFixture(0) + assert.NoError(t, err) + NTildej, h1j, h2j, err := keygen.LoadNTildeH1H2FromTestFixture(1) + assert.NoError(t, err) + + session := []byte("proof-bob-session-a") + cA, pf, err := AliceInit(tss.EC(), pk, a, NTildej, h1j, h2j, session) + assert.NoError(t, err) + _, cB, _, pfB, err := BobMid(tss.EC(), pk, pf, b, cA, NTildei, h1i, h2i, NTildej, h1j, h2j, session) + assert.NoError(t, err) + + assert.True(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, session), "proof must verify with the original session") + assert.False(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, []byte("proof-bob-session-b")), "proof must not replay across sessions") + assert.False(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB), "session-bound proof must not verify without its session") + + _, err = AliceEnd(tss.EC(), pk, pfB, h1i, h2i, cA, cB, NTildei, sk, []byte("proof-bob-session-b")) + assert.Error(t, err) +} + func TestShareProtocolWC(t *testing.T) { q := tss.EC().Params().N @@ -107,6 +137,39 @@ func TestShareProtocolWC(t *testing.T) { assert.Equal(t, 0, alpha.Cmp(aTimesBPlusBetaModQ)) } +func TestProofBobWCSessionBinding(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + sk, pk, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + q := tss.EC().Params().N + a := common.GetRandomPositiveInt(q) + b := common.GetRandomPositiveInt(q) + gBX, gBY := tss.EC().ScalarBaseMult(b.Bytes()) + gBPoint, err := crypto.NewECPoint(tss.EC(), gBX, gBY) + assert.NoError(t, err) + + NTildei, h1i, h2i, err := keygen.LoadNTildeH1H2FromTestFixture(0) + assert.NoError(t, err) + NTildej, h1j, h2j, err := keygen.LoadNTildeH1H2FromTestFixture(1) + assert.NoError(t, err) + + session := []byte("proof-bob-wc-session-a") + cA, pf, err := AliceInit(tss.EC(), pk, a, NTildej, h1j, h2j, session) + assert.NoError(t, err) + _, cB, _, pfB, err := BobMidWC(tss.EC(), pk, pf, b, cA, NTildei, h1i, h2i, NTildej, h1j, h2j, gBPoint, session) + assert.NoError(t, err) + + assert.True(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint, session), "proof must verify with the original session") + assert.False(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint, []byte("proof-bob-wc-session-b")), "proof must not replay across sessions") + assert.False(t, pfB.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "session-bound proof must not verify without its session") + + _, err = AliceEndWC(tss.EC(), pk, pfB, gBPoint, cA, cB, NTildei, h1i, h2i, sk, []byte("proof-bob-wc-session-b")) + assert.Error(t, err) +} + func cloneProofBobWC(pf *ProofBobWC) *ProofBobWC { return &ProofBobWC{ ProofBob: &ProofBob{ diff --git a/ecdsa/keygen/round_1.go b/ecdsa/keygen/round_1.go index 6a25e2986..54c65f914 100644 --- a/ecdsa/keygen/round_1.go +++ b/ecdsa/keygen/round_1.go @@ -90,7 +90,7 @@ func (round *round1) Start() *tss.Error { round.temp.ssidNonce = new(big.Int).SetUint64(0) } round.temp.ssid = round.getSSID() - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) // generate the dlnproofs for keygen h1i, h2i, alpha, beta, p, q, NTildei := diff --git a/ecdsa/keygen/round_2.go b/ecdsa/keygen/round_2.go index d8ebf5fee..ab13831d2 100644 --- a/ecdsa/keygen/round_2.go +++ b/ecdsa/keygen/round_2.go @@ -9,7 +9,6 @@ package keygen import ( "encoding/hex" "errors" - "math/big" "sync" "github.com/bnb-chain/tss-lib/common" @@ -72,7 +71,7 @@ func (round *round2) Start() *tss.Error { wg.Add(4) _j := j _msg := msg - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) verifier.VerifyDLNProof1(r1msg, H1j, H2j, NTildej, func(isValid bool) { if !isValid { @@ -130,7 +129,7 @@ func (round *round2) Start() *tss.Error { // 5. p2p send share ij to Pj shares := round.temp.shares - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) for j, Pj := range round.Parties().IDs() { // do not send to this Pj, but store for round 3 if j == i { diff --git a/ecdsa/keygen/round_3.go b/ecdsa/keygen/round_3.go index 79cc543c5..0134aeee8 100644 --- a/ecdsa/keygen/round_3.go +++ b/ecdsa/keygen/round_3.go @@ -65,7 +65,7 @@ func (round *round3) Start() *tss.Error { if j == PIdx { continue } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // 6-8. go func(j int, ch chan<- vssOut) { // 4-9. diff --git a/ecdsa/resharing/round_2_new_step_1.go b/ecdsa/resharing/round_2_new_step_1.go index cdb62eb5f..7666c2341 100644 --- a/ecdsa/resharing/round_2_new_step_1.go +++ b/ecdsa/resharing/round_2_new_step_1.go @@ -79,7 +79,7 @@ func (round *round2) Start() *tss.Error { dlnProof1 := dlnproof.NewDLNProof(h1i, h2i, alpha, p, q, NTildei, round.temp.ssid) dlnProof2 := dlnproof.NewDLNProof(h2i, h1i, beta, p, q, NTildei, round.temp.ssid) - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) modProof := preParams.PaillierSK.ModProof(contextI) // NTildei = (2p+1) * (2q+1) diff --git a/ecdsa/resharing/round_4_new_step_2.go b/ecdsa/resharing/round_4_new_step_2.go index 550b88ac3..56d118c09 100644 --- a/ecdsa/resharing/round_4_new_step_2.go +++ b/ecdsa/resharing/round_4_new_step_2.go @@ -83,7 +83,7 @@ func (round *round4) Start() *tss.Error { }(j, msg, r2msg1) _j := j _msg := msg - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) verifier.VerifyDLNProof1(r2msg1, H1j, H2j, NTildej, func(isValid bool) { if !isValid { dlnProof1FailCulprits[_j] = _msg.GetFrom() @@ -218,7 +218,7 @@ func (round *round4) Start() *tss.Error { return round.WrapError(errors2.Wrapf(err, "newBigXj.Add(Vc[c].ScalarMult(z))"), paiProofCulprits...) } - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) for j, Pj := range round.NewParties().IDs() { if common.Eq(Pi.KeyInt(), Pj.KeyInt()) { diff --git a/ecdsa/resharing/round_5_new_step_3.go b/ecdsa/resharing/round_5_new_step_3.go index 4225f9829..394691de1 100644 --- a/ecdsa/resharing/round_5_new_step_3.go +++ b/ecdsa/resharing/round_5_new_step_3.go @@ -8,7 +8,6 @@ package resharing import ( "errors" - "math/big" "github.com/hashicorp/go-multierror" @@ -50,7 +49,7 @@ func (round *round5) Start() *tss.Error { if common.Eq(Pi.KeyInt(), Pj.KeyInt()) { continue } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) go func(j int, ch chan<- proofOut) { r4msg1 := round.temp.dgRound4Message1s[j].Content().(*DGRound4Message1) diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index ea029e1fd..09c0669e5 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -48,7 +48,7 @@ func (round *round1) Start() *tss.Error { if nonce := round.Params().SessionNonce(); nonce != nil { round.temp.ssidNonce = new(big.Int).Set(nonce) } else { - round.temp.ssidNonce = new(big.Int).Set(round.temp.m) + round.temp.ssidNonce = round.messageSessionNonce() } ssid, err := round.getSSID() if err != nil { @@ -73,7 +73,7 @@ func (round *round1) Start() *tss.Error { if j == i { continue } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) cA, pi, err := mta.AliceInit(round.Params().EC(), round.key.PaillierPKs[i], k, round.key.NTildej[j], round.key.H1j[j], round.key.H2j[j], contextJ) if err != nil { return round.WrapError(fmt.Errorf("failed to init mta: %v", err)) diff --git a/ecdsa/signing/round_2.go b/ecdsa/signing/round_2.go index 7d6a07a54..4433ef125 100644 --- a/ecdsa/signing/round_2.go +++ b/ecdsa/signing/round_2.go @@ -8,7 +8,6 @@ package signing import ( "errors" - "math/big" "sync" errorspkg "github.com/pkg/errors" @@ -32,7 +31,7 @@ func (round *round2) Start() *tss.Error { errChs := make(chan *tss.Error, (len(round.Parties().IDs())-1)*2) wg := sync.WaitGroup{} wg.Add((len(round.Parties().IDs()) - 1) * 2) - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) for j, Pj := range round.Parties().IDs() { if j == i { continue diff --git a/ecdsa/signing/round_3.go b/ecdsa/signing/round_3.go index 2d941110b..92dfc8648 100644 --- a/ecdsa/signing/round_3.go +++ b/ecdsa/signing/round_3.go @@ -38,7 +38,7 @@ func (round *round3) Start() *tss.Error { if j == i { continue } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // Alice_end go func(j int, Pj *tss.PartyID) { defer wg.Done() diff --git a/ecdsa/signing/round_4.go b/ecdsa/signing/round_4.go index 019d7a58a..8955a72eb 100644 --- a/ecdsa/signing/round_4.go +++ b/ecdsa/signing/round_4.go @@ -42,7 +42,7 @@ func (round *round4) Start() *tss.Error { // compute the multiplicative inverse thelta mod q thetaInverse = modN.ModInverse(thetaInverse) i := round.PartyID().Index - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) piGamma, err := schnorr.NewZKProofWithSession(contextI, round.temp.gamma, round.temp.pointGamma) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(gamma, bigGamma)")) diff --git a/ecdsa/signing/round_5.go b/ecdsa/signing/round_5.go index f3dec630d..963378b94 100644 --- a/ecdsa/signing/round_5.go +++ b/ecdsa/signing/round_5.go @@ -8,7 +8,6 @@ package signing import ( "errors" - "math/big" errors2 "github.com/pkg/errors" @@ -47,7 +46,7 @@ func (round *round5) Start() *tss.Error { if err != nil { return round.WrapError(errors.New("failed to unmarshal bigGamma proof"), Pj) } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) ok = proof.VerifyWithSession(contextJ, bigGammaJPoint) if !ok { return round.WrapError(errors.New("failed to prove bigGamma"), Pj) diff --git a/ecdsa/signing/round_6.go b/ecdsa/signing/round_6.go index db1f92c62..95da829a0 100644 --- a/ecdsa/signing/round_6.go +++ b/ecdsa/signing/round_6.go @@ -8,7 +8,6 @@ package signing import ( "errors" - "math/big" errors2 "github.com/pkg/errors" @@ -26,7 +25,7 @@ func (round *round6) Start() *tss.Error { round.resetOK() i := round.PartyID().Index - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) piAi, err := schnorr.NewZKProofWithSession(contextI, round.temp.roi, round.temp.bigAi) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(roi, bigAi)")) diff --git a/ecdsa/signing/round_7.go b/ecdsa/signing/round_7.go index 876da1ff8..fb2c86d30 100644 --- a/ecdsa/signing/round_7.go +++ b/ecdsa/signing/round_7.go @@ -51,7 +51,7 @@ func (round *round7) Start() *tss.Error { return round.WrapError(errors2.Wrapf(err, "NewECPoint(bigAj)"), Pj) } bigAjs[j] = bigAj - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, new(big.Int).SetUint64(uint64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) pijA, err := r6msg.UnmarshalZKProof(round.Params().EC()) if err != nil || !pijA.VerifyWithSession(contextJ, bigAj) { return round.WrapError(errors.New("schnorr verify for Aj failed"), Pj) diff --git a/ecdsa/signing/rounds.go b/ecdsa/signing/rounds.go index 418b95364..a0d1ad0ad 100644 --- a/ecdsa/signing/rounds.go +++ b/ecdsa/signing/rounds.go @@ -125,6 +125,19 @@ func (round *base) resetOK() { } } +func (round *base) messageBytes() []byte { + if round.temp.fullBytesLen == 0 { + return round.temp.m.Bytes() + } + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + return mBytes +} + +func (round *base) messageSessionNonce() *big.Int { + return new(big.Int).SetBytes(common.SHA512_256(round.messageBytes())) +} + func (round *base) getSSID() ([]byte, error) { ssidList := []*big.Int{ round.EC().Params().P, diff --git a/eddsa/keygen/round_2.go b/eddsa/keygen/round_2.go index ccdb0fdb1..e5abf12d3 100644 --- a/eddsa/keygen/round_2.go +++ b/eddsa/keygen/round_2.go @@ -8,7 +8,6 @@ package keygen import ( "errors" - "math/big" errors2 "github.com/pkg/errors" @@ -47,7 +46,7 @@ func (round *round2) Start() *tss.Error { } // 5. compute Schnorr prove - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) pii, err := schnorr.NewZKProofWithSession(contextI, round.temp.ui, round.temp.vs[0]) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(ui, vi0)")) diff --git a/eddsa/keygen/round_3.go b/eddsa/keygen/round_3.go index a0231cb57..f882b8569 100644 --- a/eddsa/keygen/round_3.go +++ b/eddsa/keygen/round_3.go @@ -65,7 +65,7 @@ func (round *round3) Start() *tss.Error { if j == PIdx { continue } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // 6-9. go func(j int, ch chan<- vssOut) { // 4-10. diff --git a/eddsa/signing/round_1.go b/eddsa/signing/round_1.go index dfdb1bb3e..056724141 100644 --- a/eddsa/signing/round_1.go +++ b/eddsa/signing/round_1.go @@ -36,7 +36,7 @@ func (round *round1) Start() *tss.Error { if nonce := round.Params().SessionNonce(); nonce != nil { round.temp.ssidNonce = new(big.Int).Set(nonce) } else { - round.temp.ssidNonce = new(big.Int).Set(round.temp.m) + round.temp.ssidNonce = round.messageSessionNonce() } ssid, err := round.getSSID() if err != nil { diff --git a/eddsa/signing/round_2.go b/eddsa/signing/round_2.go index 3078c5874..ea8c6bd11 100644 --- a/eddsa/signing/round_2.go +++ b/eddsa/signing/round_2.go @@ -8,7 +8,6 @@ package signing import ( "errors" - "math/big" errors2 "github.com/pkg/errors" @@ -34,7 +33,7 @@ func (round *round2) Start() *tss.Error { } // 2. compute Schnorr prove - contextI := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(i))) + contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) pir, err := schnorr.NewZKProofWithSession(contextI, round.temp.ri, round.temp.pointRi) if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(ri, pointRi)")) diff --git a/eddsa/signing/round_3.go b/eddsa/signing/round_3.go index c421a21cf..fd0d1b5c2 100644 --- a/eddsa/signing/round_3.go +++ b/eddsa/signing/round_3.go @@ -8,7 +8,6 @@ package signing import ( "crypto/sha512" - "math/big" "github.com/agl/ed25519/edwards25519" "github.com/pkg/errors" @@ -40,7 +39,7 @@ func (round *round3) Start() *tss.Error { continue } - contextJ := common.AppendBigIntToBytesSlice(round.temp.ssid, big.NewInt(int64(j))) + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) msg := round.temp.signRound2Messages[j] r2msg := msg.Content().(*SignRound2Message) cmtDeCmt := commitments.HashCommitDecommit{C: round.temp.cjs[j], D: r2msg.UnmarshalDeCommitment()} diff --git a/eddsa/signing/rounds.go b/eddsa/signing/rounds.go index e5b38adda..8fb4b9761 100644 --- a/eddsa/signing/rounds.go +++ b/eddsa/signing/rounds.go @@ -102,6 +102,19 @@ func (round *base) resetOK() { } } +func (round *base) messageBytes() []byte { + if round.temp.fullBytesLen == 0 { + return round.temp.m.Bytes() + } + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + return mBytes +} + +func (round *base) messageSessionNonce() *big.Int { + return new(big.Int).SetBytes(common.SHA512_256(round.messageBytes())) +} + func (round *base) getSSID() ([]byte, error) { ssidList := []*big.Int{round.Params().EC().Params().P, round.Params().EC().Params().N, round.Params().EC().Params().Gx, round.Params().EC().Params().Gy} ssidList = append(ssidList, round.Parties().IDs().Keys()...) From 78ca61decbdd32602c399d1dc288d7571e1f216f Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 18 May 2026 22:40:17 -0500 Subject: [PATCH 04/24] Add session nonce helper and docs --- BNB_HARDENING_INTEGRATION.md | 6 ++--- README.md | 10 ++++++++- ecdsa/keygen/local_party_test.go | 23 +++++++++++++++++++ tss/params.go | 9 ++++++++ tss/params_test.go | 38 ++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tss/params_test.go diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index f12e52835..bf5bd59b6 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -23,7 +23,7 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. -- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce` and ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes`, and wired ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. - Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID, and signing default SSID nonces are derived from full message bytes when `fullBytesLen` is provided. @@ -47,7 +47,7 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation was retained. No BNB no-proof escape hatches were introduced. - Session parameters were added as variadic arguments to preserve existing public call sites. This is API source-compatible for callers, but not wire-compatible for proof transcripts. -- Keygen and resharing SSIDs are locally derived and use `Parameters.SessionNonce()` when set. This avoids protobuf/module churn, but callers must provide a unique agreed nonce for keygen/resharing sessions that need cross-session replay resistance. +- Keygen and resharing SSIDs are locally derived and use `Parameters.SessionNonce()` when set. This avoids protobuf/module churn, but callers must provide a unique agreed nonce, for example via `SetSessionNonceBytes`, for keygen/resharing sessions that need distinct proof transcripts. - ECDSA resharing SSID binding was adapted without adding BNB's newer wire-level SSID message fields. - `common.RejectionSample` keeps BNB's function name for porting clarity, but this implementation is modular reduction rather than a looping rejection sampler. - Constant-time operations are not included and remain a residual follow-up. @@ -74,6 +74,6 @@ Added or updated focused tests cover: ## Residual Risks -- Applications must call `SetSessionNonce` for keygen/resharing if they need unique SSIDs across otherwise identical party sets. +- Applications must call `SetSessionNonce` or `SetSessionNonceBytes` for keygen/resharing if they need unique SSIDs across otherwise identical party sets. - The optional constant-time upstream work is not integrated. - Resharing SSID binding is adapted locally rather than wire-compatible with BNB's latest protocol messages. diff --git a/README.md b/README.md index 6342c2f89..4cf82ff47 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,15 @@ When you build a transport, it should offer a broadcast channel as well as point Within your transport, each message should be wrapped with a **session ID** that is unique to a single run of the keygen, signing or re-sharing rounds. This session ID should be agreed upon out-of-band and known only by the participating parties before the rounds begin. Upon receiving any message, your program should make sure that the received session ID matches the one that was agreed upon at the start. +The same session ID should be bound into the protocol parameters before constructing local parties: + +```go +params := tss.NewParameters(curve, ctx, thisParty, len(parties), threshold) +params.SetSessionNonceBytes([]byte(sessionID)) +``` + +All parties in the run must use the same non-empty session ID. Signing derives a compatibility nonce from the message if none is set, but applications that already maintain a transport session ID should still pass it through. Keygen and re-sharing preserve the historical zero default when unset, so callers should set the nonce explicitly for distinct protocol runs. + Additionally, there should be a mechanism in your transport to allow for "reliable broadcasts", meaning parties can broadcast a message to other parties such that it's guaranteed that each one receives the same message. There are several examples of algorithms online that do this by sharing and comparing hashes of received messages. Timeouts and errors should be handled by your application. The method `WaitingFor` may be called on a `Party` to get the set of other parties that it is still waiting for messages from. You may also get the set of culprit parties that caused an error from a `*tss.Error`. @@ -155,4 +164,3 @@ A full review of this library was carried out by Kudelski Security and their fin ## References \[1\] https://eprint.iacr.org/2019/114.pdf - diff --git a/ecdsa/keygen/local_party_test.go b/ecdsa/keygen/local_party_test.go index f5abdcc1a..3df0509b7 100644 --- a/ecdsa/keygen/local_party_test.go +++ b/ecdsa/keygen/local_party_test.go @@ -34,6 +34,29 @@ const ( testThreshold = TestThreshold ) +func TestSSIDIncludesSessionNonce(t *testing.T) { + pIDs := tss.GenerateTestPartyIDs(3) + + ssidA := testKeygenSSID(pIDs, []byte("session-a")) + ssidAAgain := testKeygenSSID(pIDs, []byte("session-a")) + ssidB := testKeygenSSID(pIDs, []byte("session-b")) + + assert.Equal(t, ssidA, ssidAAgain) + assert.NotEqual(t, ssidA, ssidB) +} + +func testKeygenSSID(pIDs tss.SortedPartyIDs, sessionID []byte) []byte { + params := tss.NewParameters(tss.S256(), tss.NewPeerContext(pIDs), pIDs[0], len(pIDs), 1) + params.SetSessionNonceBytes(sessionID) + + round := &base{ + Parameters: params, + temp: &localTempData{ssidNonce: params.SessionNonce()}, + number: 1, + } + return round.getSSID() +} + func setUp(level string) { if err := log.SetLogLevel("tss-lib", level); err != nil { panic(err) diff --git a/tss/params.go b/tss/params.go index 89764ec54..e7121a903 100644 --- a/tss/params.go +++ b/tss/params.go @@ -11,6 +11,8 @@ import ( "math/big" "runtime" "time" + + "github.com/bnb-chain/tss-lib/common" ) type ( @@ -105,6 +107,13 @@ func (params *Parameters) SetSessionNonce(nonce *big.Int) { params.sessionNonce = new(big.Int).Set(nonce) } +// SetSessionNonceBytes hashes an application-level session ID into the +// per-session nonce. All parties must call it with the same non-empty session ID +// before constructing local parties for a protocol run. +func (params *Parameters) SetSessionNonceBytes(sessionID []byte) { + params.SetSessionNonce(new(big.Int).SetBytes(common.SHA512_256(sessionID))) +} + // ----- // // Exported, used in `tss` client diff --git a/tss/params_test.go b/tss/params_test.go new file mode 100644 index 000000000..ceb9918f8 --- /dev/null +++ b/tss/params_test.go @@ -0,0 +1,38 @@ +// 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 tss + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bnb-chain/tss-lib/common" +) + +func TestSetSessionNonceCopiesInput(t *testing.T) { + pIDs := GenerateTestPartyIDs(1) + params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], 1, 0) + nonce := big.NewInt(42) + + params.SetSessionNonce(nonce) + nonce.SetInt64(7) + + assert.Equal(t, big.NewInt(42), params.SessionNonce()) +} + +func TestSetSessionNonceBytesHashesSessionID(t *testing.T) { + pIDs := GenerateTestPartyIDs(1) + params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], 1, 0) + sessionID := []byte("session-1") + + params.SetSessionNonceBytes(sessionID) + + expected := new(big.Int).SetBytes(common.SHA512_256(sessionID)) + assert.Equal(t, expected, params.SessionNonce()) +} From 93e3cb472603eef21256e4f5c7b46985fc8dd871 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 18 May 2026 22:55:35 -0500 Subject: [PATCH 05/24] Reject empty session nonce IDs --- tss/params.go | 6 +++++- tss/params_test.go | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tss/params.go b/tss/params.go index e7121a903..b4868332c 100644 --- a/tss/params.go +++ b/tss/params.go @@ -109,8 +109,12 @@ func (params *Parameters) SetSessionNonce(nonce *big.Int) { // SetSessionNonceBytes hashes an application-level session ID into the // per-session nonce. All parties must call it with the same non-empty session ID -// before constructing local parties for a protocol run. +// before constructing local parties for a protocol run. It panics if the +// session ID is empty. func (params *Parameters) SetSessionNonceBytes(sessionID []byte) { + if len(sessionID) == 0 { + panic("tss: session ID must be non-empty") + } params.SetSessionNonce(new(big.Int).SetBytes(common.SHA512_256(sessionID))) } diff --git a/tss/params_test.go b/tss/params_test.go index ceb9918f8..f3c20b2fe 100644 --- a/tss/params_test.go +++ b/tss/params_test.go @@ -36,3 +36,15 @@ func TestSetSessionNonceBytesHashesSessionID(t *testing.T) { expected := new(big.Int).SetBytes(common.SHA512_256(sessionID)) assert.Equal(t, expected, params.SessionNonce()) } + +func TestSetSessionNonceBytesRejectsEmptySessionID(t *testing.T) { + pIDs := GenerateTestPartyIDs(1) + params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], 1, 0) + + assert.Panics(t, func() { + params.SetSessionNonceBytes(nil) + }) + assert.Panics(t, func() { + params.SetSessionNonceBytes([]byte{}) + }) +} From a71b1cdb5406eb884fedc8ead2857996f455d662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Tue, 19 May 2026 13:16:35 +0000 Subject: [PATCH 06/24] Extend session-tagged ModChallenge to full N-bit challenges The session-tagged ModChallenge path reduced a single 256-bit SHA512_256i_TAGGED output modulo Paillier N (~2^2048), producing challenges in [0, 2^256) instead of [0, N). The session-tagged path shares a verifier with the legacy HashToN path; the truncated challenges gave it a strictly weaker distribution than the path it replaced. Introduce HashToNTagged in common/hash_utils.go (tagged-hash analogue of HashToN) and switch the session-tagged branch of ModChallenge to it. The non-session path is unchanged. The iteration chaining of y[:i] into each y_i derivation is preserved. The session-tagged ModProof form was introduced by this PR and has not been deployed; no in-the-wild verifier is broken by this change. Add TestModChallenge_SessionPath_NotTruncated pinning y_i.BitLen() > N.BitLen()/2, and TestModChallenge_SessionPath_ChainsPreviousChallenges pinning the sequential-challenge invariant. --- common/hash_utils.go | 27 +++++++++++++++++++ crypto/paillier/mod_proof.go | 15 ++++++++--- crypto/paillier/mod_proof_test.go | 45 +++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/common/hash_utils.go b/common/hash_utils.go index ea89acd8b..616a1ffd4 100644 --- a/common/hash_utils.go +++ b/common/hash_utils.go @@ -51,3 +51,30 @@ func HashToN(N *big.Int, in ...*big.Int) *big.Int { // thus it is safe to use Mod return LiterallyJustMod(N, dest) } + +// HashToNTagged is the tagged-hash analogue of HashToN. It produces a value in +// [0, N) by concatenating ((N.BitLen()/256) + 2) blocks of SHA512_256i_TAGGED +// — one per block-index counter — and reducing modulo N. The total entropy +// before reduction is at least N.BitLen() + 256 bits, so the modular reduction +// has the same bias budget as HashToN (≤ 2^-256). +// +// Use this for Fiat-Shamir challenges over large moduli (e.g. Paillier N ≈ 2^2048) +// when the derivation must be domain-separated by a session tag. Reducing a +// single 256-bit SHA512_256i_TAGGED output modulo N would emit challenges in +// [0, 2^256) instead of [0, N). +func HashToNTagged(tag []byte, N *big.Int, in ...*big.Int) *big.Int { + bitCnt := N.BitLen() + blockCnt := (bitCnt / 256) + 2 + + dest := big.NewInt(0) + tmp := make([]*big.Int, 1, 1+len(in)) + tmp = append(tmp, in...) + + for i := 0; i < blockCnt; i++ { + tmp[0] = big.NewInt(int64(i)) + dest.Lsh(dest, 256) + dest.Or(dest, SHA512_256i_TAGGED(tag, tmp...)) + } + + return LiterallyJustMod(N, dest) +} diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go index b50b6c21e..25a756394 100644 --- a/crypto/paillier/mod_proof.go +++ b/crypto/paillier/mod_proof.go @@ -124,7 +124,16 @@ func (pf ModProof) ModVerify(N *big.Int, session ...[]byte) (bool, error) { return true, nil } -// Standard Fiat-Shamir transform +// Standard Fiat-Shamir transform. +// +// The session-tagged path uses HashToNTagged to derive each y_i with at least +// N.BitLen() + 256 bits of entropy before reducing mod N. Reducing a single +// 256-bit SHA512_256i_TAGGED output mod ~2^2048 would emit challenges in +// [0, 2^256) instead of [0, N), giving the session-tagged path a strictly +// weaker challenge distribution than the legacy HashToN path it shares the +// verifier with. Each iteration also chains the previously-derived challenges +// (y[:i]) into the hash inputs, preserving the sequential-challenge property of +// the original session-tagged construction. func ModChallenge(N, w *big.Int, session ...[]byte) [PARAM_M]*big.Int { var y [PARAM_M]*big.Int @@ -133,8 +142,8 @@ func ModChallenge(N, w *big.Int, session ...[]byte) [PARAM_M]*big.Int { y[i] = common.HashToN(N, w, big.NewInt(int64(i))) continue } - ei := common.SHA512_256i_TAGGED(session[0], append([]*big.Int{w, N}, y[:i]...)...) - y[i] = common.RejectionSample(N, ei) + inputs := append([]*big.Int{w, N}, y[:i]...) + y[i] = common.HashToNTagged(session[0], N, inputs...) } return y diff --git a/crypto/paillier/mod_proof_test.go b/crypto/paillier/mod_proof_test.go index c0030d56e..bd71c601b 100644 --- a/crypto/paillier/mod_proof_test.go +++ b/crypto/paillier/mod_proof_test.go @@ -48,6 +48,51 @@ func TestModProofSessionBinding(t *testing.T) { assert.False(t, res, "session-bound proof must not verify without its session") } +// TestModChallenge_SessionPath_NotTruncated pins the invariant that the +// session-tagged ModChallenge path produces challenges with the full +// ~N.BitLen() of entropy, not the 256-bit truncated form that would result +// from a single SHA512_256i_TAGGED → Mod N reduction against a 2048-bit +// Paillier N. A regression to truncation here would weaken the +// session-tagged proof to a strictly smaller challenge space than the +// legacy HashToN path it shares the verifier with. +func TestModChallenge_SessionPath_NotTruncated(t *testing.T) { + modSetUp(t) + N := publicKey.N + w := big.NewInt(7) + session := []byte("mod-challenge-bitwidth-pin") + + y := ModChallenge(N, w, session) + + // For uniform y_i in [0, N) with N.BitLen() ≈ 2048, the probability that + // y_i.BitLen() ≤ 1024 is ≈ 2^-1024. A pass here means no truncation path + // is silently capping the challenge to 256 bits. + bound := N.BitLen() / 2 + for i, yi := range y { + assert.NotNil(t, yi, "y_%d must be non-nil", i) + assert.True(t, yi.Sign() >= 0, "y_%d must be non-negative", i) + assert.True(t, yi.Cmp(N) < 0, "y_%d must be < N", i) + assert.True(t, yi.BitLen() > bound, + "y_%d.BitLen()=%d must exceed %d (regression to 256-bit truncation)", i, yi.BitLen(), bound) + } +} + +// TestModChallenge_SessionPath_ChainsPreviousChallenges pins that each +// session-tagged y_i mixes y[:i] into its derivation, so a single-iteration +// collision cannot be replayed across all PARAM_M iterations. +func TestModChallenge_SessionPath_ChainsPreviousChallenges(t *testing.T) { + modSetUp(t) + N := publicKey.N + w := big.NewInt(7) + session := []byte("mod-challenge-chaining-pin") + + y := ModChallenge(N, w, session) + + for i := 1; i < PARAM_M; i++ { + assert.NotEqual(t, y[0].Cmp(y[i]), 0, + "y_0 and y_%d must differ — chaining of y[:i] in derivation broken", i) + } +} + func TestModProofVerifyFail(t *testing.T) { modSetUp(t) proof := privateKey.ModProof() From 92320a899d06de76ab0d692719c3a250cb5201da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Tue, 19 May 2026 13:23:22 +0000 Subject: [PATCH 07/24] Validate signing constructor fullBytesLen at call site The signing constructors accepted a variadic fullBytesLen ...int and stored fullBytesLen[0] verbatim with no bounds check. A caller passing a negative value would later panic in make([]byte, fullBytesLen), and a caller passing a value smaller than the message's byte width would panic in (*big.Int).FillBytes with "insufficient length". Both panics fired inside a round-1 goroutine, crossing goroutine boundaries and bypassing the tss.Error reporting that the protocol state machine expects. Validate at constructor entry, before any state allocation: reject fullBytesLen < 0 and reject fullBytesLen > 0 with a message whose byte width exceeds it. Panic synchronously with a clear, parameter- named message at the caller's call site instead. The variadic shape is preserved (source-compat), so existing callers that omit fullBytesLen are unaffected. Mirror the fix in ECDSA NewLocalPartyWithKDD and EdDSA NewLocalParty. Add regression tests pinning both negative cases for both protocols. The valid positive path is covered by the existing E2E tests (which omit fullBytesLen); a future cherry-pick of upstream's TestE2EConcurrentWithLeadingZeroInMSG will exercise a valid fullBytesLen end-to-end. --- ecdsa/signing/local_party.go | 19 +++++++++++- ecdsa/signing/local_party_test.go | 48 +++++++++++++++++++++++++++++++ eddsa/signing/local_party.go | 16 +++++++++++ eddsa/signing/local_party_test.go | 45 +++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/ecdsa/signing/local_party.go b/ecdsa/signing/local_party.go index 5dbee793d..14c9b5a59 100644 --- a/ecdsa/signing/local_party.go +++ b/ecdsa/signing/local_party.go @@ -109,7 +109,14 @@ func NewLocalParty( return NewLocalPartyWithKDD(msg, params, key, nil, out, end, fullBytesLen...) } -// NewLocalPartyWithKDD returns a party with key derivation delta for HD support +// NewLocalPartyWithKDD returns a party with key derivation delta for HD support. +// +// Optional fullBytesLen, if provided, fixes the byte width used to encode the +// message in round-1 SSID derivation (preserving leading zero bytes). The +// value must be non-negative and, when non-zero, must be at least +// ceil(msg.BitLen()/8); violating either constraint is a caller bug and the +// constructor panics at the call site rather than later inside a protocol +// goroutine. func NewLocalPartyWithKDD( msg *big.Int, params *tss.Parameters, @@ -119,6 +126,16 @@ func NewLocalPartyWithKDD( end chan<- common.SignatureData, fullBytesLen ...int, ) tss.Party { + if len(fullBytesLen) > 0 { + if fullBytesLen[0] < 0 { + panic(fmt.Errorf("NewLocalPartyWithKDD: fullBytesLen must be non-negative, got %d", fullBytesLen[0])) + } + if fullBytesLen[0] > 0 && msg != nil && msg.BitLen() > 8*fullBytesLen[0] { + panic(fmt.Errorf("NewLocalPartyWithKDD: fullBytesLen=%d is too small for a %d-bit message (need at least %d bytes)", + fullBytesLen[0], msg.BitLen(), (msg.BitLen()+7)/8)) + } + } + partyCount := len(params.Parties().IDs()) p := &LocalParty{ BaseParty: new(tss.BaseParty), diff --git a/ecdsa/signing/local_party_test.go b/ecdsa/signing/local_party_test.go index a923f7a06..68fe3303b 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -12,6 +12,7 @@ import ( "fmt" "math/big" "runtime" + "strings" "sync/atomic" "testing" @@ -245,6 +246,53 @@ signing: } } +// TestNewLocalPartyWithKDD_FullBytesLen_Negative pins constructor-side +// validation for fullBytesLen. Previously, a negative fullBytesLen passed +// through to the round-1 code path, where `make([]byte, fullBytesLen)` +// panicked inside a protocol goroutine, bypassing tss.Error reporting and +// crossing goroutine boundaries. The constructor now panics synchronously +// at the caller's call site with a clear message. +func TestNewLocalPartyWithKDD_FullBytesLen_Negative(t *testing.T) { + msg := big.NewInt(1) + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for negative fullBytesLen") + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "fullBytesLen must be non-negative") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalPartyWithKDD(msg, nil, keygen.LocalPartySaveData{}, nil, nil, nil, -1) +} + +// TestNewLocalPartyWithKDD_FullBytesLen_TooSmall pins that a fullBytesLen +// smaller than the message's byte width is rejected at the constructor +// rather than later inside (*big.Int).FillBytes (which would panic with +// "big.Int.FillBytes: insufficient length" inside a protocol goroutine). +func TestNewLocalPartyWithKDD_FullBytesLen_TooSmall(t *testing.T) { + // 16-bit msg needs at least 2 bytes; pass fullBytesLen=1 to trigger. + msg := big.NewInt(0xABCD) + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for fullBytesLen smaller than msg byte width") + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "fullBytesLen=1 is too small") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalPartyWithKDD(msg, nil, keygen.LocalPartySaveData{}, nil, nil, nil, 1) +} + func TestFillTo32BytesInPlace(t *testing.T) { s := big.NewInt(123456789) normalizedS := padToLengthBytesInPlace(s.Bytes(), 32) diff --git a/eddsa/signing/local_party.go b/eddsa/signing/local_party.go index 6bcd272d8..cac058273 100644 --- a/eddsa/signing/local_party.go +++ b/eddsa/signing/local_party.go @@ -66,6 +66,12 @@ type ( } ) +// NewLocalParty returns a signing party. Optional fullBytesLen, if provided, +// fixes the byte width used to encode the message in round-1 SSID derivation +// (preserving leading zero bytes). The value must be non-negative and, when +// non-zero, must be at least ceil(msg.BitLen()/8); violating either +// constraint is a caller bug and the constructor panics at the call site +// rather than later inside a protocol goroutine. func NewLocalParty( msg *big.Int, params *tss.Parameters, @@ -74,6 +80,16 @@ func NewLocalParty( end chan<- common.SignatureData, fullBytesLen ...int, ) tss.Party { + if len(fullBytesLen) > 0 { + if fullBytesLen[0] < 0 { + panic(fmt.Errorf("NewLocalParty: fullBytesLen must be non-negative, got %d", fullBytesLen[0])) + } + if fullBytesLen[0] > 0 && msg != nil && msg.BitLen() > 8*fullBytesLen[0] { + panic(fmt.Errorf("NewLocalParty: fullBytesLen=%d is too small for a %d-bit message (need at least %d bytes)", + fullBytesLen[0], msg.BitLen(), (msg.BitLen()+7)/8)) + } + } + partyCount := len(params.Parties().IDs()) p := &LocalParty{ BaseParty: new(tss.BaseParty), diff --git a/eddsa/signing/local_party_test.go b/eddsa/signing/local_party_test.go index a87b2c6be..a4137fc58 100644 --- a/eddsa/signing/local_party_test.go +++ b/eddsa/signing/local_party_test.go @@ -10,6 +10,7 @@ import ( "encoding/hex" "fmt" "math/big" + "strings" "sync/atomic" "testing" @@ -146,3 +147,47 @@ signing: } } } + +// TestNewLocalParty_FullBytesLen_Negative pins constructor-side validation +// for fullBytesLen. Previously, a negative fullBytesLen propagated to the +// round-1/round-3 code path where `make([]byte, fullBytesLen)` panicked +// inside a protocol goroutine, bypassing tss.Error reporting. The +// constructor now panics synchronously at the caller's call site. +func TestNewLocalParty_FullBytesLen_Negative(t *testing.T) { + msg := big.NewInt(1) + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for negative fullBytesLen") + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "fullBytesLen must be non-negative") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalParty(msg, nil, keygen.LocalPartySaveData{}, nil, nil, -1) +} + +// TestNewLocalParty_FullBytesLen_TooSmall pins that a fullBytesLen smaller +// than the message's byte width is rejected at the constructor rather than +// later inside (*big.Int).FillBytes (which would panic inside a goroutine). +func TestNewLocalParty_FullBytesLen_TooSmall(t *testing.T) { + msg := big.NewInt(0xABCD) // 16-bit, needs at least 2 bytes + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for fullBytesLen smaller than msg byte width") + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "fullBytesLen=1 is too small") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalParty(msg, nil, keygen.LocalPartySaveData{}, nil, nil, 1) +} From 21efbe2ce23550e419b45a96d45c7caebbe136e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Tue, 19 May 2026 13:30:01 +0000 Subject: [PATCH 08/24] Fail closed when signing without SessionNonce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 1 of both ECDSA and EdDSA signing fell back to SHA512_256(messageBytes) for the SSID nonce when the caller did not set one. Two concurrent ceremonies on the same canonical message therefore derived the same SSID, and after AppendUint64ToBytesSlice also the same per-party Fiat-Shamir context — enabling transcript splicing between the runs (CWE-294). Re-signing the same payload under the same committee is a common production pattern (e.g. tBTC-style sighash retries), so this silently broke the very property the BNB SSID hardening was meant to add. Return tss.Error from round 1 when SessionNonce is nil, naming the constraint and the setter the caller must call. Remove the now-dead messageSessionNonce + messageBytes helpers so the fallback can't be silently reintroduced. Keygen/resharing fall-back to zero is unchanged (tracked separately). Update the two ECDSA E2E tests and the EdDSA E2E test to call params.SetSessionNonce before constructing parties. Add TestSigning_Start_RequiresSessionNonce for each protocol pinning the fail-closed behavior. Document the new requirement on tss.Parameters.SetSessionNonce and flip the corresponding line in BNB_HARDENING_INTEGRATION.md from "signing defaults to message hash as nonce" to the new contract. --- BNB_HARDENING_INTEGRATION.md | 4 ++-- ecdsa/signing/local_party_test.go | 32 +++++++++++++++++++++++++++++++ ecdsa/signing/round_1.go | 13 +++++++++---- ecdsa/signing/rounds.go | 13 ------------- eddsa/signing/local_party_test.go | 30 +++++++++++++++++++++++++++++ eddsa/signing/round_1.go | 13 +++++++++---- eddsa/signing/rounds.go | 13 ------------- tss/params.go | 13 ++++++++++++- 8 files changed, 94 insertions(+), 37 deletions(-) diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index bf5bd59b6..87a8c5903 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -23,10 +23,10 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. -- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes`, and wired ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing defaults to message hash as nonce; keygen/resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (rejects empty session IDs), and wired ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing now requires `SetSessionNonce` and fails closed if it is not set — the previous SHA512_256(messageBytes) fallback made two concurrent ceremonies on the same canonical message collide on SSID and proof contexts, enabling Fiat-Shamir transcript splicing. Keygen and resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. -- Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID, and signing default SSID nonces are derived from full message bytes when `fullBytesLen` is provided. +- Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID. The earlier signing default that derived an SSID nonce from full message bytes has been removed in favour of the fail-closed requirement above; the helpers that produced it (`messageBytes`, `messageSessionNonce`) are gone with it. ## Already Covered Or Superseded diff --git a/ecdsa/signing/local_party_test.go b/ecdsa/signing/local_party_test.go index 68fe3303b..61796f93e 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -63,8 +63,10 @@ func TestE2EConcurrent(t *testing.T) { msgInt := new(big.Int).SetBytes(msgData) // init the parties + ceremonyNonce := big.NewInt(1) for i := 0; i < len(signPIDs); i++ { params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[i], len(signPIDs), threshold) + params.SetSessionNonce(ceremonyNonce) P := NewLocalParty(msgInt, params, keys[i], outCh, endCh, len(msgData)).(*LocalParty) parties = append(parties, P) @@ -173,8 +175,10 @@ func TestE2EWithHDKeyDerivation(t *testing.T) { updater := test.SharedPartyUpdater // init the parties + ceremonyNonce := big.NewInt(2) for i := 0; i < len(signPIDs); i++ { params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[i], len(signPIDs), threshold) + params.SetSessionNonce(ceremonyNonce) P := NewLocalPartyWithKDD(big.NewInt(42), params, keys[i], keyDerivationDelta, outCh, endCh).(*LocalParty) parties = append(parties, P) @@ -246,6 +250,34 @@ signing: } } +// TestSigning_Start_RequiresSessionNonce pins that signing fails closed +// when no SessionNonce is set. Previously the round-1 code fell back to +// SHA512_256(messageBytes), making two concurrent ceremonies on the same +// canonical message reuse the same SSID and enabling Fiat-Shamir +// transcript splicing across runs. The fix removes the fallback and +// requires the caller to provide a per-ceremony nonce. +func TestSigning_Start_RequiresSessionNonce(t *testing.T) { + setUp("info") + keys, signPIDs, err := keygen.LoadKeygenTestFixturesRandomSet(testThreshold+1, testParticipants) + assert.NoError(t, err, "should load keygen fixtures") + + p2pCtx := tss.NewPeerContext(signPIDs) + outCh := make(chan tss.Message, len(signPIDs)) + endCh := make(chan common.SignatureData, len(signPIDs)) + + params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[0], len(signPIDs), testThreshold) + // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. + + P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh).(*LocalParty) + tssErr := P.Start() + if tssErr == nil { + t.Fatal("Start must return an error without SessionNonce") + } + if !strings.Contains(tssErr.Error(), "SetSessionNonce") { + t.Fatalf("error must reference SetSessionNonce, got: %v", tssErr) + } +} + // TestNewLocalPartyWithKDD_FullBytesLen_Negative pins constructor-side // validation for fullBytesLen. Previously, a negative fullBytesLen passed // through to the round-1 code path, where `make([]byte, fullBytesLen)` diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index 09c0669e5..731b032ea 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -45,11 +45,16 @@ func (round *round1) Start() *tss.Error { round.number = 1 round.started = true round.resetOK() - if nonce := round.Params().SessionNonce(); nonce != nil { - round.temp.ssidNonce = new(big.Int).Set(nonce) - } else { - round.temp.ssidNonce = round.messageSessionNonce() + // Signing fails closed if no SessionNonce is set. The previous fallback + // (SHA512_256 of the message) made two concurrent ceremonies on the same + // canonical message reuse the same SSID, which would have enabled + // Fiat-Shamir transcript splicing across the runs. The caller must now + // supply a per-ceremony nonce via tss.Parameters.SetSessionNonce. + nonce := round.Params().SessionNonce() + if nonce == nil { + return round.WrapError(errors.New("signing requires tss.Parameters.SetSessionNonce() before Start")) } + round.temp.ssidNonce = new(big.Int).Set(nonce) ssid, err := round.getSSID() if err != nil { return round.WrapError(err) diff --git a/ecdsa/signing/rounds.go b/ecdsa/signing/rounds.go index a0d1ad0ad..418b95364 100644 --- a/ecdsa/signing/rounds.go +++ b/ecdsa/signing/rounds.go @@ -125,19 +125,6 @@ func (round *base) resetOK() { } } -func (round *base) messageBytes() []byte { - if round.temp.fullBytesLen == 0 { - return round.temp.m.Bytes() - } - mBytes := make([]byte, round.temp.fullBytesLen) - round.temp.m.FillBytes(mBytes) - return mBytes -} - -func (round *base) messageSessionNonce() *big.Int { - return new(big.Int).SetBytes(common.SHA512_256(round.messageBytes())) -} - func (round *base) getSSID() ([]byte, error) { ssidList := []*big.Int{ round.EC().Params().P, diff --git a/eddsa/signing/local_party_test.go b/eddsa/signing/local_party_test.go index a4137fc58..2c91ee58e 100644 --- a/eddsa/signing/local_party_test.go +++ b/eddsa/signing/local_party_test.go @@ -65,8 +65,10 @@ func TestE2EConcurrent(t *testing.T) { assert.NoError(t, err) msg := new(big.Int).SetBytes(msgData) // init the parties + ceremonyNonce := big.NewInt(1) for i := 0; i < len(signPIDs); i++ { params := tss.NewParameters(tss.Edwards(), p2pCtx, signPIDs[i], len(signPIDs), threshold) + params.SetSessionNonce(ceremonyNonce) P := NewLocalParty(msg, params, keys[i], outCh, endCh, len(msgData)).(*LocalParty) parties = append(parties, P) @@ -148,6 +150,34 @@ signing: } } +// TestSigning_Start_RequiresSessionNonce pins that signing fails closed +// when no SessionNonce is set. Previously the round-1 code fell back to +// SHA512_256(messageBytes), making two concurrent ceremonies on the same +// canonical message reuse the same SSID and enabling Fiat-Shamir +// transcript splicing across runs. The fix removes the fallback and +// requires the caller to provide a per-ceremony nonce. +func TestSigning_Start_RequiresSessionNonce(t *testing.T) { + setUp("info") + keys, signPIDs, err := keygen.LoadKeygenTestFixturesRandomSet(testThreshold+1, testParticipants) + assert.NoError(t, err, "should load keygen fixtures") + + p2pCtx := tss.NewPeerContext(signPIDs) + outCh := make(chan tss.Message, len(signPIDs)) + endCh := make(chan common.SignatureData, len(signPIDs)) + + params := tss.NewParameters(tss.Edwards(), p2pCtx, signPIDs[0], len(signPIDs), testThreshold) + // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. + + P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh).(*LocalParty) + tssErr := P.Start() + if tssErr == nil { + t.Fatal("Start must return an error without SessionNonce") + } + if !strings.Contains(tssErr.Error(), "SetSessionNonce") { + t.Fatalf("error must reference SetSessionNonce, got: %v", tssErr) + } +} + // TestNewLocalParty_FullBytesLen_Negative pins constructor-side validation // for fullBytesLen. Previously, a negative fullBytesLen propagated to the // round-1/round-3 code path where `make([]byte, fullBytesLen)` panicked diff --git a/eddsa/signing/round_1.go b/eddsa/signing/round_1.go index 056724141..75d5c3dc7 100644 --- a/eddsa/signing/round_1.go +++ b/eddsa/signing/round_1.go @@ -33,11 +33,16 @@ func (round *round1) Start() *tss.Error { round.started = true round.resetOK() - if nonce := round.Params().SessionNonce(); nonce != nil { - round.temp.ssidNonce = new(big.Int).Set(nonce) - } else { - round.temp.ssidNonce = round.messageSessionNonce() + // Signing fails closed if no SessionNonce is set. The previous fallback + // (SHA512_256 of the message) made two concurrent ceremonies on the same + // canonical message reuse the same SSID, which would have enabled + // Fiat-Shamir transcript splicing across the runs. The caller must now + // supply a per-ceremony nonce via tss.Parameters.SetSessionNonce. + nonce := round.Params().SessionNonce() + if nonce == nil { + return round.WrapError(errors.New("signing requires tss.Parameters.SetSessionNonce() before Start")) } + round.temp.ssidNonce = new(big.Int).Set(nonce) ssid, err := round.getSSID() if err != nil { return round.WrapError(err) diff --git a/eddsa/signing/rounds.go b/eddsa/signing/rounds.go index 8fb4b9761..e5b38adda 100644 --- a/eddsa/signing/rounds.go +++ b/eddsa/signing/rounds.go @@ -102,19 +102,6 @@ func (round *base) resetOK() { } } -func (round *base) messageBytes() []byte { - if round.temp.fullBytesLen == 0 { - return round.temp.m.Bytes() - } - mBytes := make([]byte, round.temp.fullBytesLen) - round.temp.m.FillBytes(mBytes) - return mBytes -} - -func (round *base) messageSessionNonce() *big.Int { - return new(big.Int).SetBytes(common.SHA512_256(round.messageBytes())) -} - func (round *base) getSSID() ([]byte, error) { ssidList := []*big.Int{round.Params().EC().Params().P, round.Params().EC().Params().N, round.Params().EC().Params().Gx, round.Params().EC().Params().Gy} ssidList = append(ssidList, round.Parties().IDs().Keys()...) diff --git a/tss/params.go b/tss/params.go index b4868332c..c2559e44c 100644 --- a/tss/params.go +++ b/tss/params.go @@ -98,7 +98,18 @@ func (params *Parameters) SessionNonce() *big.Int { } // SetSessionNonce sets a per-session nonce that all parties in a protocol run -// must agree on. +// must agree on. It must be called before Start. +// +// Signing requires this: round 1 fails closed if no nonce is set, because the +// previous SHA512_256(messageBytes) fallback caused two concurrent ceremonies +// on the same canonical message to collide on SSID and proof contexts. For +// signing, the caller must supply a per-ceremony unique nonce; reusing the +// same nonce across distinct ceremonies on the same payload reintroduces +// transcript-splicing risk. +// +// Keygen and resharing still tolerate an unset nonce (falling back to zero), +// but applications that need unique SSIDs across otherwise identical +// keygen/resharing party sets should also set it explicitly. func (params *Parameters) SetSessionNonce(nonce *big.Int) { if nonce == nil { params.sessionNonce = nil From 762507e5a729ffd918ba966d33fa60d8d47e2857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Tue, 19 May 2026 14:01:45 +0000 Subject: [PATCH 09/24] Restore wire-format SSID broadcast in ECDSA resharing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream BNB commit fc38979 puts SSID on the wire in DGRound1Message and runs bytes.Equal(SSID, SSIDj) across all old-committee parties in the new committee's round 2. The PR strip-and-derive-locally lost this: each new-committee party derives SSID locally, so a corrupted old-committee party that broadcasts inconsistent SSIDs to different new-committee members (or whose SessionNonce diverges from the rest of the old committee) only surfaces as a downstream proof verification failure several rounds later, with no specific abort message naming the cause. Add `bytes ssid = 4` to DGRound1Message. Derive SSID in round 1 for both committees (using public inputs: party IDs, curve, round number, ssidNonce). Old-committee parties broadcast their SSID in the existing DGRound1Message; new-committee parties cross-check each received SSID against their locally-derived SSID before consuming any other field of the message, returning a tss.Error that names the sender if a mismatch is detected. SSID derivation is removed from round 2 since temp.ssid is set in round 1. ValidateBasic on DGRound1Message now requires the SSID field. EdDSA resharing has no SSID derivation today, so no changes are needed there. ecdsa-resharing.pb.go regenerated with protoc-gen-go v1.30.0 (the version pinned in the generated header); the only meaningful drift beyond the new field is the protoc binary version comment (v3.21.12 → v4.25.1) — both protoc versions emit identical wire format. Update ecdsa/resharing/local_party_test.go and eddsa/resharing/ local_party_test.go to call params.SetSessionNonce for the signing phase (made required by the preceding fail-closed fix). Add TestDGRound1Message_ValidateBasic_RequiresSsid pinning the message-format invariant. The E2E ceremony (TestE2EConcurrent) covers the broadcast → cross-verify round-trip end-to-end; a Byzantine-simulator regression test for the mismatch-detection branch is tracked under testing-plan Phase 5. --- ecdsa/resharing/ecdsa-resharing.pb.go | 181 ++++++++++++++------------ ecdsa/resharing/local_party_test.go | 2 + ecdsa/resharing/messages.go | 5 +- ecdsa/resharing/messages_test.go | 50 +++++++ ecdsa/resharing/round_1_old_step_1.go | 32 ++++- ecdsa/resharing/round_2_new_step_1.go | 10 +- eddsa/resharing/local_party_test.go | 2 + protob/ecdsa-resharing.proto | 4 + 8 files changed, 193 insertions(+), 93 deletions(-) create mode 100644 ecdsa/resharing/messages_test.go diff --git a/ecdsa/resharing/ecdsa-resharing.pb.go b/ecdsa/resharing/ecdsa-resharing.pb.go index aee10b95b..54d6858e6 100644 --- a/ecdsa/resharing/ecdsa-resharing.pb.go +++ b/ecdsa/resharing/ecdsa-resharing.pb.go @@ -7,7 +7,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.30.0 -// protoc v3.21.12 +// protoc v4.25.1 // source: protob/ecdsa-resharing.proto package resharing @@ -27,6 +27,9 @@ const ( ) // The Round 1 data is broadcast to peers of the New Committee in this message. +// Each old-committee party broadcasts the locally-derived SSID so the new +// committee can detect a corrupted old-committee party broadcasting an +// inconsistent SSID across new-committee members. type DGRound1Message struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -35,6 +38,7 @@ type DGRound1Message struct { EcdsaPubX []byte `protobuf:"bytes,1,opt,name=ecdsa_pub_x,json=ecdsaPubX,proto3" json:"ecdsa_pub_x,omitempty"` EcdsaPubY []byte `protobuf:"bytes,2,opt,name=ecdsa_pub_y,json=ecdsaPubY,proto3" json:"ecdsa_pub_y,omitempty"` VCommitment []byte `protobuf:"bytes,3,opt,name=v_commitment,json=vCommitment,proto3" json:"v_commitment,omitempty"` + Ssid []byte `protobuf:"bytes,4,opt,name=ssid,proto3" json:"ssid,omitempty"` } func (x *DGRound1Message) Reset() { @@ -90,6 +94,13 @@ func (x *DGRound1Message) GetVCommitment() []byte { return nil } +func (x *DGRound1Message) GetSsid() []byte { + if x != nil { + return x.Ssid + } + return nil +} + // The Round 2 data is broadcast to other peers of the New Committee in this message. type DGRound2Message1 struct { state protoimpl.MessageState @@ -738,92 +749,94 @@ var file_protob_ecdsa_resharing_proto_rawDesc = []byte{ 0x0a, 0x1c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x2f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, - 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x74, - 0x0a, 0x0f, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x31, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x78, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x64, 0x73, 0x61, 0x50, 0x75, 0x62, - 0x58, 0x12, 0x1e, 0x0a, 0x0b, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x64, 0x73, 0x61, 0x50, 0x75, 0x62, - 0x59, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x76, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x8c, 0x05, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, - 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x69, - 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, - 0x61, 0x69, 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x4e, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x69, 0x6c, - 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, - 0x52, 0x0d, 0x70, 0x61, 0x69, 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, - 0x17, 0x0a, 0x07, 0x6e, 0x5f, 0x74, 0x69, 0x6c, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x06, 0x6e, 0x54, 0x69, 0x6c, 0x64, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x68, 0x31, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x68, 0x31, 0x12, 0x0e, 0x0a, 0x02, 0x68, 0x32, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x68, 0x32, 0x12, 0x58, 0x0a, 0x0a, 0x64, 0x6c, 0x6e, 0x70, - 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x31, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x62, - 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, - 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, - 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x44, - 0x4c, 0x4e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x09, 0x64, 0x6c, 0x6e, 0x70, 0x72, 0x6f, 0x6f, - 0x66, 0x31, 0x12, 0x58, 0x0a, 0x0a, 0x64, 0x6c, 0x6e, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x32, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, - 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, - 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x44, 0x4c, 0x4e, 0x50, 0x72, 0x6f, 0x6f, - 0x66, 0x52, 0x09, 0x64, 0x6c, 0x6e, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x32, 0x12, 0x55, 0x0a, 0x08, - 0x6d, 0x6f, 0x64, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, - 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, - 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, - 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, - 0x2e, 0x4d, 0x6f, 0x64, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x08, 0x6d, 0x6f, 0x64, 0x70, 0x72, - 0x6f, 0x6f, 0x66, 0x12, 0x60, 0x0a, 0x0e, 0x6d, 0x6f, 0x64, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x5f, - 0x74, 0x69, 0x6c, 0x64, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x62, 0x69, + 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x88, + 0x01, 0x0a, 0x0f, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x31, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x70, 0x75, 0x62, 0x5f, + 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x64, 0x73, 0x61, 0x50, 0x75, + 0x62, 0x58, 0x12, 0x1e, 0x0a, 0x0b, 0x65, 0x63, 0x64, 0x73, 0x61, 0x5f, 0x70, 0x75, 0x62, 0x5f, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x63, 0x64, 0x73, 0x61, 0x50, 0x75, + 0x62, 0x59, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x76, 0x43, 0x6f, 0x6d, 0x6d, 0x69, + 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x73, 0x69, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x73, 0x69, 0x64, 0x22, 0x8c, 0x05, 0x0a, 0x10, 0x44, 0x47, + 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x61, 0x69, 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x09, 0x70, 0x61, 0x69, 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x4e, 0x12, 0x25, 0x0a, + 0x0e, 0x70, 0x61, 0x69, 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x70, 0x61, 0x69, 0x6c, 0x6c, 0x69, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x5f, 0x74, 0x69, 0x6c, 0x64, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6e, 0x54, 0x69, 0x6c, 0x64, 0x65, 0x12, 0x0e, 0x0a, + 0x02, 0x68, 0x31, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x68, 0x31, 0x12, 0x0e, 0x0a, + 0x02, 0x68, 0x32, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x68, 0x32, 0x12, 0x58, 0x0a, + 0x0a, 0x64, 0x6c, 0x6e, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x31, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x39, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, + 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, + 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x31, 0x2e, 0x44, 0x4c, 0x4e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x09, 0x64, 0x6c, + 0x6e, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x31, 0x12, 0x58, 0x0a, 0x0a, 0x64, 0x6c, 0x6e, 0x70, 0x72, + 0x6f, 0x6f, 0x66, 0x5f, 0x32, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, - 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x4d, 0x6f, - 0x64, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0d, 0x6d, 0x6f, 0x64, 0x70, 0x72, 0x6f, 0x6f, 0x66, - 0x54, 0x69, 0x6c, 0x64, 0x65, 0x1a, 0x2e, 0x0a, 0x08, 0x44, 0x4c, 0x4e, 0x50, 0x72, 0x6f, 0x6f, - 0x66, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, - 0x52, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x12, 0x0c, 0x0a, 0x01, 0x74, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0c, 0x52, 0x01, 0x74, 0x1a, 0x50, 0x0a, 0x08, 0x4d, 0x6f, 0x64, 0x50, 0x72, 0x6f, 0x6f, - 0x66, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x77, 0x12, - 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, - 0x01, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x08, 0x52, 0x01, 0x61, 0x12, 0x0c, 0x0a, 0x01, 0x62, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x08, 0x52, 0x01, 0x62, 0x12, 0x0c, 0x0a, 0x01, 0x7a, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0c, 0x52, 0x01, 0x7a, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, - 0x07, 0x10, 0x08, 0x22, 0x12, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x22, 0x28, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, - 0x6e, 0x64, 0x33, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x68, 0x61, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x68, 0x61, 0x72, - 0x65, 0x22, 0x39, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x33, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x32, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x5f, 0x64, 0x65, 0x63, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0d, 0x76, - 0x44, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x8b, 0x03, 0x0a, - 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x34, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x31, 0x12, 0x58, 0x0a, 0x08, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, - 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, - 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x34, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x6f, - 0x66, 0x52, 0x08, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x63, 0x0a, 0x0e, 0x66, - 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x74, 0x69, 0x6c, 0x64, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, + 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x44, 0x4c, + 0x4e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x09, 0x64, 0x6c, 0x6e, 0x70, 0x72, 0x6f, 0x6f, 0x66, + 0x32, 0x12, 0x55, 0x0a, 0x08, 0x6d, 0x6f, 0x64, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, - 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x34, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x6f, - 0x66, 0x52, 0x0d, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x54, 0x69, 0x6c, 0x64, 0x65, - 0x1a, 0xb7, 0x01, 0x0a, 0x0b, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, - 0x12, 0x0c, 0x0a, 0x01, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x70, 0x12, 0x0c, - 0x0a, 0x01, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x71, 0x12, 0x0c, 0x0a, 0x01, - 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x61, 0x12, 0x0c, 0x0a, 0x01, 0x62, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x62, 0x12, 0x0c, 0x0a, 0x01, 0x74, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x01, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x67, 0x6d, 0x61, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x69, 0x67, 0x6d, 0x61, 0x12, 0x0e, 0x0a, 0x02, - 0x7a, 0x31, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x7a, 0x31, 0x12, 0x0e, 0x0a, 0x02, - 0x7a, 0x32, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x7a, 0x32, 0x12, 0x0e, 0x0a, 0x02, - 0x77, 0x31, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x77, 0x31, 0x12, 0x0e, 0x0a, 0x02, - 0x77, 0x32, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x77, 0x32, 0x12, 0x0c, 0x0a, 0x01, - 0x76, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x76, 0x22, 0x12, 0x0a, 0x10, 0x44, 0x47, - 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x34, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x22, 0x11, - 0x0a, 0x0f, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x35, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x42, 0x11, 0x5a, 0x0f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2f, 0x72, 0x65, 0x73, 0x68, 0x61, - 0x72, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x08, + 0x6d, 0x6f, 0x64, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x60, 0x0a, 0x0e, 0x6d, 0x6f, 0x64, 0x70, + 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x74, 0x69, 0x6c, 0x64, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x39, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, + 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, + 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x31, 0x2e, 0x4d, 0x6f, 0x64, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0d, 0x6d, 0x6f, 0x64, + 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x54, 0x69, 0x6c, 0x64, 0x65, 0x1a, 0x2e, 0x0a, 0x08, 0x44, 0x4c, + 0x4e, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x12, 0x0c, 0x0a, 0x01, + 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x01, 0x74, 0x1a, 0x50, 0x0a, 0x08, 0x4d, 0x6f, + 0x64, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, + 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x08, 0x52, 0x01, 0x61, + 0x12, 0x0c, 0x0a, 0x01, 0x62, 0x18, 0x04, 0x20, 0x03, 0x28, 0x08, 0x52, 0x01, 0x62, 0x12, 0x0c, + 0x0a, 0x01, 0x7a, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x01, 0x7a, 0x4a, 0x04, 0x08, 0x06, + 0x10, 0x07, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0x12, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, + 0x75, 0x6e, 0x64, 0x32, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x22, 0x28, 0x0a, 0x10, + 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x33, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x73, 0x68, 0x61, 0x72, 0x65, 0x22, 0x39, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, + 0x64, 0x33, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x12, 0x25, 0x0a, 0x0e, 0x76, 0x5f, + 0x64, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x0d, 0x76, 0x44, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, + 0x74, 0x22, 0x8b, 0x03, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x34, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x12, 0x58, 0x0a, 0x08, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, + 0x6f, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, + 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, + 0x64, 0x34, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x46, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x08, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, + 0x12, 0x63, 0x0a, 0x0e, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x5f, 0x74, 0x69, 0x6c, + 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x62, 0x69, 0x6e, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x74, 0x73, 0x73, 0x6c, 0x69, 0x62, 0x2e, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2e, + 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, + 0x64, 0x34, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x31, 0x2e, 0x46, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x0d, 0x66, 0x61, 0x63, 0x70, 0x72, 0x6f, 0x6f, 0x66, + 0x54, 0x69, 0x6c, 0x64, 0x65, 0x1a, 0xb7, 0x01, 0x0a, 0x0b, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x0c, 0x0a, 0x01, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x01, 0x70, 0x12, 0x0c, 0x0a, 0x01, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, + 0x71, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x61, 0x12, + 0x0c, 0x0a, 0x01, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x62, 0x12, 0x0c, 0x0a, + 0x01, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x69, 0x67, 0x6d, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x69, 0x67, 0x6d, + 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x7a, 0x31, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x7a, + 0x31, 0x12, 0x0e, 0x0a, 0x02, 0x7a, 0x32, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x7a, + 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x77, 0x31, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x77, + 0x31, 0x12, 0x0e, 0x0a, 0x02, 0x77, 0x32, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x77, + 0x32, 0x12, 0x0c, 0x0a, 0x01, 0x76, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x01, 0x76, 0x22, + 0x12, 0x0a, 0x10, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x34, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x32, 0x22, 0x11, 0x0a, 0x0f, 0x44, 0x47, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x35, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x11, 0x5a, 0x0f, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2f, + 0x72, 0x65, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( diff --git a/ecdsa/resharing/local_party_test.go b/ecdsa/resharing/local_party_test.go index 390527c2d..8ecd86888 100644 --- a/ecdsa/resharing/local_party_test.go +++ b/ecdsa/resharing/local_party_test.go @@ -177,8 +177,10 @@ signing: } }() + signCeremonyNonce := big.NewInt(1) for j, signPID := range signPIDs { params := tss.NewParameters(tss.S256(), signP2pCtx, signPID, len(signPIDs), newThreshold) + params.SetSessionNonce(signCeremonyNonce) P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh).(*signing.LocalParty) signParties = append(signParties, P) go func(P *signing.LocalParty) { diff --git a/ecdsa/resharing/messages.go b/ecdsa/resharing/messages.go index d9a65b515..45d49f1ae 100644 --- a/ecdsa/resharing/messages.go +++ b/ecdsa/resharing/messages.go @@ -39,6 +39,7 @@ func NewDGRound1Message( from *tss.PartyID, ecdsaPub *crypto.ECPoint, vct cmt.HashCommitment, + ssid []byte, ) tss.ParsedMessage { meta := tss.MessageRouting{ From: from, @@ -50,6 +51,7 @@ func NewDGRound1Message( EcdsaPubX: ecdsaPub.X().Bytes(), EcdsaPubY: ecdsaPub.Y().Bytes(), VCommitment: vct.Bytes(), + Ssid: append([]byte(nil), ssid...), } msg := tss.NewMessageWrapper(meta, content) return tss.NewMessage(meta, content, msg) @@ -59,7 +61,8 @@ func (m *DGRound1Message) ValidateBasic() bool { return m != nil && common.NonEmptyBytes(m.EcdsaPubX) && common.NonEmptyBytes(m.EcdsaPubY) && - common.NonEmptyBytes(m.VCommitment) + common.NonEmptyBytes(m.VCommitment) && + common.NonEmptyBytes(m.Ssid) } func (m *DGRound1Message) UnmarshalECDSAPub(ec elliptic.Curve) (*crypto.ECPoint, error) { diff --git a/ecdsa/resharing/messages_test.go b/ecdsa/resharing/messages_test.go new file mode 100644 index 000000000..1ea47d0e3 --- /dev/null +++ b/ecdsa/resharing/messages_test.go @@ -0,0 +1,50 @@ +// 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 resharing + +import ( + "testing" +) + +// TestDGRound1Message_ValidateBasic_RequiresSsid pins the wire-format +// invariant that the SSID field must be present on every DGRound1Message. +// Without this, an attacker could strip the SSID from a broadcast and the +// new-committee cross-verification check in round 1 would silently never +// fire (the message would be rejected for other reasons or accepted with an +// empty SSID, both of which mask the disagreement-detection mechanism the +// SSID-on-the-wire was added for). +func TestDGRound1Message_ValidateBasic_RequiresSsid(t *testing.T) { + withSsid := &DGRound1Message{ + EcdsaPubX: []byte{0x01}, + EcdsaPubY: []byte{0x02}, + VCommitment: []byte{0x03}, + Ssid: []byte{0x04}, + } + if !withSsid.ValidateBasic() { + t.Fatal("ValidateBasic must accept a complete DGRound1Message") + } + + missingSsid := &DGRound1Message{ + EcdsaPubX: []byte{0x01}, + EcdsaPubY: []byte{0x02}, + VCommitment: []byte{0x03}, + // Ssid intentionally omitted + } + if missingSsid.ValidateBasic() { + t.Fatal("ValidateBasic must reject a DGRound1Message with empty Ssid") + } + + emptySsid := &DGRound1Message{ + EcdsaPubX: []byte{0x01}, + EcdsaPubY: []byte{0x02}, + VCommitment: []byte{0x03}, + Ssid: []byte{}, + } + if emptySsid.ValidateBasic() { + t.Fatal("ValidateBasic must reject a DGRound1Message with zero-length Ssid") + } +} diff --git a/ecdsa/resharing/round_1_old_step_1.go b/ecdsa/resharing/round_1_old_step_1.go index 9fa6f5cee..12b246404 100644 --- a/ecdsa/resharing/round_1_old_step_1.go +++ b/ecdsa/resharing/round_1_old_step_1.go @@ -7,8 +7,10 @@ package resharing import ( + "bytes" "errors" "fmt" + "math/big" "github.com/bnb-chain/tss-lib/crypto" "github.com/bnb-chain/tss-lib/crypto/commitments" @@ -33,6 +35,19 @@ func (round *round1) Start() *tss.Error { round.resetOK() // resets both round.oldOK and round.newOK round.allNewOK() + // Derive SSID for both committees so the old committee can broadcast it + // in DGRound1Message and the new committee can cross-check that every + // old-committee party agrees. Both committees can derive locally from + // public inputs (party IDs, curve, round number, ssidNonce); broadcasting + // adds early detection of a corrupted old-committee party who would + // otherwise emit divergent SSIDs across new-committee members. + if nonce := round.Params().SessionNonce(); nonce != nil { + round.temp.ssidNonce = new(big.Int).Set(nonce) + } else { + round.temp.ssidNonce = new(big.Int).SetUint64(0) + } + round.temp.ssid = round.getSSID() + if !round.ReSharingParams().IsOldCommittee() { return nil } @@ -66,10 +81,11 @@ func (round *round1) Start() *tss.Error { round.temp.VD = vCmt.D round.temp.NewShares = shares - // 5. "broadcast" C_i to members of the NEW committee + // 5. "broadcast" C_i to members of the NEW committee, including this + // party's locally-derived SSID so the new committee can cross-verify. r1msg := NewDGRound1Message( round.NewParties().IDs().Exclude(round.PartyID()), round.PartyID(), - round.input.ECDSAPub, vCmt.C) + round.input.ECDSAPub, vCmt.C, round.temp.ssid) round.temp.dgRound1Messages[i] = r1msg round.out <- r1msg @@ -105,6 +121,18 @@ func (round *round1) Update() (bool, *tss.Error) { ret = false continue } + // Verify the sender's broadcast SSID matches our locally-derived SSID + // before consuming any field of the message. A mismatch means either + // (a) this old-committee party is corrupted and broadcasting an + // inconsistent SSID across new-committee members, or (b) the parties + // disagree on the protocol context (party IDs, curve, session + // nonce). Either way the protocol must abort and identify the + // culprit before downstream proof verification could mask the cause. + senderMsg := round.temp.dgRound1Messages[j].Content().(*DGRound1Message) + if !bytes.Equal(senderMsg.GetSsid(), round.temp.ssid) { + return false, round.WrapError(errors.New("DGRound1Message ssid does not match locally-derived ssid — old-committee party broadcast inconsistent SSID"), msg.GetFrom()) + } + // save the ecdsa pub received from the old committee r1msg := round.temp.dgRound1Messages[0].Content().(*DGRound1Message) candidate, err := r1msg.UnmarshalECDSAPub(round.Params().EC()) diff --git a/ecdsa/resharing/round_2_new_step_1.go b/ecdsa/resharing/round_2_new_step_1.go index 7666c2341..1abc5c977 100644 --- a/ecdsa/resharing/round_2_new_step_1.go +++ b/ecdsa/resharing/round_2_new_step_1.go @@ -25,12 +25,10 @@ func (round *round2) Start() *tss.Error { round.started = true round.resetOK() // resets both round.oldOK and round.newOK round.allOldOK() - if nonce := round.Params().SessionNonce(); nonce != nil { - round.temp.ssidNonce = new(big.Int).Set(nonce) - } else { - round.temp.ssidNonce = new(big.Int).SetUint64(0) - } - round.temp.ssid = round.getSSID() + // round.temp.ssid and round.temp.ssidNonce were set in round 1 (for both + // old and new committees) so the old committee could broadcast SSID and + // the new committee could cross-verify. Reusing the round-1 value here + // keeps proof contexts consistent across rounds. if !round.ReSharingParams().IsNewCommittee() { return nil diff --git a/eddsa/resharing/local_party_test.go b/eddsa/resharing/local_party_test.go index e92048a0a..dd97f8560 100644 --- a/eddsa/resharing/local_party_test.go +++ b/eddsa/resharing/local_party_test.go @@ -170,8 +170,10 @@ signing: } }() + signCeremonyNonce := big.NewInt(1) for j, signPID := range signPIDs { params := tss.NewParameters(tss.Edwards(), signP2pCtx, signPID, len(signPIDs), newThreshold) + params.SetSessionNonce(signCeremonyNonce) P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh).(*signing.LocalParty) signParties = append(signParties, P) go func(P *signing.LocalParty) { diff --git a/protob/ecdsa-resharing.proto b/protob/ecdsa-resharing.proto index a1ac6b91c..9ec4fd243 100644 --- a/protob/ecdsa-resharing.proto +++ b/protob/ecdsa-resharing.proto @@ -10,11 +10,15 @@ option go_package = "ecdsa/resharing"; /* * The Round 1 data is broadcast to peers of the New Committee in this message. + * Each old-committee party broadcasts the locally-derived SSID so the new + * committee can detect a corrupted old-committee party broadcasting an + * inconsistent SSID across new-committee members. */ message DGRound1Message { bytes ecdsa_pub_x = 1; bytes ecdsa_pub_y = 2; bytes v_commitment = 3; + bytes ssid = 4; } /* From b6b48b227ded3ec29c5513504654e2f703dfb5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Tue, 19 May 2026 14:26:46 +0000 Subject: [PATCH 10/24] Fail closed when keygen/resharing without SessionNonce ECDSA keygen, EdDSA keygen, and ECDSA resharing all fell back to a zero ssidNonce when the caller did not set SessionNonce. The SSID derivation then collapsed to a single canonical value across any two ceremonies over otherwise-identical committees, neutralising the per-session proof-context binding that the BNB hardening was meant to add. Combined with the wire-format SSID broadcast added in the preceding commit, a zero-nonce fallback would also make the cross-old-committee SSID equality check trivially pass even when parties never explicitly agreed on a session. Return tss.Error from round 1 of all three protocols (ECDSA keygen, EdDSA keygen, ECDSA resharing) when SessionNonce is nil, naming the constraint and the setter the caller must call. EdDSA resharing has no SSID derivation today and is unchanged. Update the existing E2E ceremonies (ecdsa/keygen, ecdsa/resharing, eddsa/keygen) and the smaller keygen tests (TestStartRound1Paillier, TestFinishAndSaveH1H2, TestBadMessageCulprits) to call params.SetSessionNonce before constructing parties. Add TestKeygen_Start_RequiresSessionNonce for both protocols and TestResharing_Start_RequiresSessionNonce for ECDSA pinning the fail-closed behaviour. Update tss.Parameters.SetSessionNonce godoc and BNB_HARDENING_INTEGRATION.md so the disclosure reflects the new contract: keygen, resharing, and signing all require an explicit per-ceremony nonce. --- BNB_HARDENING_INTEGRATION.md | 2 +- ecdsa/keygen/local_party_test.go | 36 +++++++++++++++++++++++++++ ecdsa/keygen/round_1.go | 13 +++++++--- ecdsa/resharing/local_party_test.go | 36 +++++++++++++++++++++++++++ ecdsa/resharing/round_1_old_step_1.go | 13 +++++++--- eddsa/keygen/local_party_test.go | 27 ++++++++++++++++++++ eddsa/keygen/round_1.go | 13 +++++++--- tss/params.go | 15 +++++------ 8 files changed, 133 insertions(+), 22 deletions(-) diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index 87a8c5903..80636736f 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -23,7 +23,7 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. -- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (rejects empty session IDs), and wired ECDSA/EdDSA keygen/signing/resharing SSID derivation. Signing now requires `SetSessionNonce` and fails closed if it is not set — the previous SHA512_256(messageBytes) fallback made two concurrent ceremonies on the same canonical message collide on SSID and proof contexts, enabling Fiat-Shamir transcript splicing. Keygen and resharing support caller-provided nonce and otherwise preserve BNB's zero fallback. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (rejects empty session IDs), and wired ECDSA/EdDSA keygen/signing/resharing SSID derivation. Keygen, resharing, and signing now require `SetSessionNonce` and fail closed if it is not set — the previous zero fallback (keygen/resharing) and SHA512_256(messageBytes) fallback (signing) caused two ceremonies with otherwise-identical inputs to derive the same SSID, breaking the session-binding property the proofs rely on. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. - Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID. The earlier signing default that derived an SSID nonce from full message bytes has been removed in favour of the fail-closed requirement above; the helpers that produced it (`messageBytes`, `messageSessionNonce`) are gone with it. diff --git a/ecdsa/keygen/local_party_test.go b/ecdsa/keygen/local_party_test.go index 3df0509b7..e8d903799 100644 --- a/ecdsa/keygen/local_party_test.go +++ b/ecdsa/keygen/local_party_test.go @@ -14,6 +14,7 @@ import ( "math/big" "os" "runtime" + "strings" "sync/atomic" "testing" @@ -63,6 +64,36 @@ func setUp(level string) { } } +// TestKeygen_Start_RequiresSessionNonce pins that keygen fails closed when +// no SessionNonce is set. Previously, round 1 fell back to a zero nonce, +// neutralising the SSID binding for any caller that forgot +// SetSessionNonce — two keygen ceremonies over otherwise-identical +// committees would derive the same SSID, breaking the session-binding +// property the BNB hardening was meant to add. +func TestKeygen_Start_RequiresSessionNonce(t *testing.T) { + setUp("info") + pIDs := tss.GenerateTestPartyIDs(2) + p2pCtx := tss.NewPeerContext(pIDs) + params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[0], len(pIDs), 1) + // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. + + out := make(chan tss.Message, 1) + end := make(chan LocalPartySaveData, 1) + fixtures, _, err := LoadKeygenTestFixtures(testParticipants) + if err != nil { + t.Skip("test fixtures required (LocalPreParams) to reach the nonce check") + } + lp := NewLocalParty(params, out, end, fixtures[0].LocalPreParams).(*LocalParty) + + tssErr := lp.Start() + if tssErr == nil { + t.Fatal("Start must return an error without SessionNonce") + } + if !strings.Contains(tssErr.Error(), "SetSessionNonce") { + t.Fatalf("error must reference SetSessionNonce, got: %v", tssErr) + } +} + func TestStartRound1Paillier(t *testing.T) { setUp("debug") @@ -70,6 +101,7 @@ func TestStartRound1Paillier(t *testing.T) { p2pCtx := tss.NewPeerContext(pIDs) threshold := 1 params := tss.NewParameters(tss.EC(), p2pCtx, pIDs[0], len(pIDs), threshold) + params.SetSessionNonce(big.NewInt(1)) fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants) if err != nil { @@ -110,6 +142,7 @@ func TestFinishAndSaveH1H2(t *testing.T) { p2pCtx := tss.NewPeerContext(pIDs) threshold := 1 params := tss.NewParameters(tss.EC(), p2pCtx, pIDs[0], len(pIDs), threshold) + params.SetSessionNonce(big.NewInt(2)) fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants) if err != nil { @@ -157,6 +190,7 @@ func TestBadMessageCulprits(t *testing.T) { pIDs := tss.GenerateTestPartyIDs(2) p2pCtx := tss.NewPeerContext(pIDs) params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[0], len(pIDs), 1) + params.SetSessionNonce(big.NewInt(3)) fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants) if err != nil { @@ -215,9 +249,11 @@ func TestE2EConcurrentAndSaveFixtures(t *testing.T) { startGR := runtime.NumGoroutine() // init the parties + ceremonyNonce := big.NewInt(4) for i := 0; i < len(pIDs); i++ { var P *LocalParty params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold) + params.SetSessionNonce(ceremonyNonce) if i < len(fixtures) { P = NewLocalParty(params, outCh, endCh, fixtures[i].LocalPreParams).(*LocalParty) } else { diff --git a/ecdsa/keygen/round_1.go b/ecdsa/keygen/round_1.go index 54c65f914..9f7e46e2e 100644 --- a/ecdsa/keygen/round_1.go +++ b/ecdsa/keygen/round_1.go @@ -84,11 +84,16 @@ func (round *round1) Start() *tss.Error { round.save.NTildej[i] = preParams.NTildei round.save.H1j[i], round.save.H2j[i] = preParams.H1i, preParams.H2i - if nonce := round.Params().SessionNonce(); nonce != nil { - round.temp.ssidNonce = new(big.Int).Set(nonce) - } else { - round.temp.ssidNonce = new(big.Int).SetUint64(0) + // Keygen fails closed if no SessionNonce is set. The previous zero + // fallback neutralised the SSID binding for any caller that forgot + // SetSessionNonce — two keygen ceremonies over otherwise identical + // committees would derive the same SSID, exposing proof transcripts + // to splicing between runs. + nonce := round.Params().SessionNonce() + if nonce == nil { + return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) } + round.temp.ssidNonce = new(big.Int).Set(nonce) round.temp.ssid = round.getSSID() contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) diff --git a/ecdsa/resharing/local_party_test.go b/ecdsa/resharing/local_party_test.go index 8ecd86888..d45aa1e72 100644 --- a/ecdsa/resharing/local_party_test.go +++ b/ecdsa/resharing/local_party_test.go @@ -12,6 +12,7 @@ import ( "math/big" "reflect" "runtime" + "strings" "sync/atomic" "testing" @@ -38,6 +39,38 @@ func setUp(level string) { } } +// TestResharing_Start_RequiresSessionNonce pins that resharing fails closed +// when no SessionNonce is set. Previously, round 1 fell back to a zero +// nonce, neutralising the SSID binding for any caller that forgot +// SetSessionNonce — two resharing ceremonies over identical committees +// would derive the same SSID, breaking session binding (and the new +// wire-format SSID broadcast check loses its meaning when the SSIDs +// collapse to a single canonical zero-nonced value across all ceremonies). +func TestResharing_Start_RequiresSessionNonce(t *testing.T) { + setUp("info") + oldKeys, oldPIDs, err := keygen.LoadKeygenTestFixtures(testThreshold + 1) + assert.NoError(t, err, "should load keygen fixtures") + + oldP2PCtx := tss.NewPeerContext(oldPIDs) + newPIDs := tss.GenerateTestPartyIDs(testParticipants) + newP2PCtx := tss.NewPeerContext(newPIDs) + + out := make(chan tss.Message, 8) + end := make(chan keygen.LocalPartySaveData, 8) + + // Old-committee party 0, no SetSessionNonce. + params := tss.NewReSharingParameters(tss.S256(), oldP2PCtx, newP2PCtx, oldPIDs[0], testParticipants, testThreshold, len(newPIDs), testThreshold) + + P := NewLocalParty(params, oldKeys[0], out, end).(*LocalParty) + tssErr := P.Start() + if tssErr == nil { + t.Fatal("Start must return an error without SessionNonce") + } + if !strings.Contains(tssErr.Error(), "SetSessionNonce") { + t.Fatalf("error must reference SetSessionNonce, got: %v", tssErr) + } +} + func TestE2EConcurrent(t *testing.T) { setUp("info") @@ -72,14 +105,17 @@ func TestE2EConcurrent(t *testing.T) { updater := test.SharedPartyUpdater // init the old parties first + resharingCeremonyNonce := big.NewInt(7) for j, pID := range oldPIDs { params := tss.NewReSharingParameters(tss.S256(), oldP2PCtx, newP2PCtx, pID, testParticipants, threshold, newPCount, newThreshold) + params.SetSessionNonce(resharingCeremonyNonce) P := NewLocalParty(params, oldKeys[j], outCh, endCh).(*LocalParty) // discard old key data oldCommittee = append(oldCommittee, P) } // init the new parties for j, pID := range newPIDs { params := tss.NewReSharingParameters(tss.S256(), oldP2PCtx, newP2PCtx, pID, testParticipants, threshold, newPCount, newThreshold) + params.SetSessionNonce(resharingCeremonyNonce) save := keygen.NewLocalPartySaveData(newPCount) if j < len(fixtures) && len(newPIDs) <= len(fixtures) { save.LocalPreParams = fixtures[j].LocalPreParams diff --git a/ecdsa/resharing/round_1_old_step_1.go b/ecdsa/resharing/round_1_old_step_1.go index 12b246404..9feb59eb3 100644 --- a/ecdsa/resharing/round_1_old_step_1.go +++ b/ecdsa/resharing/round_1_old_step_1.go @@ -41,11 +41,16 @@ func (round *round1) Start() *tss.Error { // public inputs (party IDs, curve, round number, ssidNonce); broadcasting // adds early detection of a corrupted old-committee party who would // otherwise emit divergent SSIDs across new-committee members. - if nonce := round.Params().SessionNonce(); nonce != nil { - round.temp.ssidNonce = new(big.Int).Set(nonce) - } else { - round.temp.ssidNonce = new(big.Int).SetUint64(0) + // + // Resharing fails closed if no SessionNonce is set. The previous zero + // fallback neutralised the SSID binding for any caller that forgot + // SetSessionNonce — two resharing ceremonies over identical committees + // would derive the same SSID, breaking session binding. + nonce := round.Params().SessionNonce() + if nonce == nil { + return round.WrapError(errors.New("resharing requires tss.Parameters.SetSessionNonce() before Start")) } + round.temp.ssidNonce = new(big.Int).Set(nonce) round.temp.ssid = round.getSSID() if !round.ReSharingParams().IsOldCommittee() { diff --git a/eddsa/keygen/local_party_test.go b/eddsa/keygen/local_party_test.go index 9eda7338c..45f1641dc 100644 --- a/eddsa/keygen/local_party_test.go +++ b/eddsa/keygen/local_party_test.go @@ -12,6 +12,7 @@ import ( "math/big" "os" "runtime" + "strings" "sync/atomic" "testing" @@ -37,6 +38,30 @@ func setUp(level string) { } } +// TestKeygen_Start_RequiresSessionNonce pins that keygen fails closed when +// no SessionNonce is set. Previously, round 1 fell back to a zero nonce, +// neutralising the SSID binding for any caller that forgot +// SetSessionNonce. +func TestKeygen_Start_RequiresSessionNonce(t *testing.T) { + tss.SetCurve(tss.Edwards()) + pIDs := tss.GenerateTestPartyIDs(1) + p2pCtx := tss.NewPeerContext(pIDs) + params := tss.NewParameters(tss.Edwards(), p2pCtx, pIDs[0], len(pIDs), 0) + // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. + + out := make(chan tss.Message, 1) + end := make(chan LocalPartySaveData, 1) + lp := NewLocalParty(params, out, end).(*LocalParty) + + tssErr := lp.Start() + if tssErr == nil { + t.Fatal("Start must return an error without SessionNonce") + } + if !strings.Contains(tssErr.Error(), "SetSessionNonce") { + t.Fatalf("error must reference SetSessionNonce, got: %v", tssErr) + } +} + func TestE2EConcurrentAndSaveFixtures(t *testing.T) { setUp("info") @@ -59,9 +84,11 @@ func TestE2EConcurrentAndSaveFixtures(t *testing.T) { startGR := runtime.NumGoroutine() // init the parties + ceremonyNonce := big.NewInt(1) for i := 0; i < len(pIDs); i++ { var P *LocalParty params := tss.NewParameters(tss.Edwards(), p2pCtx, pIDs[i], len(pIDs), threshold) + params.SetSessionNonce(ceremonyNonce) if i < len(fixtures) { P = NewLocalParty(params, outCh, endCh).(*LocalParty) } else { diff --git a/eddsa/keygen/round_1.go b/eddsa/keygen/round_1.go index 634f06f20..8b22b8702 100644 --- a/eddsa/keygen/round_1.go +++ b/eddsa/keygen/round_1.go @@ -38,11 +38,16 @@ func (round *round1) Start() *tss.Error { Pi := round.PartyID() i := Pi.Index - if nonce := round.Params().SessionNonce(); nonce != nil { - round.temp.ssidNonce = new(big.Int).Set(nonce) - } else { - round.temp.ssidNonce = new(big.Int).SetUint64(0) + // Keygen fails closed if no SessionNonce is set. The previous zero + // fallback neutralised the SSID binding for any caller that forgot + // SetSessionNonce — two keygen ceremonies over otherwise identical + // committees would derive the same SSID, exposing proof transcripts + // to splicing between runs. + nonce := round.Params().SessionNonce() + if nonce == nil { + return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) } + round.temp.ssidNonce = new(big.Int).Set(nonce) ssid, err := round.getSSID() if err != nil { return round.WrapError(err) diff --git a/tss/params.go b/tss/params.go index c2559e44c..38f9e94fd 100644 --- a/tss/params.go +++ b/tss/params.go @@ -100,16 +100,13 @@ func (params *Parameters) SessionNonce() *big.Int { // SetSessionNonce sets a per-session nonce that all parties in a protocol run // must agree on. It must be called before Start. // -// Signing requires this: round 1 fails closed if no nonce is set, because the -// previous SHA512_256(messageBytes) fallback caused two concurrent ceremonies -// on the same canonical message to collide on SSID and proof contexts. For -// signing, the caller must supply a per-ceremony unique nonce; reusing the -// same nonce across distinct ceremonies on the same payload reintroduces +// Keygen, resharing, and signing all fail closed if no nonce is set. The +// previous zero (keygen/resharing) and SHA512_256(messageBytes) (signing) +// fallbacks caused two ceremonies with otherwise-identical inputs to derive +// the same SSID, breaking the session-binding property that the proofs rely +// on. The caller must supply a per-ceremony unique nonce; reusing the same +// nonce across distinct ceremonies on the same inputs reintroduces // transcript-splicing risk. -// -// Keygen and resharing still tolerate an unset nonce (falling back to zero), -// but applications that need unique SSIDs across otherwise identical -// keygen/resharing party sets should also set it explicitly. func (params *Parameters) SetSessionNonce(nonce *big.Int) { if nonce == nil { params.sessionNonce = nil From 78c95280bcdae0aca9e0cce742fd471c223b1cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Tue, 19 May 2026 15:40:33 +0000 Subject: [PATCH 11/24] Pin party-context separation for AppendUint64ToBytesSlice Adds a regression test ensuring per-party Fiat-Shamir context derivation never collapses to the bare SSID, even for party index 0, and that distinct party indices produce distinct contexts. Guards against a future "skip leading zeros" optimization that would re-introduce the party-0 context collision the BNB hardening fixed. --- common/int_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/common/int_test.go b/common/int_test.go index 044ecf7fa..b5f516d3d 100644 --- a/common/int_test.go +++ b/common/int_test.go @@ -144,3 +144,37 @@ func TestAnyIsNil(t *testing.T) { assert.True(common.AnyIsNil(nil, big.NewInt(2))) assert.False(common.AnyIsNil(big.NewInt(1), big.NewInt(2))) } + +// TestAppendUint64ToBytesSlice_PartyContextSeparation pins the invariant that +// per-party Fiat-Shamir context derivation depends on: appending a party index +// (including 0) must always produce a value distinct from the bare SSID, and +// distinct party indices must produce distinct contexts. If this invariant +// regresses (e.g. via a future "skip leading zeros" optimization), party 0's +// proof transcripts would collapse back to the untagged SSID. +func TestAppendUint64ToBytesSlice_PartyContextSeparation(t *testing.T) { + assert := assert.New(t) + + ssid := []byte{0xDE, 0xAD, 0xBE, 0xEF} + + ctx0 := common.AppendUint64ToBytesSlice(ssid, 0) + ctx1 := common.AppendUint64ToBytesSlice(ssid, 1) + ctx256 := common.AppendUint64ToBytesSlice(ssid, 256) + + assert.NotEqual(ssid, ctx0, "party-0 context must not collapse to bare SSID") + assert.NotEqual(ctx0, ctx1, "party-0 and party-1 contexts must differ") + assert.NotEqual(ctx0, ctx256, "party-0 and party-256 contexts must differ") + assert.NotEqual(ctx1, ctx256, "party-1 and party-256 contexts must differ") + + assert.Equal(len(ssid)+8, len(ctx0), "appended index must be a fixed 8 bytes") + assert.Equal(len(ssid)+8, len(ctx1), "appended index must be a fixed 8 bytes") + assert.Equal(ssid, ctx0[:len(ssid)], "SSID prefix must be preserved") + + expectedCtx0 := append(append([]byte{}, ssid...), 0, 0, 0, 0, 0, 0, 0, 0) + assert.Equal(expectedCtx0, ctx0, "party-0 must append 8 zero bytes (big-endian uint64)") + + // nil and empty SSID must still yield distinct, non-collapsing contexts. + emptyCtx0 := common.AppendUint64ToBytesSlice(nil, 0) + emptyCtx1 := common.AppendUint64ToBytesSlice(nil, 1) + assert.Equal(8, len(emptyCtx0), "empty SSID + index 0 must still produce 8 bytes") + assert.NotEqual(emptyCtx0, emptyCtx1, "indices must differ even with empty SSID") +} From c541bda58637a5400aa6825bf31cd6a27058c25a Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 19 May 2026 13:27:59 -0500 Subject: [PATCH 12/24] Validate resharing SSIDs before acceptance --- BNB_HARDENING_INTEGRATION.md | 12 +++--- ecdsa/resharing/messages_test.go | 60 +++++++++++++++++++++++++++ ecdsa/resharing/round_1_old_step_1.go | 12 +++--- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index 80636736f..8a6b526f6 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -23,7 +23,7 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. -- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (rejects empty session IDs), and wired ECDSA/EdDSA keygen/signing/resharing SSID derivation. Keygen, resharing, and signing now require `SetSessionNonce` and fail closed if it is not set — the previous zero fallback (keygen/resharing) and SHA512_256(messageBytes) fallback (signing) caused two ceremonies with otherwise-identical inputs to derive the same SSID, breaking the session-binding property the proofs rely on. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (rejects empty session IDs), and wired ECDSA/EdDSA keygen/signing plus ECDSA resharing SSID derivation. Keygen, ECDSA resharing, and signing now require `SetSessionNonce` and fail closed if it is not set — the previous zero fallback (keygen/resharing) and SHA512_256(messageBytes) fallback (signing) caused two ceremonies with otherwise-identical inputs to derive the same SSID, breaking the session-binding property the proofs rely on. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. - Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID. The earlier signing default that derived an SSID nonce from full message bytes has been removed in favour of the fail-closed requirement above; the helpers that produced it (`messageBytes`, `messageSessionNonce`) are gone with it. @@ -46,9 +46,9 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose ## Semantic Differences From BNB - Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation was retained. No BNB no-proof escape hatches were introduced. -- Session parameters were added as variadic arguments to preserve existing public call sites. This is API source-compatible for callers, but not wire-compatible for proof transcripts. -- Keygen and resharing SSIDs are locally derived and use `Parameters.SessionNonce()` when set. This avoids protobuf/module churn, but callers must provide a unique agreed nonce, for example via `SetSessionNonceBytes`, for keygen/resharing sessions that need distinct proof transcripts. -- ECDSA resharing SSID binding was adapted without adding BNB's newer wire-level SSID message fields. +- Session parameters were added as variadic arguments to preserve existing public call sites. This is API source-compatible for callers, but not wire-compatible for proof transcripts; callers must set `Parameters.SessionNonce()` before starting keygen, signing, or ECDSA resharing. +- Keygen, signing, and ECDSA resharing SSIDs use `Parameters.SessionNonce()`. Callers must provide a unique agreed nonce, for example via `SetSessionNonceBytes`, for every ceremony. +- ECDSA resharing now broadcasts the locally-derived SSID in `DGRound1Message` so the new committee can reject old-committee broadcasts whose SSID differs from the local protocol context. - `common.RejectionSample` keeps BNB's function name for porting clarity, but this implementation is modular reduction rather than a looping rejection sampler. - Constant-time operations are not included and remain a residual follow-up. @@ -74,6 +74,6 @@ Added or updated focused tests cover: ## Residual Risks -- Applications must call `SetSessionNonce` or `SetSessionNonceBytes` for keygen/resharing if they need unique SSIDs across otherwise identical party sets. +- Applications must call `SetSessionNonce` or `SetSessionNonceBytes` before keygen, signing, and ECDSA resharing; those protocols now fail closed without it. - The optional constant-time upstream work is not integrated. -- Resharing SSID binding is adapted locally rather than wire-compatible with BNB's latest protocol messages. +- EdDSA resharing has no SSID-bound proof transcript in this port. diff --git a/ecdsa/resharing/messages_test.go b/ecdsa/resharing/messages_test.go index 1ea47d0e3..442c3ad82 100644 --- a/ecdsa/resharing/messages_test.go +++ b/ecdsa/resharing/messages_test.go @@ -7,7 +7,11 @@ package resharing import ( + "math/big" + "strings" "testing" + + "github.com/bnb-chain/tss-lib/tss" ) // TestDGRound1Message_ValidateBasic_RequiresSsid pins the wire-format @@ -48,3 +52,59 @@ func TestDGRound1Message_ValidateBasic_RequiresSsid(t *testing.T) { t.Fatal("ValidateBasic must reject a DGRound1Message with zero-length Ssid") } } + +// TestRound1Update_RejectsMismatchedSsidBeforePartyZero pins that every old +// committee broadcast is SSID-checked before being marked accepted. In +// particular, old party j>0 may arrive before old party 0; that ordering must +// not bypass the SSID mismatch check. +func TestRound1Update_RejectsMismatchedSsidBeforePartyZero(t *testing.T) { + oldPIDs := tss.GenerateTestPartyIDs(2) + newPIDs := tss.GenerateTestPartyIDs(2) + oldCtx := tss.NewPeerContext(oldPIDs) + newCtx := tss.NewPeerContext(newPIDs) + + params := tss.NewReSharingParameters(tss.S256(), oldCtx, newCtx, newPIDs[0], len(oldPIDs), 1, len(newPIDs), 1) + params.SetSessionNonce(big.NewInt(7)) + + round := &round1{ + base: &base{ + ReSharingParameters: params, + temp: &localTempData{ + localMessageStore: localMessageStore{ + dgRound1Messages: make([]tss.ParsedMessage, len(oldPIDs)), + }, + ssidNonce: params.SessionNonce(), + }, + oldOK: make([]bool, len(oldPIDs)), + newOK: make([]bool, len(newPIDs)), + started: true, + number: 1, + }, + } + round.allNewOK() + round.temp.ssid = round.getSSID() + + content := &DGRound1Message{ + EcdsaPubX: []byte{0x01}, + EcdsaPubY: []byte{0x02}, + VCommitment: []byte{0x03}, + Ssid: []byte("wrong-ssid"), + } + routing := tss.MessageRouting{ + From: oldPIDs[1], + To: newPIDs, + IsBroadcast: true, + } + round.temp.dgRound1Messages[1] = tss.NewMessage(routing, content, tss.NewMessageWrapper(routing, content)) + + _, tssErr := round.Update() + if tssErr == nil { + t.Fatal("expected mismatched SSID to be rejected even when old party 0 has not arrived") + } + if !strings.Contains(tssErr.Error(), "ssid does not match") { + t.Fatalf("unexpected error: %v", tssErr) + } + if round.oldOK[1] { + t.Fatal("old party 1 must not be marked accepted after SSID mismatch") + } +} diff --git a/ecdsa/resharing/round_1_old_step_1.go b/ecdsa/resharing/round_1_old_step_1.go index 9feb59eb3..471b19eb7 100644 --- a/ecdsa/resharing/round_1_old_step_1.go +++ b/ecdsa/resharing/round_1_old_step_1.go @@ -120,12 +120,6 @@ func (round *round1) Update() (bool, *tss.Error) { ret = false continue } - round.oldOK[j] = true - - if round.temp.dgRound1Messages[0] == nil { - ret = false - continue - } // Verify the sender's broadcast SSID matches our locally-derived SSID // before consuming any field of the message. A mismatch means either // (a) this old-committee party is corrupted and broadcasting an @@ -137,6 +131,12 @@ func (round *round1) Update() (bool, *tss.Error) { if !bytes.Equal(senderMsg.GetSsid(), round.temp.ssid) { return false, round.WrapError(errors.New("DGRound1Message ssid does not match locally-derived ssid — old-committee party broadcast inconsistent SSID"), msg.GetFrom()) } + round.oldOK[j] = true + + if round.temp.dgRound1Messages[0] == nil { + ret = false + continue + } // save the ecdsa pub received from the old committee r1msg := round.temp.dgRound1Messages[0].Content().(*DGRound1Message) From 083591414ac76ed67c7b3878f753cdda91e73c40 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 19 May 2026 18:45:37 -0500 Subject: [PATCH 13/24] Address resharing and CI review findings --- .github/workflows/gofmt.yml | 28 +++++++++--- .github/workflows/test.yml | 25 ++++++----- README.md | 2 +- ecdsa/resharing/messages_test.go | 62 +++++++++++++++++++++++++++ ecdsa/resharing/round_1_old_step_1.go | 10 +---- 5 files changed, 100 insertions(+), 27 deletions(-) diff --git a/.github/workflows/gofmt.yml b/.github/workflows/gofmt.yml index a64672894..a0259a0da 100644 --- a/.github/workflows/gofmt.yml +++ b/.github/workflows/gofmt.yml @@ -1,11 +1,29 @@ name: Go-fmt -on: push +on: + push: + branches: + - master + - release/* + pull_request: + branches: + - master + +permissions: + contents: read + jobs: gofmt: name: Go fmt project runs-on: ubuntu-latest steps: - - name: check out - uses: actions/checkout@v2 - - name: go fmt project - uses: Jerome1337/gofmt-action@v1.0.2 \ No newline at end of file + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.x' + cache: true + + - name: Check gofmt + run: test -z "$(gofmt -l .)" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 380e2e857..9b78e55a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,24 +8,23 @@ on: branches: - master +permissions: + contents: read + jobs: build: name: Test - runs-on: macOS-latest + runs-on: ubuntu-latest steps: - - name: Set up Go 1.13 - uses: actions/setup-go@v1 - with: - go-version: 1.13 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v1 + - name: Check out code + uses: actions/checkout@v4 - - name: Get dependencies - run: go get -v -t -d ./... + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.x' + cache: true - name: Run Tests - run: make test_unit_race - + run: make test_unit diff --git a/README.md b/README.md index 4cf82ff47..538addf4f 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ params := tss.NewParameters(curve, ctx, thisParty, len(parties), threshold) params.SetSessionNonceBytes([]byte(sessionID)) ``` -All parties in the run must use the same non-empty session ID. Signing derives a compatibility nonce from the message if none is set, but applications that already maintain a transport session ID should still pass it through. Keygen and re-sharing preserve the historical zero default when unset, so callers should set the nonce explicitly for distinct protocol runs. +All parties in the run must use the same non-empty session ID, and it must be unique to the ceremony. Keygen, signing, and ECDSA re-sharing fail closed if no session nonce is set; reusing a session ID across otherwise identical ceremonies reintroduces transcript-splicing risk. Additionally, there should be a mechanism in your transport to allow for "reliable broadcasts", meaning parties can broadcast a message to other parties such that it's guaranteed that each one receives the same message. There are several examples of algorithms online that do this by sharing and comparing hashes of received messages. diff --git a/ecdsa/resharing/messages_test.go b/ecdsa/resharing/messages_test.go index 442c3ad82..8a37faf1a 100644 --- a/ecdsa/resharing/messages_test.go +++ b/ecdsa/resharing/messages_test.go @@ -11,6 +11,8 @@ import ( "strings" "testing" + "github.com/bnb-chain/tss-lib/crypto" + "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/bnb-chain/tss-lib/tss" ) @@ -108,3 +110,63 @@ func TestRound1Update_RejectsMismatchedSsidBeforePartyZero(t *testing.T) { t.Fatal("old party 1 must not be marked accepted after SSID mismatch") } } + +// TestRound1Update_RejectsMismatchedECDSAPubBeforePartyZero pins that +// DGRound1Message ECDSAPub is checked per sender. A non-zero old party may +// arrive before old party 0, and its public key copy must not be silently +// skipped by waiting for party 0 as a canonical source. +func TestRound1Update_RejectsMismatchedECDSAPubBeforePartyZero(t *testing.T) { + oldPIDs := tss.GenerateTestPartyIDs(2) + newPIDs := tss.GenerateTestPartyIDs(2) + oldCtx := tss.NewPeerContext(oldPIDs) + newCtx := tss.NewPeerContext(newPIDs) + + params := tss.NewReSharingParameters(tss.S256(), oldCtx, newCtx, newPIDs[0], len(oldPIDs), 1, len(newPIDs), 1) + params.SetSessionNonce(big.NewInt(7)) + save := keygen.NewLocalPartySaveData(len(newPIDs)) + + round := &round1{ + base: &base{ + ReSharingParameters: params, + temp: &localTempData{ + localMessageStore: localMessageStore{ + dgRound1Messages: make([]tss.ParsedMessage, len(oldPIDs)), + }, + ssidNonce: params.SessionNonce(), + }, + save: &save, + oldOK: make([]bool, len(oldPIDs)), + newOK: make([]bool, len(newPIDs)), + started: true, + number: 1, + }, + } + round.allNewOK() + round.temp.ssid = round.getSSID() + round.save.ECDSAPub = crypto.ScalarBaseMult(tss.S256(), big.NewInt(1)) + + differentECDSAPub := crypto.ScalarBaseMult(tss.S256(), big.NewInt(2)) + content := &DGRound1Message{ + EcdsaPubX: differentECDSAPub.X().Bytes(), + EcdsaPubY: differentECDSAPub.Y().Bytes(), + VCommitment: []byte{0x03}, + Ssid: round.temp.ssid, + } + routing := tss.MessageRouting{ + From: oldPIDs[1], + To: newPIDs, + IsBroadcast: true, + } + round.temp.dgRound1Messages[1] = tss.NewMessage(routing, content, tss.NewMessageWrapper(routing, content)) + + _, tssErr := round.Update() + if tssErr == nil { + t.Fatal("expected mismatched ECDSA public key to be rejected even when old party 0 has not arrived") + } + if !strings.Contains(tssErr.Error(), "ecdsa pub key did not match") { + t.Fatalf("unexpected error: %v", tssErr) + } + if round.oldOK[1] { + t.Fatal("old party 1 must not be marked accepted after ECDSA public key mismatch") + } +} diff --git a/ecdsa/resharing/round_1_old_step_1.go b/ecdsa/resharing/round_1_old_step_1.go index 471b19eb7..f16c461a2 100644 --- a/ecdsa/resharing/round_1_old_step_1.go +++ b/ecdsa/resharing/round_1_old_step_1.go @@ -131,16 +131,9 @@ func (round *round1) Update() (bool, *tss.Error) { if !bytes.Equal(senderMsg.GetSsid(), round.temp.ssid) { return false, round.WrapError(errors.New("DGRound1Message ssid does not match locally-derived ssid — old-committee party broadcast inconsistent SSID"), msg.GetFrom()) } - round.oldOK[j] = true - - if round.temp.dgRound1Messages[0] == nil { - ret = false - continue - } // save the ecdsa pub received from the old committee - r1msg := round.temp.dgRound1Messages[0].Content().(*DGRound1Message) - candidate, err := r1msg.UnmarshalECDSAPub(round.Params().EC()) + candidate, err := senderMsg.UnmarshalECDSAPub(round.Params().EC()) if err != nil { return false, round.WrapError(errors.New("unable to unmarshal the ecdsa pub key"), msg.GetFrom()) } @@ -150,6 +143,7 @@ func (round *round1) Update() (bool, *tss.Error) { return false, round.WrapError(errors.New("ecdsa pub key did not match what we received previously"), msg.GetFrom()) } round.save.ECDSAPub = candidate + round.oldOK[j] = true } return ret, nil } From 7bf584b10123cc2875ff23a4fa106be517790ecc Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 19 May 2026 18:52:58 -0500 Subject: [PATCH 14/24] Run Go CI on hardening branch pushes --- .github/workflows/gofmt.yml | 3 --- .github/workflows/test.yml | 3 --- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/gofmt.yml b/.github/workflows/gofmt.yml index a0259a0da..88e201908 100644 --- a/.github/workflows/gofmt.yml +++ b/.github/workflows/gofmt.yml @@ -1,9 +1,6 @@ name: Go-fmt on: push: - branches: - - master - - release/* pull_request: branches: - master diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b78e55a3..e705d50d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,6 @@ name: Go Test on: push: - branches: - - master - - release/* pull_request: branches: - master From d2fe895795057bcdf715f272a4ba5411ed094d2b Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 20 May 2026 09:29:13 -0500 Subject: [PATCH 15/24] Address proof hardening review comments --- common/hash.go | 12 ++-- crypto/mta/proofs.go | 19 +++++-- crypto/mta/range_proof_test.go | 50 ++++++++++++++++ crypto/paillier/mod_proof.go | 16 +++--- crypto/paillier/mod_proof_test.go | 94 +++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 20 deletions(-) diff --git a/common/hash.go b/common/hash.go index 75764ea7f..2b4a795ff 100644 --- a/common/hash.go +++ b/common/hash.go @@ -94,18 +94,15 @@ func SHA512_256i(in ...*big.Int) *big.Int { } // SHA512_256i_TAGGED is a domain-separated variant of SHA512_256i. The tag is -// hashed and prepended twice, matching the tagged-hash construction used by BNB -// upstream for proof challenges. +// hashed and prepended twice. func SHA512_256i_TAGGED(tag []byte, in ...*big.Int) *big.Int { tagBz := SHA512_256(tag) state := crypto.SHA512_256.New() if _, err := state.Write(tagBz); err != nil { - Logger.Errorf("SHA512_256i_TAGGED Write(tag) failed: %v", err) - return nil + panic("SHA512_256i_TAGGED Write(tag) failed: " + err.Error()) } if _, err := state.Write(tagBz); err != nil { - Logger.Errorf("SHA512_256i_TAGGED Write(tag) failed: %v", err) - return nil + panic("SHA512_256i_TAGGED Write(tag) failed: " + err.Error()) } inLen := len(in) @@ -133,8 +130,7 @@ func SHA512_256i_TAGGED(tag []byte, in ...*big.Int) *big.Int { data = append(data, dataLen...) } if _, err := state.Write(data); err != nil { - Logger.Errorf("SHA512_256i_TAGGED Write(data) failed: %v", err) - return nil + panic("SHA512_256i_TAGGED Write(data) failed: " + err.Error()) } return new(big.Int).SetBytes(state.Sum(nil)) } diff --git a/crypto/mta/proofs.go b/crypto/mta/proofs.go index 03173ca33..0b004160c 100644 --- a/crypto/mta/proofs.go +++ b/crypto/mta/proofs.go @@ -191,11 +191,17 @@ func ProofBobFromBytes(bzs [][]byte) (*ProofBob, error) { // an absent `X` verifies a proof generated without the X consistency check X = g^x 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 || !pf.ProofBob.ValidateBasic() || - (X != nil && (pf.U == nil || !pf.U.ValidateBasic())) || + if pf == nil || pf.ProofBob == nil || pk == nil || NTilde == nil || h1 == nil || h2 == nil || c1 == nil || c2 == nil { return false } + if X != nil { + if !pf.ValidateBasic() { + return false + } + } else if !pf.ProofBob.ValidateBasic() { + return false + } q := ec.Params().N q3 := new(big.Int).Mul(q, q) @@ -358,7 +364,8 @@ func optionalProofSession(session [][]byte) []byte { } func (pf *ProofBob) ValidateBasic() bool { - return pf.Z != nil && + return pf != nil && + pf.Z != nil && pf.ZPrm != nil && pf.T != nil && pf.V != nil && @@ -371,7 +378,11 @@ func (pf *ProofBob) ValidateBasic() bool { } func (pf *ProofBobWC) ValidateBasic() bool { - return pf.ProofBob.ValidateBasic() && pf.U != nil + return pf != nil && + pf.ProofBob != nil && + pf.ProofBob.ValidateBasic() && + pf.U != nil && + pf.U.ValidateBasic() } func (pf *ProofBob) Bytes() [ProofBobBytesParts][]byte { diff --git a/crypto/mta/range_proof_test.go b/crypto/mta/range_proof_test.go index 53e920df8..35141b9df 100644 --- a/crypto/mta/range_proof_test.go +++ b/crypto/mta/range_proof_test.go @@ -48,6 +48,56 @@ func TestProveRangeAlice(t *testing.T) { assert.True(t, ok, "proof must verify") } +func TestProveRangeAliceBypassed(t *testing.T) { + q := tss.EC().Params().N + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + sk0, pk0, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + m0 := common.GetRandomPositiveInt(q) + c0, r0, err := sk0.EncryptAndReturnRandomness(m0) + assert.NoError(t, err) + + primes0 := [2]*big.Int{common.GetRandomPrimeInt(testSafePrimeBits), common.GetRandomPrimeInt(testSafePrimeBits)} + NTildei0, h1i0, h2i0, err := crypto.GenerateNTildei(primes0) + assert.NoError(t, err) + proof0, err := ProveRangeAlice(tss.EC(), pk0, c0, NTildei0, h1i0, h2i0, m0, r0) + assert.NoError(t, err) + + assert.True(t, proof0.Verify(tss.EC(), pk0, NTildei0, h1i0, h2i0, c0), "proof 0 must verify against its own parameters") + + sk1, pk1, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + m1 := common.GetRandomPositiveInt(q) + c1, r1, err := sk1.EncryptAndReturnRandomness(m1) + assert.NoError(t, err) + + primes1 := [2]*big.Int{common.GetRandomPrimeInt(testSafePrimeBits), common.GetRandomPrimeInt(testSafePrimeBits)} + NTildei1, h1i1, h2i1, err := crypto.GenerateNTildei(primes1) + assert.NoError(t, err) + proof1, err := ProveRangeAlice(tss.EC(), pk1, c1, NTildei1, h1i1, h2i1, m1, r1) + assert.NoError(t, err) + + assert.True(t, proof1.Verify(tss.EC(), pk1, NTildei1, h1i1, h2i1, c1), "proof 1 must verify against its own parameters") + + assert.False(t, proof0.Verify(tss.EC(), pk1, NTildei1, h1i1, h2i1, c1), "proof 0 must not verify against proof 1 parameters") + assert.False(t, proof1.Verify(tss.EC(), pk0, NTildei0, h1i0, h2i0, c0), "proof 1 must not verify against proof 0 parameters") + + bypassedProof := &RangeProofAlice{ + S: big.NewInt(1), + S1: big.NewInt(0), + S2: big.NewInt(0), + Z: big.NewInt(1), + U: big.NewInt(1), + W: big.NewInt(1), + } + assert.False(t, bypassedProof.Verify(tss.EC(), pk1, NTildei1, h1i1, h2i1, big.NewInt(1)), "bypassed proof must not verify") +} + func TestProveRangeAliceSessionBinding(t *testing.T) { q := tss.EC().Params().N diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go index 25a756394..e41ff0eb8 100644 --- a/crypto/paillier/mod_proof.go +++ b/crypto/paillier/mod_proof.go @@ -83,22 +83,22 @@ func (pf ModProof) ModVerify(N *big.Int, session ...[]byte) (bool, error) { return false, fmt.Errorf("mod proof verify: modulus %d seems prime", N) } - if big.Jacobi(pf.W, N) != -1 { - return false, fmt.Errorf("mod proof verify: w %d has invalid jacobi symbol %d", pf.W, big.Jacobi(pf.W, N)) + if !common.Gt(pf.W, zero) || !common.Lt(pf.W, N) { + return false, fmt.Errorf("mod proof verify: w must be in [1, N), got %d", pf.W) } - if !common.Lt(pf.W, N) { - return false, fmt.Errorf("mod proof verify: w %d exceeds N %d", pf.W, N) + if big.Jacobi(pf.W, N) != -1 { + return false, fmt.Errorf("mod proof verify: w %d has invalid jacobi symbol %d", pf.W, big.Jacobi(pf.W, N)) } y := ModChallenge(N, pf.W, session...) for i, yi := range y { - if !common.Lt(pf.X[i], N) { - return false, fmt.Errorf("mod proof verify: x_%d %d exceeds N %d", i, pf.X[i], N) + if !common.Gt(pf.X[i], zero) || !common.Lt(pf.X[i], N) { + return false, fmt.Errorf("mod proof verify: x_%d must be in [1, N), got %d", i, pf.X[i]) } - if !common.Lt(pf.Z[i], N) { - return false, fmt.Errorf("mod proof verify: z_%d %d exceeds N %d", i, pf.Z[i], N) + if !common.Gt(pf.Z[i], zero) || !common.Lt(pf.Z[i], N) { + return false, fmt.Errorf("mod proof verify: z_%d must be in [1, N), got %d", i, pf.Z[i]) } ziN := new(big.Int).Exp(pf.Z[i], N, N) diff --git a/crypto/paillier/mod_proof_test.go b/crypto/paillier/mod_proof_test.go index bd71c601b..785686068 100644 --- a/crypto/paillier/mod_proof_test.go +++ b/crypto/paillier/mod_proof_test.go @@ -2,10 +2,12 @@ package paillier import ( "context" + "fmt" "math/big" "testing" "time" + "github.com/bnb-chain/tss-lib/common" "github.com/stretchr/testify/assert" ) @@ -146,6 +148,98 @@ func TestModProofVerify_ForgedProof(t *testing.T) { assert.False(t, res, "proof verify result must be false") } +func TestModProofVerify_AttackMod(t *testing.T) { + session := []byte("mod-proof-attack-session") + + P := mustSetString("11956161572522965463") + Q := []*big.Int{ + mustSetString("2495927741"), + mustSetString("3726287311"), + mustSetString("3756248813"), + mustSetString("3962607427"), + mustSetString("2685519289"), + mustSetString("2316427879"), + mustSetString("3704490329"), + } + + N := new(big.Int).Set(P) + for _, q := range Q { + N.Mul(N, q) + } + + proof, err := newHackedModProof(session, N, P, Q) + assert.NoError(t, err) + + ok, err := proof.ModVerify(N, session) + assert.Error(t, err) + assert.False(t, ok, "false proof should not verify") +} + +func newHackedModProof(session []byte, N, P *big.Int, Q []*big.Int) (*ModProof, error) { + phi := new(big.Int).Sub(P, one) + bigQ := new(big.Int).Set(one) + for _, q := range Q { + phi.Mul(phi, new(big.Int).Sub(q, one)) + bigQ.Mul(bigQ, q) + } + + invBigQ := new(big.Int).ModInverse(bigQ, P) + w := new(big.Int).Mul(invBigQ, bigQ) + if new(big.Int).Mod(w, P).Cmp(one) != 0 { + return nil, fmt.Errorf("w is not congruent to 1 modulo p") + } + for _, q := range Q { + if new(big.Int).Mod(w, q).Cmp(zero) != 0 { + return nil, fmt.Errorf("w is not congruent to 0 modulo all q values") + } + } + + y := ModChallenge(N, w, session) + invN := new(big.Int).ModInverse(N, phi) + if invN == nil { + return nil, fmt.Errorf("N is not invertible modulo phi") + } + + modN := common.ModInt(N) + modP := common.ModInt(P) + expo := new(big.Int).Add(P, one) + expo.Rsh(expo, 3) + + var x [PARAM_M]*big.Int + var a [PARAM_M]bool + var b [PARAM_M]bool + var z [PARAM_M]*big.Int + + for i, yi := range y { + yiP := new(big.Int).Set(yi) + if big.Jacobi(yiP, P) == 1 { + a[i] = false + } else { + a[i] = true + yiP = modN.Mul(big.NewInt(-1), yiP) + } + b[i] = true + x[i] = modN.Mul(modP.Exp(yiP, expo), w) + z[i] = modN.Exp(yi, invN) + } + + return &ModProof{ + W: w, + X: x, + A: a, + B: b, + Z: z, + }, nil +} + +func mustSetString(s string) *big.Int { + i, ok := new(big.Int).SetString(s, 10) + if !ok { + panic("failed to parse integer: " + s) + } + return i +} + func TestModSqrt(t *testing.T) { assert := assert.New(t) b := big.NewInt From c62bf909ce7579941475ca36a6bf4926822db8fb Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 20 May 2026 09:49:00 -0500 Subject: [PATCH 16/24] Stabilize tss-lib CI test setup --- .github/workflows/gofmt.yml | 3 +++ .github/workflows/test.yml | 3 +++ crypto/paillier/factor_proof_test.go | 15 ++++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gofmt.yml b/.github/workflows/gofmt.yml index 26c022e7a..89af57b2a 100644 --- a/.github/workflows/gofmt.yml +++ b/.github/workflows/gofmt.yml @@ -1,6 +1,9 @@ name: Go-fmt on: push: + branches: + - master + - release/* pull_request: branches: - master diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b2523351..b9de11a6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,9 @@ name: Go Test on: push: + branches: + - master + - release/* pull_request: branches: - master diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index 6bd79b694..ffe4cab90 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -25,16 +25,21 @@ func facSetUp(t *testing.T) { return } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Minute) defer cancel() var err error privateKey, publicKey, err = GenerateKeyPair(ctx, testPaillierKeyLength) - assert.NoError(t, err) + if err != nil { + t.Fatalf("failed to generate Paillier key pair: %v", err) + } var err2 error var auxSecret *PrivateKey auxSecret, auxPrime, err2 = GenerateKeyPair(ctx, testPaillierKeyLength) + if err2 != nil { + t.Fatalf("failed to generate auxiliary Paillier key pair: %v", err2) + } lambda := common.GetRandomPositiveInt(auxSecret.PhiN) N := auxPrime.N @@ -42,11 +47,11 @@ func facSetUp(t *testing.T) { tt = new(big.Int).Mod(new(big.Int).Mul(r, r), N) s = new(big.Int).Exp(tt, lambda, N) - assert.NoError(t, err2) - var err3 error badPrivateKey, badPublicKey, err3 = GenerateBadKeyPair(ctx, testPaillierKeyLength) - assert.NoError(t, err3) + if err3 != nil { + t.Fatalf("failed to generate malformed Paillier key pair: %v", err3) + } } func GenerateBadKeyPair(ctx context.Context, modulusBitLen int) (privateKey *PrivateKey, publicKey *PublicKey, err error) { From ae7075f3409ef625409eb86220b361fdae97d7a6 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 20 May 2026 12:26:12 -0500 Subject: [PATCH 17/24] Tighten hardening integration contracts --- BNB_HARDENING_INTEGRATION.md | 5 +- README.md | 5 +- common/hash_test.go | 60 ++++++++++++++++++++++ common/hash_utils_test.go | 15 ++++++ crypto/dlnproof/proof.go | 11 ++-- crypto/dlnproof/proof_test.go | 41 +++++++++++++++ crypto/mta/proofs.go | 12 +++++ crypto/mta/range_proof.go | 11 ++++ crypto/mta/range_proof_test.go | 19 +++++++ crypto/mta/share_protocol_test.go | 14 +++++ crypto/paillier/factor_proof.go | 3 ++ crypto/paillier/factor_proof_test.go | 7 +++ crypto/paillier/mod_proof.go | 3 ++ crypto/paillier/mod_proof_test.go | 6 +++ crypto/vss/feldman_vss.go | 3 ++ ecdsa/keygen/local_party_test.go | 6 +-- ecdsa/keygen/round_1.go | 4 +- ecdsa/keygen/rounds.go | 2 +- ecdsa/resharing/local_party_test.go | 2 +- ecdsa/resharing/messages.go | 2 +- ecdsa/resharing/messages_test.go | 12 ++++- ecdsa/resharing/round_1_old_step_1.go | 4 +- ecdsa/resharing/rounds.go | 2 +- ecdsa/signing/finalize.go | 10 ++-- ecdsa/signing/local_party.go | 48 ++++++++++------- ecdsa/signing/local_party_test.go | 74 +++++++++++++++++++++------ ecdsa/signing/round_1.go | 4 +- ecdsa/signing/rounds.go | 2 +- eddsa/keygen/round_1.go | 4 +- eddsa/keygen/round_3.go | 7 ++- eddsa/keygen/rounds.go | 2 +- eddsa/resharing/local_party_test.go | 7 +-- eddsa/signing/finalize.go | 10 ++-- eddsa/signing/local_party.go | 49 +++++++++++------- eddsa/signing/local_party_test.go | 68 +++++++++++++++++++----- eddsa/signing/round_1.go | 4 +- eddsa/signing/round_3.go | 10 ++-- eddsa/signing/rounds.go | 2 +- tss/params.go | 25 ++++----- tss/params_test.go | 22 +++++++- 40 files changed, 460 insertions(+), 137 deletions(-) create mode 100644 common/hash_test.go create mode 100644 crypto/dlnproof/proof_test.go diff --git a/BNB_HARDENING_INTEGRATION.md b/BNB_HARDENING_INTEGRATION.md index 8a6b526f6..cea9bfc9c 100644 --- a/BNB_HARDENING_INTEGRATION.md +++ b/BNB_HARDENING_INTEGRATION.md @@ -18,12 +18,12 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - `ff989bf` / PR `#257`, tagged hash encoding: ported length-delimited tagged hashing as `common.SHA512_256i_TAGGED`. - `f3aad28` / PR `#276`, nil `String()` panic: ported `BaseParty.String()` nil-round guard. - `409542e` / PR `#282`, round update correctness: ported the `round.ok` accumulation fix for all non-terminal ECDSA/EdDSA keygen, signing, and resharing rounds, plus the resharing party-0 broadcast nil guard. -- `9acd90b`, `2f294cf`, `6b92e7d`, `c0de534` / PR `#284`, leading-zero message signing: manually adapted for ECDSA and EdDSA with backward-compatible variadic `fullBytesLen` parameters. EdDSA now also hashes the full-length message bytes in round 3. +- `9acd90b`, `2f294cf`, `6b92e7d`, `c0de534` / PR `#284`, leading-zero message signing: manually adapted for ECDSA and EdDSA with variadic `fullBytesLen` parameters that are now required at runtime and bounded to the curve order byte length. EdDSA now also hashes the full-length message bytes in round 3. - `843de68` / PR `#291`, VSS threshold-size validation: ported `len(vs) == threshold+1` verification and added test coverage. - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. -- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (rejects empty session IDs), and wired ECDSA/EdDSA keygen/signing plus ECDSA resharing SSID derivation. Keygen, ECDSA resharing, and signing now require `SetSessionNonce` and fail closed if it is not set — the previous zero fallback (keygen/resharing) and SHA512_256(messageBytes) fallback (signing) caused two ceremonies with otherwise-identical inputs to derive the same SSID, breaking the session-binding property the proofs rely on. +- `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (requires session IDs of at least 16 bytes), and wired ECDSA/EdDSA keygen/signing plus ECDSA resharing SSID derivation. Keygen, ECDSA resharing, and signing now require a positive `SetSessionNonce` and fail closed if it is not set — the previous zero fallback (keygen/resharing) and SHA512_256(messageBytes) fallback (signing) caused two ceremonies with otherwise-identical inputs to derive the same SSID, breaking the session-binding property the proofs rely on. - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. - Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID. The earlier signing default that derived an SSID nonce from full message bytes has been removed in favour of the fail-closed requirement above; the helpers that produced it (`messageBytes`, `messageSessionNonce`) are gone with it. @@ -47,6 +47,7 @@ This is a protocol/wire compatibility break for proof transcripts. Proofs whose - Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation was retained. No BNB no-proof escape hatches were introduced. - Session parameters were added as variadic arguments to preserve existing public call sites. This is API source-compatible for callers, but not wire-compatible for proof transcripts; callers must set `Parameters.SessionNonce()` before starting keygen, signing, or ECDSA resharing. +- ECDSA/EdDSA signing constructors still accept `fullBytesLen` as a variadic argument for source compatibility, but exactly one positive value is required at runtime so all signers agree on message byte width before the protocol starts. - Keygen, signing, and ECDSA resharing SSIDs use `Parameters.SessionNonce()`. Callers must provide a unique agreed nonce, for example via `SetSessionNonceBytes`, for every ceremony. - ECDSA resharing now broadcasts the locally-derived SSID in `DGRound1Message` so the new committee can reject old-committee broadcasts whose SSID differs from the local protocol context. - `common.RejectionSample` keeps BNB's function name for porting clarity, but this implementation is modular reduction rather than a looping rejection sampler. diff --git a/README.md b/README.md index 538addf4f..d0f48ff3b 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ Use the `signing.LocalParty` for signing and provide it with a `message` to sign Please note that `t+1` signers are required to sign a message and for optimal usage no more than this should be involved. Each signer should have the same view of who the `t+1` signers are. ```go -party := signing.NewLocalParty(message, params, ourKeyData, outCh, endCh) +fullBytesLen := (params.EC().Params().N.BitLen() + 7) / 8 +party := signing.NewLocalParty(message, params, ourKeyData, outCh, endCh, fullBytesLen) go func() { err := party.Start() // handle err ... @@ -153,7 +154,7 @@ params := tss.NewParameters(curve, ctx, thisParty, len(parties), threshold) params.SetSessionNonceBytes([]byte(sessionID)) ``` -All parties in the run must use the same non-empty session ID, and it must be unique to the ceremony. Keygen, signing, and ECDSA re-sharing fail closed if no session nonce is set; reusing a session ID across otherwise identical ceremonies reintroduces transcript-splicing risk. +All parties in the run must use the same high-entropy session ID of at least 16 bytes, and it must be unique to the ceremony. Keygen, signing, and ECDSA re-sharing fail closed if no session nonce is set; reusing a session ID across otherwise identical ceremonies reintroduces transcript-splicing risk. Additionally, there should be a mechanism in your transport to allow for "reliable broadcasts", meaning parties can broadcast a message to other parties such that it's guaranteed that each one receives the same message. There are several examples of algorithms online that do this by sharing and comparing hashes of received messages. diff --git a/common/hash_test.go b/common/hash_test.go new file mode 100644 index 000000000..1da6969e6 --- /dev/null +++ b/common/hash_test.go @@ -0,0 +1,60 @@ +// 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 common_test + +import ( + "math/big" + "testing" + + "github.com/bnb-chain/tss-lib/common" +) + +func TestSHA512_256iTaggedDomainSeparation(t *testing.T) { + in := []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)} + + tagA := common.SHA512_256i_TAGGED([]byte("tag-a"), in...) + tagAAgain := common.SHA512_256i_TAGGED([]byte("tag-a"), in...) + tagB := common.SHA512_256i_TAGGED([]byte("tag-b"), in...) + + if tagA.Cmp(tagAAgain) != 0 { + t.Fatal("same tag and inputs must hash deterministically") + } + if tagA.Cmp(tagB) == 0 { + t.Fatal("different tags must produce different hashes") + } +} + +func TestSHA512_256iTaggedLengthDelimitsInputs(t *testing.T) { + left := common.SHA512_256i_TAGGED([]byte("tag"), big.NewInt(1), big.NewInt(0x0203)) + right := common.SHA512_256i_TAGGED([]byte("tag"), big.NewInt(0x0102), big.NewInt(3)) + + if left.Cmp(right) == 0 { + t.Fatal("tagged hash must length-delimit adjacent inputs") + } +} + +func TestSHA512_256iTaggedNilAndEmptyTagMatch(t *testing.T) { + nilTag := common.SHA512_256i_TAGGED(nil, big.NewInt(1)) + emptyTag := common.SHA512_256i_TAGGED([]byte{}, big.NewInt(1)) + + if nilTag.Cmp(emptyTag) != 0 { + t.Fatal("nil and empty tags should preserve the legacy untagged domain") + } +} + +func TestHashToNTaggedUsesFullModulusWidth(t *testing.T) { + N := new(big.Int).Lsh(big.NewInt(1), 2048) + N.Sub(N, big.NewInt(159)) + + got := common.HashToNTagged([]byte("large-modulus-tag"), N, big.NewInt(1), big.NewInt(2)) + if got.Sign() < 0 || got.Cmp(N) >= 0 { + t.Fatal("HashToNTagged must return a value in [0, N)") + } + if got.BitLen() <= 256 { + t.Fatalf("HashToNTagged appears truncated to one hash block: bitlen=%d", got.BitLen()) + } +} diff --git a/common/hash_utils_test.go b/common/hash_utils_test.go index 6104affba..91220886a 100644 --- a/common/hash_utils_test.go +++ b/common/hash_utils_test.go @@ -60,3 +60,18 @@ func TestLiterallyJustMod(t *testing.T) { }) } } + +func TestRejectionSampleReducesModuloQ(t *testing.T) { + q := big.NewInt(101) + eHash := big.NewInt(12345) + + got := common.RejectionSample(q, new(big.Int).Set(eHash)) + want := new(big.Int).Mod(eHash, q) + + if got.Cmp(want) != 0 { + t.Fatalf("RejectionSample() = %v, want %v", got, want) + } + if got.Sign() < 0 || got.Cmp(q) >= 0 { + t.Fatal("RejectionSample must return a value in [0, q)") + } +} diff --git a/crypto/dlnproof/proof.go b/crypto/dlnproof/proof.go index 124c41c3d..446e92c7d 100644 --- a/crypto/dlnproof/proof.go +++ b/crypto/dlnproof/proof.go @@ -77,14 +77,16 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { return false } for i := range p.T { - a := new(big.Int).Mod(p.T[i], N) - if a.Cmp(one) != 1 || a.Cmp(N) != -1 { + if p.T[i] == nil || p.T[i].Cmp(one) <= 0 || p.T[i].Cmp(N) >= 0 { return false } } 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) != 1 || a.Cmp(N) != -1 { + if a.Cmp(one) <= 0 || a.Cmp(N) >= 0 { return false } } @@ -111,6 +113,9 @@ func optionalSession(session [][]byte) []byte { if len(session) == 0 { return nil } + if len(session[0]) == 0 { + panic("dlnproof: session tag must be non-empty") + } return session[0] } diff --git a/crypto/dlnproof/proof_test.go b/crypto/dlnproof/proof_test.go new file mode 100644 index 000000000..c319add70 --- /dev/null +++ b/crypto/dlnproof/proof_test.go @@ -0,0 +1,41 @@ +// 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 dlnproof + +import ( + "math/big" + "testing" +) + +func TestDLNProofRejectsEmptySessionTag(t *testing.T) { + assertPanics(t, func() { + _ = NewDLNProof(nil, nil, nil, nil, nil, nil, []byte{}) + }) +} + +func TestDLNProofVerifyRejectsOverwideT(t *testing.T) { + proof := &Proof{} + for i := 0; i < Iterations; i++ { + proof.Alpha[i] = big.NewInt(2) + proof.T[i] = big.NewInt(2) + } + proof.T[0] = big.NewInt(25) + + if proof.Verify(big.NewInt(2), big.NewInt(3), big.NewInt(23)) { + t.Fatal("Verify must reject T values outside [2, N)") + } +} + +func assertPanics(t *testing.T, f func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatal("expected panic") + } + }() + f() +} diff --git a/crypto/mta/proofs.go b/crypto/mta/proofs.go index 0b004160c..51e6e3a6a 100644 --- a/crypto/mta/proofs.go +++ b/crypto/mta/proofs.go @@ -208,6 +208,9 @@ 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) + q3NTilde := new(big.Int).Mul(q3, NTilde) + maxS2 := new(big.Int).Lsh(q3NTilde, 1) + maxT2 := new(big.Int).Set(maxS2) if !common.IsInInterval(pf.Z, NTilde) { return false @@ -272,9 +275,15 @@ 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 { + return false + } if pf.T1.Cmp(q7) > 0 { return false } + if pf.T2.Cmp(maxT2) > 0 { + return false + } // 1-2. e' var e *big.Int @@ -360,6 +369,9 @@ func optionalProofSession(session [][]byte) []byte { if len(session) == 0 { return nil } + if len(session[0]) == 0 { + panic("mta: proof session tag must be non-empty") + } return session[0] } diff --git a/crypto/mta/range_proof.go b/crypto/mta/range_proof.go index f4bbc4668..c7ee82c6b 100644 --- a/crypto/mta/range_proof.go +++ b/crypto/mta/range_proof.go @@ -114,6 +114,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) + q3NTilde := new(big.Int).Mul(q3, NTilde) + maxS2 := new(big.Int).Lsh(q3NTilde, 1) if !common.IsInInterval(pf.Z, NTilde) { return false @@ -145,6 +147,12 @@ func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTi if pf.S.Cmp(one) == 0 { return false } + if pf.S.Cmp(zero) == 0 { + return false + } + if new(big.Int).GCD(nil, nil, pf.S, pk.N).Cmp(one) != 0 { + return false + } if pf.Z.Cmp(one) == 0 { return false } @@ -156,6 +164,9 @@ 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 { + return false + } // 1-2. e' eHash := common.SHA512_256i_TAGGED(Session, append(pk.AsInts(), NTilde, h1, h2, c, pf.Z, pf.U, pf.W)...) diff --git a/crypto/mta/range_proof_test.go b/crypto/mta/range_proof_test.go index 35141b9df..1c41f263d 100644 --- a/crypto/mta/range_proof_test.go +++ b/crypto/mta/range_proof_test.go @@ -25,6 +25,12 @@ const ( testSafePrimeBits = 1024 ) +func TestProofSessionRejectsEmptyTag(t *testing.T) { + assert.Panics(t, func() { + _, _ = ProveRangeAlice(nil, nil, nil, nil, nil, nil, nil, nil, []byte{}) + }) +} + func TestProveRangeAlice(t *testing.T) { q := tss.EC().Params().N @@ -152,6 +158,19 @@ func TestRangeProofAliceRejectsMalformedInputs(t *testing.T) { badS.S = big.NewInt(1) assert.False(t, badS.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "S equal to one must fail") + badSZero := *proof + badSZero.S = big.NewInt(0) + assert.False(t, badSZero.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "S equal to zero must fail") + + q3 := new(big.Int).Mul(q, q) + q3.Mul(q3, q) + tooLargeS2 := new(big.Int).Mul(q3, NTildei) + tooLargeS2.Lsh(tooLargeS2, 1) + tooLargeS2.Add(tooLargeS2, big.NewInt(1)) + badS2 := *proof + badS2.S2 = tooLargeS2 + assert.False(t, badS2.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "overwide S2 must fail before exponentiation") + badZ := *proof badZ.Z = big.NewInt(1) assert.False(t, badZ.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "Z equal to one must fail") diff --git a/crypto/mta/share_protocol_test.go b/crypto/mta/share_protocol_test.go index abddb19ba..5bff62c99 100644 --- a/crypto/mta/share_protocol_test.go +++ b/crypto/mta/share_protocol_test.go @@ -120,6 +120,20 @@ func TestShareProtocolWC(t *testing.T) { badS1.S1 = new(big.Int).Sub(q, big.NewInt(1)) assert.False(t, badS1.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "S1 below q must fail") + q3 := new(big.Int).Mul(q, q) + q3.Mul(q3, q) + tooLargeBlind := new(big.Int).Mul(q3, NTildei) + tooLargeBlind.Lsh(tooLargeBlind, 1) + tooLargeBlind.Add(tooLargeBlind, big.NewInt(1)) + + badS2 := cloneProofBobWC(pfB) + badS2.S2 = new(big.Int).Set(tooLargeBlind) + assert.False(t, badS2.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "overwide S2 must fail before exponentiation") + + badT2 := cloneProofBobWC(pfB) + badT2.T2 = new(big.Int).Set(tooLargeBlind) + assert.False(t, badT2.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "overwide T2 must fail before exponentiation") + badV := cloneProofBobWC(pfB) badV.V = big.NewInt(0) assert.False(t, badV.Verify(tss.EC(), pk, NTildei, h1i, h2i, cA, cB, gBPoint), "V equal to zero must fail") diff --git a/crypto/paillier/factor_proof.go b/crypto/paillier/factor_proof.go index 009008d7a..2302abf60 100644 --- a/crypto/paillier/factor_proof.go +++ b/crypto/paillier/factor_proof.go @@ -154,6 +154,9 @@ func FactorChallenge(N, s, t, pkN, P, Q, A, B, T, sigma *big.Int, session ...[]b qDoubleMinus1 := new(big.Int).Add(q, qMinus1) // q+q-1 = 2q-1 if len(session) > 0 { + if len(session[0]) == 0 { + panic("paillier: factor proof session tag must be non-empty") + } eHash := common.SHA512_256i_TAGGED(session[0], N, s, t, pkN, P, Q, A, B, T, sigma) return common.RejectionSample(q, eHash) } diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index ffe4cab90..20f670e80 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -114,6 +114,13 @@ func TestFactorProofSessionBinding(t *testing.T) { assert.False(t, res, "session-bound proof must not verify without its session") } +func TestFactorChallengeRejectsEmptySessionTag(t *testing.T) { + assert.Panics(t, func() { + _ = FactorChallenge(big.NewInt(11), big.NewInt(2), big.NewInt(3), big.NewInt(5), + big.NewInt(7), big.NewInt(11), big.NewInt(13), big.NewInt(17), big.NewInt(19), big.NewInt(23), []byte{}) + }) +} + func TestFactorProofVerifyFail1(t *testing.T) { facSetUp(t) badN := new(big.Int).Mul(publicKey.N, big.NewInt(3)) diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go index e41ff0eb8..b62557d3d 100644 --- a/crypto/paillier/mod_proof.go +++ b/crypto/paillier/mod_proof.go @@ -142,6 +142,9 @@ func ModChallenge(N, w *big.Int, session ...[]byte) [PARAM_M]*big.Int { y[i] = common.HashToN(N, w, big.NewInt(int64(i))) continue } + if len(session[0]) == 0 { + panic("paillier: mod proof session tag must be non-empty") + } inputs := append([]*big.Int{w, N}, y[:i]...) y[i] = common.HashToNTagged(session[0], N, inputs...) } diff --git a/crypto/paillier/mod_proof_test.go b/crypto/paillier/mod_proof_test.go index 785686068..bf83f87f1 100644 --- a/crypto/paillier/mod_proof_test.go +++ b/crypto/paillier/mod_proof_test.go @@ -50,6 +50,12 @@ func TestModProofSessionBinding(t *testing.T) { assert.False(t, res, "session-bound proof must not verify without its session") } +func TestModChallengeRejectsEmptySessionTag(t *testing.T) { + assert.Panics(t, func() { + _ = ModChallenge(big.NewInt(11), big.NewInt(2), []byte{}) + }) +} + // TestModChallenge_SessionPath_NotTruncated pins the invariant that the // session-tagged ModChallenge path produces challenges with the full // ~N.BitLen() of entropy, not the 256-bit truncated form that would result diff --git a/crypto/vss/feldman_vss.go b/crypto/vss/feldman_vss.go index 07fc81137..8ba7a0454 100644 --- a/crypto/vss/feldman_vss.go +++ b/crypto/vss/feldman_vss.go @@ -113,6 +113,9 @@ func (share *Share) Verify(ec elliptic.Curve, threshold int, vs Vs) bool { } func (shares Shares) ReConstruct(ec elliptic.Curve) (secret *big.Int, err error) { + if len(shares) == 0 { + return nil, ErrNumSharesBelowThreshold + } if shares != nil && shares[0].Threshold+1 > len(shares) { return nil, ErrNumSharesBelowThreshold } diff --git a/ecdsa/keygen/local_party_test.go b/ecdsa/keygen/local_party_test.go index e8d903799..8d8fb4ea2 100644 --- a/ecdsa/keygen/local_party_test.go +++ b/ecdsa/keygen/local_party_test.go @@ -38,9 +38,9 @@ const ( func TestSSIDIncludesSessionNonce(t *testing.T) { pIDs := tss.GenerateTestPartyIDs(3) - ssidA := testKeygenSSID(pIDs, []byte("session-a")) - ssidAAgain := testKeygenSSID(pIDs, []byte("session-a")) - ssidB := testKeygenSSID(pIDs, []byte("session-b")) + ssidA := testKeygenSSID(pIDs, []byte("session-a-with-128-bits")) + ssidAAgain := testKeygenSSID(pIDs, []byte("session-a-with-128-bits")) + ssidB := testKeygenSSID(pIDs, []byte("session-b-with-128-bits")) assert.Equal(t, ssidA, ssidAAgain) assert.NotEqual(t, ssidA, ssidB) diff --git a/ecdsa/keygen/round_1.go b/ecdsa/keygen/round_1.go index 9f7e46e2e..f68476282 100644 --- a/ecdsa/keygen/round_1.go +++ b/ecdsa/keygen/round_1.go @@ -90,8 +90,8 @@ func (round *round1) Start() *tss.Error { // committees would derive the same SSID, exposing proof transcripts // to splicing between runs. nonce := round.Params().SessionNonce() - if nonce == nil { - return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) } round.temp.ssidNonce = new(big.Int).Set(nonce) round.temp.ssid = round.getSSID() diff --git a/ecdsa/keygen/rounds.go b/ecdsa/keygen/rounds.go index 0e7c7a707..9d9c18583 100644 --- a/ecdsa/keygen/rounds.go +++ b/ecdsa/keygen/rounds.go @@ -108,5 +108,5 @@ func (round *base) getSSID() []byte { ssidList = append(ssidList, round.Parties().IDs().Keys()...) ssidList = append(ssidList, big.NewInt(int64(round.number))) ssidList = append(ssidList, round.temp.ssidNonce) - return common.SHA512_256i(ssidList...).Bytes() + return common.SHA512_256i(ssidList...).FillBytes(make([]byte, 32)) } diff --git a/ecdsa/resharing/local_party_test.go b/ecdsa/resharing/local_party_test.go index d45aa1e72..a1981e431 100644 --- a/ecdsa/resharing/local_party_test.go +++ b/ecdsa/resharing/local_party_test.go @@ -217,7 +217,7 @@ signing: for j, signPID := range signPIDs { params := tss.NewParameters(tss.S256(), signP2pCtx, signPID, len(signPIDs), newThreshold) params.SetSessionNonce(signCeremonyNonce) - P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh).(*signing.LocalParty) + P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh, 32).(*signing.LocalParty) signParties = append(signParties, P) go func(P *signing.LocalParty) { if err := P.Start(); err != nil { diff --git a/ecdsa/resharing/messages.go b/ecdsa/resharing/messages.go index 45d49f1ae..e2f1a8020 100644 --- a/ecdsa/resharing/messages.go +++ b/ecdsa/resharing/messages.go @@ -62,7 +62,7 @@ func (m *DGRound1Message) ValidateBasic() bool { common.NonEmptyBytes(m.EcdsaPubX) && common.NonEmptyBytes(m.EcdsaPubY) && common.NonEmptyBytes(m.VCommitment) && - common.NonEmptyBytes(m.Ssid) + len(m.Ssid) == 32 } func (m *DGRound1Message) UnmarshalECDSAPub(ec elliptic.Curve) (*crypto.ECPoint, error) { diff --git a/ecdsa/resharing/messages_test.go b/ecdsa/resharing/messages_test.go index 8a37faf1a..7dd807561 100644 --- a/ecdsa/resharing/messages_test.go +++ b/ecdsa/resharing/messages_test.go @@ -28,7 +28,7 @@ func TestDGRound1Message_ValidateBasic_RequiresSsid(t *testing.T) { EcdsaPubX: []byte{0x01}, EcdsaPubY: []byte{0x02}, VCommitment: []byte{0x03}, - Ssid: []byte{0x04}, + Ssid: make([]byte, 32), } if !withSsid.ValidateBasic() { t.Fatal("ValidateBasic must accept a complete DGRound1Message") @@ -53,6 +53,16 @@ func TestDGRound1Message_ValidateBasic_RequiresSsid(t *testing.T) { if emptySsid.ValidateBasic() { t.Fatal("ValidateBasic must reject a DGRound1Message with zero-length Ssid") } + + shortSsid := &DGRound1Message{ + EcdsaPubX: []byte{0x01}, + EcdsaPubY: []byte{0x02}, + VCommitment: []byte{0x03}, + Ssid: []byte("short-ssid"), + } + if shortSsid.ValidateBasic() { + t.Fatal("ValidateBasic must reject a DGRound1Message with short Ssid") + } } // TestRound1Update_RejectsMismatchedSsidBeforePartyZero pins that every old diff --git a/ecdsa/resharing/round_1_old_step_1.go b/ecdsa/resharing/round_1_old_step_1.go index f16c461a2..7876b595a 100644 --- a/ecdsa/resharing/round_1_old_step_1.go +++ b/ecdsa/resharing/round_1_old_step_1.go @@ -47,8 +47,8 @@ func (round *round1) Start() *tss.Error { // SetSessionNonce — two resharing ceremonies over identical committees // would derive the same SSID, breaking session binding. nonce := round.Params().SessionNonce() - if nonce == nil { - return round.WrapError(errors.New("resharing requires tss.Parameters.SetSessionNonce() before Start")) + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("resharing requires tss.Parameters.SetSessionNonce() before Start")) } round.temp.ssidNonce = new(big.Int).Set(nonce) round.temp.ssid = round.getSSID() diff --git a/ecdsa/resharing/rounds.go b/ecdsa/resharing/rounds.go index 2981290bc..3a820943a 100644 --- a/ecdsa/resharing/rounds.go +++ b/ecdsa/resharing/rounds.go @@ -152,5 +152,5 @@ func (round *base) getSSID() []byte { ssidList = append(ssidList, round.NewParties().IDs().Keys()...) ssidList = append(ssidList, big.NewInt(int64(round.number))) ssidList = append(ssidList, round.temp.ssidNonce) - return common.SHA512_256i(ssidList...).Bytes() + return common.SHA512_256i(ssidList...).FillBytes(make([]byte, 32)) } diff --git a/ecdsa/signing/finalize.go b/ecdsa/signing/finalize.go index 797ba38e2..4b2c0c6e8 100644 --- a/ecdsa/signing/finalize.go +++ b/ecdsa/signing/finalize.go @@ -61,13 +61,9 @@ func (round *finalization) Start() *tss.Error { round.data.S = padToLengthBytesInPlace(sumS.Bytes(), bitSizeInBytes) round.data.Signature = append(round.data.R, round.data.S...) round.data.SignatureRecovery = []byte{byte(recid)} - if round.temp.fullBytesLen == 0 { - round.data.M = round.temp.m.Bytes() - } else { - mBytes := make([]byte, round.temp.fullBytesLen) - round.temp.m.FillBytes(mBytes) - round.data.M = mBytes - } + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + round.data.M = mBytes pk := ecdsa.PublicKey{ Curve: round.Params().EC(), diff --git a/ecdsa/signing/local_party.go b/ecdsa/signing/local_party.go index 14c9b5a59..9fe9f2741 100644 --- a/ecdsa/signing/local_party.go +++ b/ecdsa/signing/local_party.go @@ -111,12 +111,12 @@ func NewLocalParty( // NewLocalPartyWithKDD returns a party with key derivation delta for HD support. // -// Optional fullBytesLen, if provided, fixes the byte width used to encode the -// message in round-1 SSID derivation (preserving leading zero bytes). The -// value must be non-negative and, when non-zero, must be at least -// ceil(msg.BitLen()/8); violating either constraint is a caller bug and the -// constructor panics at the call site rather than later inside a protocol -// goroutine. +// fullBytesLen fixes the byte width used to encode the message for the final +// ECDSA verification/output path (preserving leading zero bytes). Every signer +// in a ceremony must pass the same value. It must be positive, no larger than +// the curve order byte length, and at least ceil(msg.BitLen()/8); violating +// these constraints is a caller bug and the constructor panics at the call site +// rather than later inside a protocol goroutine. func NewLocalPartyWithKDD( msg *big.Int, params *tss.Parameters, @@ -126,15 +126,7 @@ func NewLocalPartyWithKDD( end chan<- common.SignatureData, fullBytesLen ...int, ) tss.Party { - if len(fullBytesLen) > 0 { - if fullBytesLen[0] < 0 { - panic(fmt.Errorf("NewLocalPartyWithKDD: fullBytesLen must be non-negative, got %d", fullBytesLen[0])) - } - if fullBytesLen[0] > 0 && msg != nil && msg.BitLen() > 8*fullBytesLen[0] { - panic(fmt.Errorf("NewLocalPartyWithKDD: fullBytesLen=%d is too small for a %d-bit message (need at least %d bytes)", - fullBytesLen[0], msg.BitLen(), (msg.BitLen()+7)/8)) - } - } + validatedFullBytesLen := validateFullBytesLen("NewLocalPartyWithKDD", msg, params, fullBytesLen) partyCount := len(params.Parties().IDs()) p := &LocalParty{ @@ -160,9 +152,7 @@ func NewLocalPartyWithKDD( // temp data init p.temp.keyDerivationDelta = keyDerivationDelta p.temp.m = msg - if len(fullBytesLen) > 0 { - p.temp.fullBytesLen = fullBytesLen[0] - } + p.temp.fullBytesLen = validatedFullBytesLen p.temp.cis = make([]*big.Int, partyCount) p.temp.bigWs = make([]*crypto.ECPoint, partyCount) p.temp.betas = make([]*big.Int, partyCount) @@ -174,6 +164,28 @@ func NewLocalPartyWithKDD( return p } +func validateFullBytesLen(caller string, msg *big.Int, params *tss.Parameters, fullBytesLen []int) int { + if len(fullBytesLen) != 1 { + panic(fmt.Errorf("%s: fullBytesLen is required and must match all signing parties", caller)) + } + length := fullBytesLen[0] + if length <= 0 { + panic(fmt.Errorf("%s: fullBytesLen must be positive, got %d", caller, length)) + } + if msg != nil && msg.BitLen() > 8*length { + panic(fmt.Errorf("%s: fullBytesLen=%d is too small for a %d-bit message (need at least %d bytes)", + caller, length, msg.BitLen(), (msg.BitLen()+7)/8)) + } + if params == nil || params.EC() == nil || params.EC().Params() == nil || params.EC().Params().N == nil { + panic(fmt.Errorf("%s: params with a curve order is required to validate fullBytesLen", caller)) + } + orderBytes := (params.EC().Params().N.BitLen() + 7) / 8 + if length > orderBytes { + panic(fmt.Errorf("%s: fullBytesLen=%d exceeds curve order byte length %d", caller, length, orderBytes)) + } + return length +} + func (p *LocalParty) FirstRound() tss.Round { return newRound1(p.params, &p.keys, &p.data, &p.temp, p.out, p.end) } diff --git a/ecdsa/signing/local_party_test.go b/ecdsa/signing/local_party_test.go index 61796f93e..306accf65 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -180,7 +180,7 @@ func TestE2EWithHDKeyDerivation(t *testing.T) { params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[i], len(signPIDs), threshold) params.SetSessionNonce(ceremonyNonce) - P := NewLocalPartyWithKDD(big.NewInt(42), params, keys[i], keyDerivationDelta, outCh, endCh).(*LocalParty) + P := NewLocalPartyWithKDD(big.NewInt(42), params, keys[i], keyDerivationDelta, outCh, endCh, 32).(*LocalParty) parties = append(parties, P) go func(P *LocalParty) { if err := P.Start(); err != nil { @@ -268,7 +268,7 @@ func TestSigning_Start_RequiresSessionNonce(t *testing.T) { params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[0], len(signPIDs), testThreshold) // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. - P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh).(*LocalParty) + P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh, 32).(*LocalParty) tssErr := P.Start() if tssErr == nil { t.Fatal("Start must return an error without SessionNonce") @@ -278,51 +278,91 @@ func TestSigning_Start_RequiresSessionNonce(t *testing.T) { } } -// TestNewLocalPartyWithKDD_FullBytesLen_Negative pins constructor-side +// TestNewLocalPartyWithKDD_FullBytesLen_NonPositive pins constructor-side // validation for fullBytesLen. Previously, a negative fullBytesLen passed // through to the round-1 code path, where `make([]byte, fullBytesLen)` // panicked inside a protocol goroutine, bypassing tss.Error reporting and // crossing goroutine boundaries. The constructor now panics synchronously // at the caller's call site with a clear message. -func TestNewLocalPartyWithKDD_FullBytesLen_Negative(t *testing.T) { +func TestNewLocalPartyWithKDD_FullBytesLen_NonPositive(t *testing.T) { msg := big.NewInt(1) + for _, length := range []int{-1, 0} { + func() { + defer func() { + r := recover() + if r == nil { + t.Fatalf("expected panic for fullBytesLen=%d", length) + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "fullBytesLen must be positive") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalPartyWithKDD(msg, nil, keygen.LocalPartySaveData{}, nil, nil, nil, length) + }() + } +} + +// TestNewLocalPartyWithKDD_FullBytesLen_TooSmall pins that a fullBytesLen +// smaller than the message's byte width is rejected at the constructor +// rather than later inside (*big.Int).FillBytes (which would panic with +// "big.Int.FillBytes: insufficient length" inside a protocol goroutine). +func TestNewLocalPartyWithKDD_FullBytesLen_TooSmall(t *testing.T) { + // 16-bit msg needs at least 2 bytes; pass fullBytesLen=1 to trigger. + msg := big.NewInt(0xABCD) defer func() { r := recover() if r == nil { - t.Fatal("expected panic for negative fullBytesLen") + t.Fatal("expected panic for fullBytesLen smaller than msg byte width") } err, ok := r.(error) if !ok { t.Fatalf("panic value must be an error, got %T: %v", r, r) } - if !strings.Contains(err.Error(), "fullBytesLen must be non-negative") { + if !strings.Contains(err.Error(), "fullBytesLen=1 is too small") { t.Fatalf("unexpected panic message: %v", err) } }() - _ = NewLocalPartyWithKDD(msg, nil, keygen.LocalPartySaveData{}, nil, nil, nil, -1) + _ = NewLocalPartyWithKDD(msg, nil, keygen.LocalPartySaveData{}, nil, nil, nil, 1) } -// TestNewLocalPartyWithKDD_FullBytesLen_TooSmall pins that a fullBytesLen -// smaller than the message's byte width is rejected at the constructor -// rather than later inside (*big.Int).FillBytes (which would panic with -// "big.Int.FillBytes: insufficient length" inside a protocol goroutine). -func TestNewLocalPartyWithKDD_FullBytesLen_TooSmall(t *testing.T) { - // 16-bit msg needs at least 2 bytes; pass fullBytesLen=1 to trigger. - msg := big.NewInt(0xABCD) +func TestNewLocalPartyWithKDD_FullBytesLen_Required(t *testing.T) { defer func() { r := recover() if r == nil { - t.Fatal("expected panic for fullBytesLen smaller than msg byte width") + t.Fatal("expected panic when fullBytesLen is omitted") } err, ok := r.(error) if !ok { t.Fatalf("panic value must be an error, got %T: %v", r, r) } - if !strings.Contains(err.Error(), "fullBytesLen=1 is too small") { + if !strings.Contains(err.Error(), "fullBytesLen is required") { t.Fatalf("unexpected panic message: %v", err) } }() - _ = NewLocalPartyWithKDD(msg, nil, keygen.LocalPartySaveData{}, nil, nil, nil, 1) + _ = NewLocalPartyWithKDD(big.NewInt(42), nil, keygen.LocalPartySaveData{}, nil, nil, nil) +} + +func TestNewLocalPartyWithKDD_FullBytesLen_TooWide(t *testing.T) { + pIDs := tss.GenerateTestPartyIDs(1) + params := tss.NewParameters(tss.S256(), tss.NewPeerContext(pIDs), pIDs[0], 1, 0) + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for fullBytesLen wider than the curve order") + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "exceeds curve order byte length") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalPartyWithKDD(big.NewInt(1), params, keygen.LocalPartySaveData{}, nil, nil, nil, 33) } func TestFillTo32BytesInPlace(t *testing.T) { diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index 731b032ea..f4f938d4e 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -51,8 +51,8 @@ func (round *round1) Start() *tss.Error { // Fiat-Shamir transcript splicing across the runs. The caller must now // supply a per-ceremony nonce via tss.Parameters.SetSessionNonce. nonce := round.Params().SessionNonce() - if nonce == nil { - return round.WrapError(errors.New("signing requires tss.Parameters.SetSessionNonce() before Start")) + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("signing requires tss.Parameters.SetSessionNonce() before Start")) } round.temp.ssidNonce = new(big.Int).Set(nonce) ssid, err := round.getSSID() diff --git a/ecdsa/signing/rounds.go b/ecdsa/signing/rounds.go index 418b95364..82f7d6b63 100644 --- a/ecdsa/signing/rounds.go +++ b/ecdsa/signing/rounds.go @@ -144,5 +144,5 @@ func (round *base) getSSID() ([]byte, error) { ssidList = append(ssidList, round.key.H2j...) ssidList = append(ssidList, big.NewInt(int64(round.number))) ssidList = append(ssidList, round.temp.ssidNonce) - return common.SHA512_256i(ssidList...).Bytes(), nil + return common.SHA512_256i(ssidList...).FillBytes(make([]byte, 32)), nil } diff --git a/eddsa/keygen/round_1.go b/eddsa/keygen/round_1.go index 8b22b8702..ed8d0af3c 100644 --- a/eddsa/keygen/round_1.go +++ b/eddsa/keygen/round_1.go @@ -44,8 +44,8 @@ func (round *round1) Start() *tss.Error { // committees would derive the same SSID, exposing proof transcripts // to splicing between runs. nonce := round.Params().SessionNonce() - if nonce == nil { - return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) } round.temp.ssidNonce = new(big.Int).Set(nonce) ssid, err := round.getSSID() diff --git a/eddsa/keygen/round_3.go b/eddsa/keygen/round_3.go index f882b8569..8dfa37ff9 100644 --- a/eddsa/keygen/round_3.go +++ b/eddsa/keygen/round_3.go @@ -80,14 +80,13 @@ func (round *round3) Start() *tss.Error { } PjVs, err := crypto.UnFlattenECPoints(round.Params().EC(), flatPolyGs) - for i, PjV := range PjVs { - PjVs[i] = PjV.EightInvEight() - } - if err != nil { ch <- vssOut{err, nil} return } + for i, PjV := range PjVs { + PjVs[i] = PjV.EightInvEight() + } proof, err := r2msg2.UnmarshalZKProof(round.Params().EC()) if err != nil { ch <- vssOut{errors.New("failed to unmarshal schnorr proof"), nil} diff --git a/eddsa/keygen/rounds.go b/eddsa/keygen/rounds.go index f67d47aaa..33e87ecc5 100644 --- a/eddsa/keygen/rounds.go +++ b/eddsa/keygen/rounds.go @@ -91,5 +91,5 @@ func (round *base) getSSID() ([]byte, error) { ssidList = append(ssidList, round.Parties().IDs().Keys()...) ssidList = append(ssidList, big.NewInt(int64(round.number))) ssidList = append(ssidList, round.temp.ssidNonce) - return common.SHA512_256i(ssidList...).Bytes(), nil + return common.SHA512_256i(ssidList...).FillBytes(make([]byte, 32)), nil } diff --git a/eddsa/resharing/local_party_test.go b/eddsa/resharing/local_party_test.go index dd97f8560..726dd118d 100644 --- a/eddsa/resharing/local_party_test.go +++ b/eddsa/resharing/local_party_test.go @@ -174,7 +174,7 @@ signing: for j, signPID := range signPIDs { params := tss.NewParameters(tss.Edwards(), signP2pCtx, signPID, len(signPIDs), newThreshold) params.SetSessionNonce(signCeremonyNonce) - P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh).(*signing.LocalParty) + P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh, 32).(*signing.LocalParty) signParties = append(signParties, P) go func(P *signing.LocalParty) { if err := P.Start(); err != nil { @@ -225,8 +225,9 @@ signing: println("new sig error, ", err.Error()) } - ok := edwards.Verify(&pk, big.NewInt(42).Bytes(), - newSig.R, newSig.S) + msgBytes := make([]byte, 32) + big.NewInt(42).FillBytes(msgBytes) + ok := edwards.Verify(&pk, msgBytes, newSig.R, newSig.S) assert.True(t, ok, "eddsa verify must pass") t.Log("EDDSA signing test done.") diff --git a/eddsa/signing/finalize.go b/eddsa/signing/finalize.go index d383355ab..f865ae28c 100644 --- a/eddsa/signing/finalize.go +++ b/eddsa/signing/finalize.go @@ -43,13 +43,9 @@ func (round *finalization) Start() *tss.Error { round.data.Signature = append(bigIntToEncodedBytes(round.temp.r)[:], sumS[:]...) round.data.R = round.temp.r.Bytes() round.data.S = s.Bytes() - if round.temp.fullBytesLen == 0 { - round.data.M = round.temp.m.Bytes() - } else { - mBytes := make([]byte, round.temp.fullBytesLen) - round.temp.m.FillBytes(mBytes) - round.data.M = mBytes - } + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + round.data.M = mBytes pk := edwards.PublicKey{ Curve: round.Params().EC(), diff --git a/eddsa/signing/local_party.go b/eddsa/signing/local_party.go index cac058273..cf06efff0 100644 --- a/eddsa/signing/local_party.go +++ b/eddsa/signing/local_party.go @@ -66,12 +66,13 @@ type ( } ) -// NewLocalParty returns a signing party. Optional fullBytesLen, if provided, -// fixes the byte width used to encode the message in round-1 SSID derivation -// (preserving leading zero bytes). The value must be non-negative and, when -// non-zero, must be at least ceil(msg.BitLen()/8); violating either -// constraint is a caller bug and the constructor panics at the call site -// rather than later inside a protocol goroutine. +// NewLocalParty returns a signing party. fullBytesLen fixes the byte width used +// to encode the message for EdDSA lambda hashing and final verification/output +// (preserving leading zero bytes). Every signer in a ceremony must pass the +// same value. It must be positive, no larger than the curve order byte length, +// and at least ceil(msg.BitLen()/8); violating these constraints is a caller +// bug and the constructor panics at the call site rather than later inside a +// protocol goroutine. func NewLocalParty( msg *big.Int, params *tss.Parameters, @@ -80,15 +81,7 @@ func NewLocalParty( end chan<- common.SignatureData, fullBytesLen ...int, ) tss.Party { - if len(fullBytesLen) > 0 { - if fullBytesLen[0] < 0 { - panic(fmt.Errorf("NewLocalParty: fullBytesLen must be non-negative, got %d", fullBytesLen[0])) - } - if fullBytesLen[0] > 0 && msg != nil && msg.BitLen() > 8*fullBytesLen[0] { - panic(fmt.Errorf("NewLocalParty: fullBytesLen=%d is too small for a %d-bit message (need at least %d bytes)", - fullBytesLen[0], msg.BitLen(), (msg.BitLen()+7)/8)) - } - } + validatedFullBytesLen := validateFullBytesLen("NewLocalParty", msg, params, fullBytesLen) partyCount := len(params.Parties().IDs()) p := &LocalParty{ @@ -107,13 +100,33 @@ func NewLocalParty( // temp data init p.temp.m = msg - if len(fullBytesLen) > 0 { - p.temp.fullBytesLen = fullBytesLen[0] - } + p.temp.fullBytesLen = validatedFullBytesLen p.temp.cjs = make([]*big.Int, partyCount) return p } +func validateFullBytesLen(caller string, msg *big.Int, params *tss.Parameters, fullBytesLen []int) int { + if len(fullBytesLen) != 1 { + panic(fmt.Errorf("%s: fullBytesLen is required and must match all signing parties", caller)) + } + length := fullBytesLen[0] + if length <= 0 { + panic(fmt.Errorf("%s: fullBytesLen must be positive, got %d", caller, length)) + } + if msg != nil && msg.BitLen() > 8*length { + panic(fmt.Errorf("%s: fullBytesLen=%d is too small for a %d-bit message (need at least %d bytes)", + caller, length, msg.BitLen(), (msg.BitLen()+7)/8)) + } + if params == nil || params.EC() == nil || params.EC().Params() == nil || params.EC().Params().N == nil { + panic(fmt.Errorf("%s: params with a curve order is required to validate fullBytesLen", caller)) + } + orderBytes := (params.EC().Params().N.BitLen() + 7) / 8 + if length > orderBytes { + panic(fmt.Errorf("%s: fullBytesLen=%d exceeds curve order byte length %d", caller, length, orderBytes)) + } + return length +} + func (p *LocalParty) FirstRound() tss.Round { return newRound1(p.params, &p.keys, &p.data, &p.temp, p.out, p.end) } diff --git a/eddsa/signing/local_party_test.go b/eddsa/signing/local_party_test.go index 2c91ee58e..8b2757052 100644 --- a/eddsa/signing/local_party_test.go +++ b/eddsa/signing/local_party_test.go @@ -168,7 +168,7 @@ func TestSigning_Start_RequiresSessionNonce(t *testing.T) { params := tss.NewParameters(tss.Edwards(), p2pCtx, signPIDs[0], len(signPIDs), testThreshold) // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. - P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh).(*LocalParty) + P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh, 32).(*LocalParty) tssErr := P.Start() if tssErr == nil { t.Fatal("Start must return an error without SessionNonce") @@ -178,46 +178,86 @@ func TestSigning_Start_RequiresSessionNonce(t *testing.T) { } } -// TestNewLocalParty_FullBytesLen_Negative pins constructor-side validation +// TestNewLocalParty_FullBytesLen_NonPositive pins constructor-side validation // for fullBytesLen. Previously, a negative fullBytesLen propagated to the // round-1/round-3 code path where `make([]byte, fullBytesLen)` panicked // inside a protocol goroutine, bypassing tss.Error reporting. The // constructor now panics synchronously at the caller's call site. -func TestNewLocalParty_FullBytesLen_Negative(t *testing.T) { +func TestNewLocalParty_FullBytesLen_NonPositive(t *testing.T) { msg := big.NewInt(1) + for _, length := range []int{-1, 0} { + func() { + defer func() { + r := recover() + if r == nil { + t.Fatalf("expected panic for fullBytesLen=%d", length) + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "fullBytesLen must be positive") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalParty(msg, nil, keygen.LocalPartySaveData{}, nil, nil, length) + }() + } +} + +// TestNewLocalParty_FullBytesLen_TooSmall pins that a fullBytesLen smaller +// than the message's byte width is rejected at the constructor rather than +// later inside (*big.Int).FillBytes (which would panic inside a goroutine). +func TestNewLocalParty_FullBytesLen_TooSmall(t *testing.T) { + msg := big.NewInt(0xABCD) // 16-bit, needs at least 2 bytes defer func() { r := recover() if r == nil { - t.Fatal("expected panic for negative fullBytesLen") + t.Fatal("expected panic for fullBytesLen smaller than msg byte width") } err, ok := r.(error) if !ok { t.Fatalf("panic value must be an error, got %T: %v", r, r) } - if !strings.Contains(err.Error(), "fullBytesLen must be non-negative") { + if !strings.Contains(err.Error(), "fullBytesLen=1 is too small") { t.Fatalf("unexpected panic message: %v", err) } }() - _ = NewLocalParty(msg, nil, keygen.LocalPartySaveData{}, nil, nil, -1) + _ = NewLocalParty(msg, nil, keygen.LocalPartySaveData{}, nil, nil, 1) } -// TestNewLocalParty_FullBytesLen_TooSmall pins that a fullBytesLen smaller -// than the message's byte width is rejected at the constructor rather than -// later inside (*big.Int).FillBytes (which would panic inside a goroutine). -func TestNewLocalParty_FullBytesLen_TooSmall(t *testing.T) { - msg := big.NewInt(0xABCD) // 16-bit, needs at least 2 bytes +func TestNewLocalParty_FullBytesLen_Required(t *testing.T) { defer func() { r := recover() if r == nil { - t.Fatal("expected panic for fullBytesLen smaller than msg byte width") + t.Fatal("expected panic when fullBytesLen is omitted") } err, ok := r.(error) if !ok { t.Fatalf("panic value must be an error, got %T: %v", r, r) } - if !strings.Contains(err.Error(), "fullBytesLen=1 is too small") { + if !strings.Contains(err.Error(), "fullBytesLen is required") { t.Fatalf("unexpected panic message: %v", err) } }() - _ = NewLocalParty(msg, nil, keygen.LocalPartySaveData{}, nil, nil, 1) + _ = NewLocalParty(big.NewInt(42), nil, keygen.LocalPartySaveData{}, nil, nil) +} + +func TestNewLocalParty_FullBytesLen_TooWide(t *testing.T) { + pIDs := tss.GenerateTestPartyIDs(1) + params := tss.NewParameters(tss.Edwards(), tss.NewPeerContext(pIDs), pIDs[0], 1, 0) + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for fullBytesLen wider than the curve order") + } + err, ok := r.(error) + if !ok { + t.Fatalf("panic value must be an error, got %T: %v", r, r) + } + if !strings.Contains(err.Error(), "exceeds curve order byte length") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = NewLocalParty(big.NewInt(1), params, keygen.LocalPartySaveData{}, nil, nil, 33) } diff --git a/eddsa/signing/round_1.go b/eddsa/signing/round_1.go index 75d5c3dc7..5df6c3397 100644 --- a/eddsa/signing/round_1.go +++ b/eddsa/signing/round_1.go @@ -39,8 +39,8 @@ func (round *round1) Start() *tss.Error { // Fiat-Shamir transcript splicing across the runs. The caller must now // supply a per-ceremony nonce via tss.Parameters.SetSessionNonce. nonce := round.Params().SessionNonce() - if nonce == nil { - return round.WrapError(errors.New("signing requires tss.Parameters.SetSessionNonce() before Start")) + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("signing requires tss.Parameters.SetSessionNonce() before Start")) } round.temp.ssidNonce = new(big.Int).Set(nonce) ssid, err := round.getSSID() diff --git a/eddsa/signing/round_3.go b/eddsa/signing/round_3.go index fd0d1b5c2..a5d7d3e39 100644 --- a/eddsa/signing/round_3.go +++ b/eddsa/signing/round_3.go @@ -79,13 +79,9 @@ func (round *round3) Start() *tss.Error { h.Reset() h.Write(encodedR[:]) h.Write(encodedPubKey[:]) - if round.temp.fullBytesLen == 0 { - h.Write(round.temp.m.Bytes()) - } else { - mBytes := make([]byte, round.temp.fullBytesLen) - round.temp.m.FillBytes(mBytes) - h.Write(mBytes) - } + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + h.Write(mBytes) var lambda [64]byte h.Sum(lambda[:0]) diff --git a/eddsa/signing/rounds.go b/eddsa/signing/rounds.go index e5b38adda..5917ff10a 100644 --- a/eddsa/signing/rounds.go +++ b/eddsa/signing/rounds.go @@ -112,5 +112,5 @@ func (round *base) getSSID() ([]byte, error) { ssidList = append(ssidList, bigXjList...) ssidList = append(ssidList, big.NewInt(int64(round.number))) ssidList = append(ssidList, round.temp.ssidNonce) - return common.SHA512_256i(ssidList...).Bytes(), nil + return common.SHA512_256i(ssidList...).FillBytes(make([]byte, 32)), nil } diff --git a/tss/params.go b/tss/params.go index 38f9e94fd..1d8612e75 100644 --- a/tss/params.go +++ b/tss/params.go @@ -24,9 +24,9 @@ type ( threshold int concurrency int safePrimeGenTimeout time.Duration - // sessionNonce provides per-session SSID uniqueness for GG20 - // proof binding. Signing falls back to the message hash; keygen - // and resharing require callers to coordinate a shared nonce. + // sessionNonce provides per-session SSID uniqueness for GG20 proof + // binding. Keygen, signing, and resharing require callers to coordinate + // a shared positive nonce before Start. sessionNonce *big.Int } @@ -106,22 +106,23 @@ func (params *Parameters) SessionNonce() *big.Int { // the same SSID, breaking the session-binding property that the proofs rely // on. The caller must supply a per-ceremony unique nonce; reusing the same // nonce across distinct ceremonies on the same inputs reintroduces -// transcript-splicing risk. +// transcript-splicing risk. Set the nonce before Start on the same goroutine +// that constructs the party; do not mutate Parameters concurrently with a +// running protocol. func (params *Parameters) SetSessionNonce(nonce *big.Int) { - if nonce == nil { - params.sessionNonce = nil - return + if nonce == nil || nonce.Sign() <= 0 { + panic("tss: session nonce must be positive") } params.sessionNonce = new(big.Int).Set(nonce) } // SetSessionNonceBytes hashes an application-level session ID into the -// per-session nonce. All parties must call it with the same non-empty session ID -// before constructing local parties for a protocol run. It panics if the -// session ID is empty. +// per-session nonce. All parties must call it with the same high-entropy +// session ID before constructing local parties for a protocol run. It panics if +// the session ID is shorter than 16 bytes. func (params *Parameters) SetSessionNonceBytes(sessionID []byte) { - if len(sessionID) == 0 { - panic("tss: session ID must be non-empty") + if len(sessionID) < 16 { + panic("tss: session ID must be at least 16 bytes") } params.SetSessionNonce(new(big.Int).SetBytes(common.SHA512_256(sessionID))) } diff --git a/tss/params_test.go b/tss/params_test.go index f3c20b2fe..358fce167 100644 --- a/tss/params_test.go +++ b/tss/params_test.go @@ -29,7 +29,7 @@ func TestSetSessionNonceCopiesInput(t *testing.T) { func TestSetSessionNonceBytesHashesSessionID(t *testing.T) { pIDs := GenerateTestPartyIDs(1) params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], 1, 0) - sessionID := []byte("session-1") + sessionID := []byte("session-1-with-128-bits") params.SetSessionNonceBytes(sessionID) @@ -37,7 +37,7 @@ func TestSetSessionNonceBytesHashesSessionID(t *testing.T) { assert.Equal(t, expected, params.SessionNonce()) } -func TestSetSessionNonceBytesRejectsEmptySessionID(t *testing.T) { +func TestSetSessionNonceBytesRejectsShortSessionID(t *testing.T) { pIDs := GenerateTestPartyIDs(1) params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], 1, 0) @@ -47,4 +47,22 @@ func TestSetSessionNonceBytesRejectsEmptySessionID(t *testing.T) { assert.Panics(t, func() { params.SetSessionNonceBytes([]byte{}) }) + assert.Panics(t, func() { + params.SetSessionNonceBytes([]byte("short-session")) + }) +} + +func TestSetSessionNonceRejectsNonPositiveNonce(t *testing.T) { + pIDs := GenerateTestPartyIDs(1) + params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], 1, 0) + + assert.Panics(t, func() { + params.SetSessionNonce(nil) + }) + assert.Panics(t, func() { + params.SetSessionNonce(big.NewInt(0)) + }) + assert.Panics(t, func() { + params.SetSessionNonce(big.NewInt(-1)) + }) } From caa64fcaed29ea46514e5a3f8fe5ed5d75917613 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 20 May 2026 13:33:04 -0500 Subject: [PATCH 18/24] Expand FactorProof invalid-base coverage --- crypto/paillier/factor_proof_test.go | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index 20f670e80..c9d6924f0 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -160,6 +160,75 @@ func TestFactorProofVerifyRejectsNonInvertibleBase(t *testing.T) { }) } +func TestFactorProofVerifyRejectsNonZeroInvalidBases(t *testing.T) { + facSetUp(t) + + cases := []struct { + name string + verify func(proof *FactorProof) (bool, error) + }{ + { + name: "verifier s", + verify: func(proof *FactorProof) (bool, error) { + return proof.FactorVerify(publicKey.N, auxPrime.N, new(big.Int).Set(auxPrime.N), tt) + }, + }, + { + name: "verifier t", + verify: func(proof *FactorProof) (bool, error) { + return proof.FactorVerify(publicKey.N, auxPrime.N, s, new(big.Int).Set(auxPrime.N)) + }, + }, + { + name: "proof P", + verify: func(proof *FactorProof) (bool, error) { + proof.P = new(big.Int).Set(auxPrime.N) + return proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + }, + }, + { + name: "proof Q", + verify: func(proof *FactorProof) (bool, error) { + proof.Q = new(big.Int).Set(auxPrime.N) + return proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + }, + }, + { + name: "proof A", + verify: func(proof *FactorProof) (bool, error) { + proof.A = new(big.Int).Set(auxPrime.N) + return proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + }, + }, + { + name: "proof B", + verify: func(proof *FactorProof) (bool, error) { + proof.B = new(big.Int).Set(auxPrime.N) + return proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + }, + }, + { + name: "proof T", + verify: func(proof *FactorProof) (bool, error) { + proof.T = new(big.Int).Set(auxPrime.N) + return proof.FactorVerify(publicKey.N, auxPrime.N, s, tt) + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + proof := privateKey.FactorProof(auxPrime.N, s, tt) + + assert.NotPanics(t, func() { + res, err := test.verify(proof) + 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) From d4555c803b48df5f5fd20934a6eb359274efe5f6 Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 21 May 2026 10:42:17 -0500 Subject: [PATCH 19/24] Validate resharing modulus widths --- ecdsa/resharing/messages.go | 8 ++++ ecdsa/resharing/messages_test.go | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/ecdsa/resharing/messages.go b/ecdsa/resharing/messages.go index e2f1a8020..b17a3bf93 100644 --- a/ecdsa/resharing/messages.go +++ b/ecdsa/resharing/messages.go @@ -32,6 +32,8 @@ var ( } ) +const paillierBitsLen = 2048 + // ----- // func NewDGRound1Message( @@ -132,6 +134,8 @@ func (m *DGRound2Message1) ValidateBasic() bool { common.NonEmptyMultiBytes(m.PaillierProof) && common.NonEmptyBytes(m.PaillierN) && common.NonEmptyBytes(m.NTilde) && + hasBitLen(m.PaillierN, paillierBitsLen) && + hasBitLen(m.NTilde, paillierBitsLen) && common.NonEmptyBytes(m.H1) && common.NonEmptyBytes(m.H2) && m.GetDlnproof_1().ValidateBasic() && @@ -140,6 +144,10 @@ func (m *DGRound2Message1) ValidateBasic() bool { m.GetModproofTilde().ValidateBasic() } +func hasBitLen(value []byte, bits int) bool { + return new(big.Int).SetBytes(value).BitLen() == bits +} + func (m *DGRound2Message1) UnmarshalPaillierPK() *paillier.PublicKey { return &paillier.PublicKey{ N: new(big.Int).SetBytes(m.PaillierN), diff --git a/ecdsa/resharing/messages_test.go b/ecdsa/resharing/messages_test.go index 7dd807561..9f089d3ff 100644 --- a/ecdsa/resharing/messages_test.go +++ b/ecdsa/resharing/messages_test.go @@ -12,6 +12,8 @@ import ( "testing" "github.com/bnb-chain/tss-lib/crypto" + "github.com/bnb-chain/tss-lib/crypto/dlnproof" + "github.com/bnb-chain/tss-lib/crypto/paillier" "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/bnb-chain/tss-lib/tss" ) @@ -65,6 +67,82 @@ func TestDGRound1Message_ValidateBasic_RequiresSsid(t *testing.T) { } } +func TestDGRound2Message1ValidateBasicRequiresExactModulusWidth(t *testing.T) { + msg := validDGRound2Message1ForValidation() + if !msg.ValidateBasic() { + t.Fatal("expected baseline message to validate") + } + + msg = validDGRound2Message1ForValidation() + msg.PaillierN = big.NewInt(1).Bytes() + if msg.ValidateBasic() { + t.Fatal("expected sub-2048-bit Paillier modulus to fail validation") + } + + msg = validDGRound2Message1ForValidation() + msg.NTilde = big.NewInt(1).Bytes() + if msg.ValidateBasic() { + t.Fatal("expected sub-2048-bit NTilde modulus to fail validation") + } + + msg = validDGRound2Message1ForValidation() + 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 = validDGRound2Message1ForValidation() + 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") + } +} + +func validDGRound2Message1ForValidation() *DGRound2Message1 { + largeModulus := new(big.Int).Lsh(big.NewInt(1), paillierBitsLen-1).Bytes() + modProof := validDGRound2ModProofForValidation() + + return &DGRound2Message1{ + PaillierProof: [][]byte{{0x01}}, + PaillierN: largeModulus, + NTilde: largeModulus, + H1: []byte{0x02}, + H2: []byte{0x03}, + Dlnproof_1: validDGRound2DLNProofForValidation(), + Dlnproof_2: validDGRound2DLNProofForValidation(), + Modproof: modProof, + ModproofTilde: modProof, + } +} + +func validDGRound2DLNProofForValidation() *DGRound2Message1_DLNProof { + alpha := make([][]byte, dlnproof.Iterations) + tValues := make([][]byte, dlnproof.Iterations) + for i := range alpha { + alpha[i] = []byte{0x01} + tValues[i] = []byte{0x02} + } + + return &DGRound2Message1_DLNProof{Alpha: alpha, T: tValues} +} + +func validDGRound2ModProofForValidation() *DGRound2Message1_ModProof { + xValues := make([][]byte, paillier.PARAM_M) + zValues := make([][]byte, paillier.PARAM_M) + for i := range xValues { + xValues[i] = []byte{0x01} + zValues[i] = []byte{0x02} + } + + return &DGRound2Message1_ModProof{ + W: []byte{0x01}, + X: xValues, + A: make([]bool, paillier.PARAM_M), + B: make([]bool, paillier.PARAM_M), + Z: zValues, + } +} + // TestRound1Update_RejectsMismatchedSsidBeforePartyZero pins that every old // committee broadcast is SSID-checked before being marked accepted. In // particular, old party j>0 may arrive before old party 0; that ordering must From f2a959a4afcc2ebebe7db27e2b0817338cabf78d Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 21 May 2026 11:39:52 -0500 Subject: [PATCH 20/24] Stabilize malformed factor proof fixture --- crypto/paillier/factor_proof_test.go | 32 +++++++--------------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/crypto/paillier/factor_proof_test.go b/crypto/paillier/factor_proof_test.go index c9d6924f0..5ff8cc281 100644 --- a/crypto/paillier/factor_proof_test.go +++ b/crypto/paillier/factor_proof_test.go @@ -3,7 +3,6 @@ package paillier import ( "context" "math/big" - "runtime" "testing" "time" @@ -47,33 +46,18 @@ func facSetUp(t *testing.T) { tt = new(big.Int).Mod(new(big.Int).Mul(r, r), N) s = new(big.Int).Exp(tt, lambda, N) - var err3 error - badPrivateKey, badPublicKey, err3 = GenerateBadKeyPair(ctx, testPaillierKeyLength) - if err3 != nil { - t.Fatalf("failed to generate malformed Paillier key pair: %v", err3) - } + badPrivateKey, badPublicKey = GenerateBadKeyPair() } -func GenerateBadKeyPair(ctx context.Context, modulusBitLen int) (privateKey *PrivateKey, publicKey *PublicKey, err error) { - var concurrency int - concurrency = runtime.NumCPU() +func GenerateBadKeyPair() (privateKey *PrivateKey, publicKey *PublicKey) { one := big.NewInt(1) - // KS-BTL-F-03: use two safe primes for P, Q - var P, Q, N *big.Int - { - tmp := new(big.Int) - sgpsLong, err := common.GetRandomSafePrimesConcurrent(ctx, modulusBitLen-128, 1, concurrency) - if err != nil { - return nil, nil, err - } - sgpsShort, err := common.GetRandomSafePrimesConcurrent(ctx, 128, 1, concurrency) - if err != nil { - return nil, nil, err - } - P, Q = sgpsLong[0].SafePrime(), sgpsShort[0].SafePrime() - N = tmp.Mul(P, Q) - } + // Use fixed odd factors with a 1792-bit size gap. The factor proof must + // reject this malformed Paillier modulus, and the fixture should not spend + // CI time searching for random safe primes just to construct bad inputs. + P := new(big.Int).Sub(new(big.Int).Lsh(one, 1920), big.NewInt(133)) + Q := new(big.Int).Sub(new(big.Int).Lsh(one, 128), big.NewInt(159)) + N := new(big.Int).Mul(P, Q) // phiN = P-1 * Q-1 PMinus1, QMinus1 := new(big.Int).Sub(P, one), new(big.Int).Sub(Q, one) From 8c235ef8566626ef07a4b955a32f1c3802a19ca8 Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 21 May 2026 10:00:39 -0500 Subject: [PATCH 21/24] Stabilize EdDSA keygen scalar test encoding --- eddsa/keygen/local_party_test.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/eddsa/keygen/local_party_test.go b/eddsa/keygen/local_party_test.go index 45f1641dc..d499ec7eb 100644 --- a/eddsa/keygen/local_party_test.go +++ b/eddsa/keygen/local_party_test.go @@ -186,8 +186,7 @@ keygen: u = new(big.Int).Add(u, uj) } u = new(big.Int).Mod(u, tss.Edwards().Params().N) - scalar := make([]byte, 0, 32) - copy(scalar, u.Bytes()) + scalar := u.FillBytes(make([]byte, 32)) // build eddsa key pair pkX, pkY := save.EDDSAPub.X(), save.EDDSAPub.Y() @@ -196,8 +195,7 @@ keygen: X: pkX, Y: pkY, } - println("u len: ", len(u.Bytes())) - sk, _, err := edwards.PrivKeyFromScalar(u.Bytes()) + sk, _, err := edwards.PrivKeyFromScalar(scalar) if !assert.NoError(t, err) { return } @@ -207,7 +205,7 @@ keygen: // public key tests assert.NotZero(t, u, "u should not be zero") - ourPkX, ourPkY := tss.Edwards().ScalarBaseMult(u.Bytes()) + ourPkX, ourPkY := tss.Edwards().ScalarBaseMult(scalar) assert.Equal(t, pkX, ourPkX, "pkX should match expected pk derived from u") assert.Equal(t, pkY, ourPkY, "pkY should match expected pk derived from u") t.Log("Public key tests done.") From 56e25daabed696d2d7317297a1d00a599c0ea21b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Sat, 23 May 2026 10:28:28 +0000 Subject: [PATCH 22/24] Document SetSessionNonceBytes entropy requirement --- tss/params.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tss/params.go b/tss/params.go index 1d8612e75..d961c264e 100644 --- a/tss/params.go +++ b/tss/params.go @@ -120,6 +120,14 @@ func (params *Parameters) SetSessionNonce(nonce *big.Int) { // per-session nonce. All parties must call it with the same high-entropy // session ID before constructing local parties for a protocol run. It panics if // the session ID is shorter than 16 bytes. +// +// The 16-byte minimum is a floor that catches obvious misuse (empty input, a +// short ASCII tag); it is not a sufficient condition. Callers must supply at +// least 128 bits of true randomness. A counter, timestamp, or other +// low-entropy 16-byte value passes the length check but defeats the +// session-binding property that the proofs rely on. Prefer a freshly drawn +// random session ID from a CSPRNG, or a high-entropy ceremony identifier +// negotiated out of band. func (params *Parameters) SetSessionNonceBytes(sessionID []byte) { if len(sessionID) < 16 { panic("tss: session ID must be at least 16 bytes") From 16d848b8509b4311fea07f92507e8d5082ff9297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Sat, 23 May 2026 10:50:14 +0000 Subject: [PATCH 23/24] Document range proof acceptance of zero contribution --- crypto/mta/range_proof_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crypto/mta/range_proof_test.go b/crypto/mta/range_proof_test.go index 1c41f263d..92add050f 100644 --- a/crypto/mta/range_proof_test.go +++ b/crypto/mta/range_proof_test.go @@ -175,3 +175,31 @@ func TestRangeProofAliceRejectsMalformedInputs(t *testing.T) { badZ.Z = big.NewInt(1) assert.False(t, badZ.Verify(tss.EC(), pk, NTildei, h1i, h2i, c), "Z equal to one must fail") } + +// TestRangeProofAliceAcceptsZeroContribution codifies that the range proof +// intentionally accepts c=1 with r=1 and m=0. BNB upstream flags c=1 as a +// "bypass" in TestProveRangeAliceBypassed via a println, but tracing c=1 +// through BobMid (share_protocol.go) gives alpha+beta = 0 mod q, identical +// to an honest a=0 contribution. The proof accepts m=0 because GG18 bounds +// s1 < q^3, and signing tolerates one peer contributing zero because +// k = sum(k_i) stays unpredictable from other parties' randomness. We do +// not reject c=1 in Verify because it is not a verifier-side bug. +func TestRangeProofAliceAcceptsZeroContribution(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + _, pk, err := paillier.GenerateKeyPair(ctx, testPaillierKeyLength) + assert.NoError(t, err) + + primes := [2]*big.Int{common.GetRandomPrimeInt(testSafePrimeBits), common.GetRandomPrimeInt(testSafePrimeBits)} + NTildei, h1i, h2i, err := crypto.GenerateNTildei(primes) + assert.NoError(t, err) + + mZero := big.NewInt(0) + rOne := big.NewInt(1) + cOne := big.NewInt(1) + proof, err := ProveRangeAlice(tss.EC(), pk, cOne, NTildei, h1i, h2i, mZero, rOne) + assert.NoError(t, err) + assert.True(t, proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, cOne), + "c=1 with r=1, m=0 verifies because it is honest zero contribution; see test docstring") +} From 630a7e57a5a1d29352c53ce86575bc51375792ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?= Date: Sat, 23 May 2026 10:52:36 +0000 Subject: [PATCH 24/24] Drop misleading legacy framing from ModChallenge docs --- crypto/paillier/mod_proof.go | 2 +- crypto/paillier/mod_proof_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go index b62557d3d..f39c48c3f 100644 --- a/crypto/paillier/mod_proof.go +++ b/crypto/paillier/mod_proof.go @@ -130,7 +130,7 @@ func (pf ModProof) ModVerify(N *big.Int, session ...[]byte) (bool, error) { // N.BitLen() + 256 bits of entropy before reducing mod N. Reducing a single // 256-bit SHA512_256i_TAGGED output mod ~2^2048 would emit challenges in // [0, 2^256) instead of [0, N), giving the session-tagged path a strictly -// weaker challenge distribution than the legacy HashToN path it shares the +// weaker challenge distribution than the HashToN path it shares the // verifier with. Each iteration also chains the previously-derived challenges // (y[:i]) into the hash inputs, preserving the sequential-challenge property of // the original session-tagged construction. diff --git a/crypto/paillier/mod_proof_test.go b/crypto/paillier/mod_proof_test.go index bf83f87f1..4596c58af 100644 --- a/crypto/paillier/mod_proof_test.go +++ b/crypto/paillier/mod_proof_test.go @@ -62,7 +62,7 @@ func TestModChallengeRejectsEmptySessionTag(t *testing.T) { // from a single SHA512_256i_TAGGED → Mod N reduction against a 2048-bit // Paillier N. A regression to truncation here would weaken the // session-tagged proof to a strictly smaller challenge space than the -// legacy HashToN path it shares the verifier with. +// HashToN path it shares the verifier with. func TestModChallenge_SessionPath_NotTruncated(t *testing.T) { modSetUp(t) N := publicKey.N