diff --git a/README.md b/README.md index e835e4c5..02a50319 100644 --- a/README.md +++ b/README.md @@ -126,15 +126,24 @@ 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 or signing 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: +The proof-transcript mode must be selected explicitly before constructing an +ECDSA keygen or signing party. New ceremonies should use the session-bound +security-v2 mode and bind the same session ID into the protocol parameters: ```go params := tss.NewParameters(curve, ctx, thisParty, len(parties), threshold) +params.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonceBytes([]byte(sessionID)) ``` 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 and signing fail closed if no session nonce is set; reusing a session ID across otherwise identical ceremonies reintroduces transcript-splicing risk. +`ProtocolModeLegacy` exists only for coordinated compatibility with peers that +use the historical untagged GG20 transcript. A ceremony must be homogeneous: +legacy and security-v2 parties cannot interoperate. The selected mode is frozen +when the local party is constructed and cannot change while the protocol is in +flight. + 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`. diff --git a/crypto/dlnproof/proof.go b/crypto/dlnproof/proof.go index 14215214..5cf15e69 100644 --- a/crypto/dlnproof/proof.go +++ b/crypto/dlnproof/proof.go @@ -50,7 +50,7 @@ func NewDLNProof(h1, h2, x, p, q, N *big.Int, session ...[]byte) *Proof { alpha[i] = modN.Exp(h1, a[i]) } msg := append([]*big.Int{h1, h2, N}, alpha[:]...) - c := common.SHA512_256i_TAGGED(fsSessionDLNProof(Session), msg...) + c := proofChallenge(Session, msg...) t := [Iterations]*big.Int{} cIBI := new(big.Int) for i := range t { @@ -87,7 +87,7 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { } } msg := append([]*big.Int{h1, h2, N}, p.Alpha[:]...) - c := common.SHA512_256i_TAGGED(fsSessionDLNProof(Session), msg...) + c := proofChallenge(Session, msg...) cIBI := new(big.Int) for i := 0; i < Iterations; i++ { cI := c.Bit(i) @@ -102,6 +102,13 @@ func (p *Proof) Verify(h1, h2, N *big.Int, session ...[]byte) bool { return true } +func proofChallenge(session []byte, values ...*big.Int) *big.Int { + if session == nil { + return common.SHA512_256i(values...) + } + return common.SHA512_256i_TAGGED(fsSessionDLNProof(session), values...) +} + func optionalSession(session [][]byte) []byte { if len(session) == 0 { return nil diff --git a/crypto/dlnproof/proof_test.go b/crypto/dlnproof/proof_test.go index 576f16f7..9b9243b6 100644 --- a/crypto/dlnproof/proof_test.go +++ b/crypto/dlnproof/proof_test.go @@ -9,8 +9,25 @@ package dlnproof import ( "math/big" "testing" + + "github.com/bnb-chain/tss-lib/common" ) +func TestLegacyChallengeMatchesHistoricalTranscript(t *testing.T) { + values := []*big.Int{ + big.NewInt(2), + big.NewInt(3), + big.NewInt(5), + big.NewInt(7), + } + + expected := common.SHA512_256i(values...) + actual := proofChallenge(nil, values...) + if expected.Cmp(actual) != 0 { + t.Fatalf("legacy challenge changed: expected %v, got %v", expected, actual) + } +} + func TestDLNProofRejectsEmptySessionTag(t *testing.T) { assertPanics(t, func() { _ = NewDLNProof(nil, nil, nil, nil, nil, nil, []byte{}) diff --git a/crypto/mta/proofs.go b/crypto/mta/proofs.go index 6536339d..ce3ea6ff 100644 --- a/crypto/mta/proofs.go +++ b/crypto/mta/proofs.go @@ -97,17 +97,23 @@ func ProveBobWC(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, h1, h2, c1, c w = modNTilde.Mul(w, modNTilde.Exp(h2, tau)) // 11-12. e' - var e *big.Int - { // derive the Fiat-Shamir challenge by reducing the hash mod q - var eHash *big.Int - // X is nil if called by ProveBob (Bob's proof "without check") - if X == nil { - eHash = common.SHA512_256i_TAGGED(fsSessionBob(Session), append(pk.AsInts(), NTilde, h1, h2, c1, c2, z, zPrm, t, v, w)...) - } else { - eHash = common.SHA512_256i_TAGGED(fsSessionBobWC(Session), append(pk.AsInts(), NTilde, h1, h2, X.X(), X.Y(), c1, c2, u.X(), u.Y(), z, zPrm, t, v, w)...) - } - e = common.ModReduceHash(q, eHash) - } + e := bobProofChallenge( + Session, + q, + pk, + NTilde, + h1, + h2, + c1, + c2, + X, + u, + z, + zPrm, + t, + v, + w, + ) // 13. modN := common.ModInt(pk.N) @@ -292,23 +298,31 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, } // 1-2. e' - var e *big.Int - { // derive the Fiat-Shamir challenge by reducing the hash mod q - var eHash *big.Int - // X is nil if called on a ProveBob (Bob's proof "without check") - if X == nil { - eHash = common.SHA512_256i_TAGGED(fsSessionBob(Session), append(pk.AsInts(), NTilde, h1, h2, c1, c2, pf.Z, pf.ZPrm, pf.T, pf.V, pf.W)...) - } else { - if !X.ValidateBasic() || !crypto.SameCurve(ec, X.Curve()) { - return false - } - if !pf.U.ValidateBasic() || !crypto.SameCurve(ec, pf.U.Curve()) { - return false - } - eHash = common.SHA512_256i_TAGGED(fsSessionBobWC(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)...) + if X != nil { + if !X.ValidateBasic() || !crypto.SameCurve(ec, X.Curve()) { + return false + } + if !pf.U.ValidateBasic() || !crypto.SameCurve(ec, pf.U.Curve()) { + return false } - e = common.ModReduceHash(q, eHash) } + e := bobProofChallenge( + Session, + q, + pk, + NTilde, + h1, + h2, + c1, + c2, + X, + pf.U, + pf.Z, + pf.ZPrm, + pf.T, + pf.V, + pf.W, + ) if e.Sign() == 0 { return false } @@ -372,6 +386,83 @@ func (pf *ProofBobWC) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTilde, return true } +func bobProofChallenge( + session []byte, + q *big.Int, + pk *paillier.PublicKey, + nTilde, h1, h2, c1, c2 *big.Int, + x, u *crypto.ECPoint, + z, zPrime, t, v, w *big.Int, +) *big.Int { + if session == nil { + if x == nil { + return common.HashToN( + q, + append(pk.AsInts(), c1, c2, z, zPrime, t, v, w)..., + ) + } + return common.HashToN( + q, + append( + pk.AsInts(), + x.X(), + x.Y(), + c1, + c2, + u.X(), + u.Y(), + z, + zPrime, + t, + v, + w, + )..., + ) + } + + if x == nil { + challengeHash := common.SHA512_256i_TAGGED( + fsSessionBob(session), + append( + pk.AsInts(), + nTilde, + h1, + h2, + c1, + c2, + z, + zPrime, + t, + v, + w, + )..., + ) + return common.ModReduceHash(q, challengeHash) + } + + challengeHash := common.SHA512_256i_TAGGED( + fsSessionBobWC(session), + append( + pk.AsInts(), + nTilde, + h1, + h2, + x.X(), + x.Y(), + c1, + c2, + u.X(), + u.Y(), + z, + zPrime, + t, + v, + w, + )..., + ) + return common.ModReduceHash(q, challengeHash) +} + // 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, session ...[]byte) bool { if pf == nil { diff --git a/crypto/mta/range_proof.go b/crypto/mta/range_proof.go index 6598787c..a8718921 100644 --- a/crypto/mta/range_proof.go +++ b/crypto/mta/range_proof.go @@ -86,8 +86,7 @@ func ProveRangeAlice(ec elliptic.Curve, pk *paillier.PublicKey, c, NTilde, h1, h w = modNTilde.Mul(w, modNTilde.Exp(h2, gamma)) // 8-9. e' - eHash := common.SHA512_256i_TAGGED(fsSessionRangeAlice(Session), append(pk.AsInts(), NTilde, h1, h2, c, z, u, w)...) - e := common.ModReduceHash(q, eHash) + e := rangeProofChallenge(Session, q, pk, NTilde, h1, h2, c, z, u, w) modN := common.ModInt(pk.N) s := modN.Exp(r, e) @@ -196,8 +195,18 @@ func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTi } // 1-2. e' - eHash := common.SHA512_256i_TAGGED(fsSessionRangeAlice(Session), append(pk.AsInts(), NTilde, h1, h2, c, pf.Z, pf.U, pf.W)...) - e := common.ModReduceHash(q, eHash) + e := rangeProofChallenge( + Session, + q, + pk, + NTilde, + h1, + h2, + c, + pf.Z, + pf.U, + pf.W, + ) if e.Sign() == 0 { return false } @@ -235,6 +244,25 @@ func (pf *RangeProofAlice) Verify(ec elliptic.Curve, pk *paillier.PublicKey, NTi return true } +func rangeProofChallenge( + session []byte, + q *big.Int, + pk *paillier.PublicKey, + nTilde, h1, h2, c, z, u, w *big.Int, +) *big.Int { + if session == nil { + // Historical GG20 transcript. The auxiliary modulus and generators + // were not included in the challenge input. + return common.HashToN(q, append(pk.AsInts(), c, z, u, w)...) + } + + challengeHash := common.SHA512_256i_TAGGED( + fsSessionRangeAlice(session), + append(pk.AsInts(), nTilde, h1, h2, c, z, u, w)..., + ) + return common.ModReduceHash(q, challengeHash) +} + func (pf *RangeProofAlice) ValidateBasic() bool { return pf.Z != nil && pf.U != nil && diff --git a/crypto/mta/range_proof_test.go b/crypto/mta/range_proof_test.go index 92add050..885e4716 100644 --- a/crypto/mta/range_proof_test.go +++ b/crypto/mta/range_proof_test.go @@ -31,6 +31,31 @@ func TestProofSessionRejectsEmptyTag(t *testing.T) { }) } +func TestLegacyRangeChallengeMatchesHistoricalTranscript(t *testing.T) { + q := tss.EC().Params().N + pk := &paillier.PublicKey{N: big.NewInt(17)} + c := big.NewInt(19) + z := big.NewInt(23) + u := big.NewInt(29) + w := big.NewInt(31) + + expected := common.HashToN(q, append(pk.AsInts(), c, z, u, w)...) + actual := rangeProofChallenge( + nil, + q, + pk, + big.NewInt(37), + big.NewInt(41), + big.NewInt(43), + c, + z, + u, + w, + ) + + assert.Equal(t, 0, expected.Cmp(actual)) +} + func TestProveRangeAlice(t *testing.T) { q := tss.EC().Params().N diff --git a/crypto/mta/share_protocol_test.go b/crypto/mta/share_protocol_test.go index f581adb4..dd9354bf 100644 --- a/crypto/mta/share_protocol_test.go +++ b/crypto/mta/share_protocol_test.go @@ -27,6 +27,79 @@ const ( testPaillierKeyLength = 2048 ) +func TestLegacyBobChallengesMatchHistoricalTranscript(t *testing.T) { + q := tss.EC().Params().N + pk := &paillier.PublicKey{N: big.NewInt(17)} + c1 := big.NewInt(19) + c2 := big.NewInt(23) + z := big.NewInt(29) + zPrime := big.NewInt(31) + transcriptT := big.NewInt(37) + v := big.NewInt(41) + w := big.NewInt(43) + + expected := common.HashToN( + q, + append(pk.AsInts(), c1, c2, z, zPrime, transcriptT, v, w)..., + ) + actual := bobProofChallenge( + nil, + q, + pk, + big.NewInt(47), + big.NewInt(53), + big.NewInt(59), + c1, + c2, + nil, + nil, + z, + zPrime, + transcriptT, + v, + w, + ) + assert.Equal(t, 0, expected.Cmp(actual)) + + x := crypto.ScalarBaseMult(tss.EC(), big.NewInt(2)) + u := crypto.ScalarBaseMult(tss.EC(), big.NewInt(3)) + expectedWithCheck := common.HashToN( + q, + append( + pk.AsInts(), + x.X(), + x.Y(), + c1, + c2, + u.X(), + u.Y(), + z, + zPrime, + transcriptT, + v, + w, + )..., + ) + actualWithCheck := bobProofChallenge( + nil, + q, + pk, + big.NewInt(47), + big.NewInt(53), + big.NewInt(59), + c1, + c2, + x, + u, + z, + zPrime, + transcriptT, + v, + w, + ) + assert.Equal(t, 0, expectedWithCheck.Cmp(actualWithCheck)) +} + func TestShareProtocol(t *testing.T) { q := tss.EC().Params().N diff --git a/ecdsa/keygen/local_party.go b/ecdsa/keygen/local_party.go index f20c5003..61bddb74 100644 --- a/ecdsa/keygen/local_party.go +++ b/ecdsa/keygen/local_party.go @@ -65,6 +65,11 @@ func NewLocalParty( end chan<- LocalPartySaveData, optionalPreParams ...LocalPreParams, ) tss.Party { + if params == nil { + panic("keygen.NewLocalParty requires parameters") + } + params.FreezeProtocolMode() + partyCount := params.PartyCount() data := NewLocalPartySaveData(partyCount) // when `optionalPreParams` is provided we'll use the pre-computed primes instead of generating them from scratch diff --git a/ecdsa/keygen/local_party_test.go b/ecdsa/keygen/local_party_test.go index 137edd4e..ce866853 100644 --- a/ecdsa/keygen/local_party_test.go +++ b/ecdsa/keygen/local_party_test.go @@ -49,6 +49,7 @@ func TestSSIDIncludesSessionNonce(t *testing.T) { func testKeygenSSID(pIDs tss.SortedPartyIDs, sessionID []byte) []byte { params := tss.NewParameters(tss.S256(), tss.NewPeerContext(pIDs), pIDs[0], len(pIDs), 1) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonceBytes(sessionID) round := &base{ @@ -62,6 +63,7 @@ func testKeygenSSID(pIDs tss.SortedPartyIDs, sessionID []byte) []byte { func TestStoreMessageRejectsContentDifferentReplay(t *testing.T) { pIDs := tss.GenerateTestPartyIDs(2) params := tss.NewParameters(tss.S256(), tss.NewPeerContext(pIDs), pIDs[0], len(pIDs), 1) + params.SetProtocolMode(tss.ProtocolModeLegacy) lp := NewLocalParty(params, nil, nil).(*LocalParty) msg1 := NewKGRound2Message2(pIDs[1], []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}) @@ -86,6 +88,7 @@ func TestStoreMessageRejectsContentDifferentReplay(t *testing.T) { func TestStoreMessageAllowsSelfReplacement(t *testing.T) { pIDs := tss.GenerateTestPartyIDs(2) params := tss.NewParameters(tss.S256(), tss.NewPeerContext(pIDs), pIDs[0], len(pIDs), 1) + params.SetProtocolMode(tss.ProtocolModeLegacy) lp := NewLocalParty(params, nil, nil).(*LocalParty) msg1 := NewKGRound2Message2(pIDs[0], []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}) @@ -116,6 +119,7 @@ func TestKeygen_Start_RequiresSessionNonce(t *testing.T) { pIDs := tss.GenerateTestPartyIDs(2) p2pCtx := tss.NewPeerContext(pIDs) params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[0], len(pIDs), 1) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. out := make(chan tss.Message, 1) @@ -142,6 +146,7 @@ func TestStartRound1Paillier(t *testing.T) { p2pCtx := tss.NewPeerContext(pIDs) threshold := 1 params := tss.NewParameters(tss.EC(), p2pCtx, pIDs[0], len(pIDs), threshold) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonce(big.NewInt(1)) fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants) @@ -183,6 +188,7 @@ func TestFinishAndSaveH1H2(t *testing.T) { p2pCtx := tss.NewPeerContext(pIDs) threshold := 1 params := tss.NewParameters(tss.EC(), p2pCtx, pIDs[0], len(pIDs), threshold) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonce(big.NewInt(2)) fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants) @@ -231,6 +237,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.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonce(big.NewInt(3)) fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants) @@ -267,6 +274,20 @@ func TestBadMessageCulprits(t *testing.T) { } func TestE2EConcurrentAndSaveFixtures(t *testing.T) { + for _, testCase := range []struct { + name string + mode tss.ProtocolMode + }{ + {"legacy", tss.ProtocolModeLegacy}, + {"security-v2", tss.ProtocolModeSecurityV2}, + } { + t.Run(testCase.name, func(t *testing.T) { + testE2EConcurrentAndSaveFixtures(t, testCase.mode) + }) + } +} + +func testE2EConcurrentAndSaveFixtures(t *testing.T, mode tss.ProtocolMode) { setUp("info") // tss.SetCurve(elliptic.P256()) @@ -294,7 +315,10 @@ func TestE2EConcurrentAndSaveFixtures(t *testing.T) { for i := 0; i < len(pIDs); i++ { var P *LocalParty params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold) - params.SetSessionNonce(ceremonyNonce) + params.SetProtocolMode(mode) + if mode == tss.ProtocolModeSecurityV2 { + 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 f6847628..e07d85e0 100644 --- a/ecdsa/keygen/round_1.go +++ b/ecdsa/keygen/round_1.go @@ -84,18 +84,17 @@ func (round *round1) Start() *tss.Error { round.save.NTildej[i] = preParams.NTildei round.save.H1j[i], round.save.H2j[i] = preParams.H1i, preParams.H2i - // 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 || nonce.Sign() <= 0 { - return round.WrapError(errors.New("keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) + if round.ProtocolMode() == tss.ProtocolModeSecurityV2 { + // Security-v2 fails closed if no SessionNonce is set. Legacy mode + // intentionally has no nonce and uses the historical untagged proof + // transcript. + nonce := round.Params().SessionNonce() + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("security-v2 keygen requires tss.Parameters.SetSessionNonce() before Start"), Pi) + } + round.temp.ssidNonce = new(big.Int).Set(nonce) + round.temp.ssid = round.getSSID() } - round.temp.ssidNonce = new(big.Int).Set(nonce) - round.temp.ssid = round.getSSID() - contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) // generate the dlnproofs for keygen h1i, h2i, alpha, beta, p, q, NTildei := @@ -106,10 +105,26 @@ func (round *round1) Start() *tss.Error { preParams.P, preParams.Q, preParams.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) + dlnProof1 := dlnproof.NewDLNProof( + h1i, + h2i, + alpha, + p, + q, + NTildei, + round.proofSession()..., + ) + dlnProof2 := dlnproof.NewDLNProof( + h2i, + h1i, + beta, + p, + q, + NTildei, + round.proofSession()..., + ) + + modProof := preParams.PaillierSK.ModProof(round.proofContext(i)...) // NTildei = (2p+1) * (2q+1) // phi(NTildei) = ((2p+1) - 1) * ((2q+1) - 1) = 2p * 2q @@ -122,7 +137,7 @@ func (round *round1) Start() *tss.Error { pkTilde := &paillier.PublicKey{N: NTildei} skTilde := &paillier.PrivateKey{PublicKey: *pkTilde, LambdaN: lambdaNTilde, PhiN: phiNTilde} - modProofTilde := skTilde.ModProof(contextI) + modProofTilde := skTilde.ModProof(round.proofContext(i)...) // for this P: SAVE // - shareID diff --git a/ecdsa/keygen/round_2.go b/ecdsa/keygen/round_2.go index ab13831d..8d0c4a92 100644 --- a/ecdsa/keygen/round_2.go +++ b/ecdsa/keygen/round_2.go @@ -71,32 +71,30 @@ func (round *round2) Start() *tss.Error { wg.Add(4) _j := j _msg := msg - contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) - verifier.VerifyDLNProof1(r1msg, H1j, H2j, NTildej, func(isValid bool) { if !isValid { dlnProof1FailCulprits[_j] = _msg.GetFrom() } wg.Done() - }, round.temp.ssid) + }, round.proofSession()...) verifier.VerifyDLNProof2(r1msg, H2j, H1j, NTildej, func(isValid bool) { if !isValid { dlnProof2FailCulprits[_j] = _msg.GetFrom() } wg.Done() - }, round.temp.ssid) + }, round.proofSession()...) verifier.VerifyModProof(r1msg, paillierPKj.N, func(isValid bool) { if !isValid { modProofFailCulprits[_j] = _msg.GetFrom() } wg.Done() - }, contextJ) + }, round.proofContext(j)...) verifier.VerifyModProofTilde(r1msg, NTildej, func(isValid bool) { if !isValid { modProofTildeFailCulprits[_j] = _msg.GetFrom() } wg.Done() - }, contextJ) + }, round.proofContext(j)...) } wg.Wait() for _, culprit := range append(dlnProof1FailCulprits, dlnProof2FailCulprits...) { @@ -129,7 +127,6 @@ func (round *round2) Start() *tss.Error { // 5. p2p send share ij to Pj shares := round.temp.shares - 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 { @@ -137,8 +134,18 @@ 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, contextI) - facProofTilde := round.temp.skTilde.FactorProof(NTildej, H1j, H2j, contextI) + facProof := round.save.LocalPreParams.PaillierSK.FactorProof( + NTildej, + H1j, + H2j, + round.proofContext(i)..., + ) + facProofTilde := round.temp.skTilde.FactorProof( + NTildej, + H1j, + H2j, + round.proofContext(i)..., + ) r2msg1 := NewKGRound2Message1(Pj, round.PartyID(), shares[j], facProof, facProofTilde) round.out <- r2msg1 diff --git a/ecdsa/keygen/round_3.go b/ecdsa/keygen/round_3.go index 0134aeee..1658149b 100644 --- a/ecdsa/keygen/round_3.go +++ b/ecdsa/keygen/round_3.go @@ -65,7 +65,6 @@ func (round *round3) Start() *tss.Error { if j == PIdx { continue } - contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // 6-8. go func(j int, ch chan<- vssOut) { // 4-9. @@ -97,7 +96,13 @@ 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, contextJ) + ok, err = FacProof.FactorVerify( + pkN, + NTilde, + H1i, + H2i, + round.proofContext(j)..., + ) if err != nil { ch <- vssOut{err, nil} return @@ -108,7 +113,13 @@ func (round *round3) Start() *tss.Error { } FacProofTilde := r2msg1.UnmarshalFactorProofTilde() NTildej := round.save.NTildej[j] - ok, err = FacProofTilde.FactorVerify(NTildej, NTilde, H1i, H2i, contextJ) + ok, err = FacProofTilde.FactorVerify( + NTildej, + NTilde, + H1i, + H2i, + round.proofContext(j)..., + ) if err != nil { ch <- vssOut{err, nil} return diff --git a/ecdsa/keygen/rounds.go b/ecdsa/keygen/rounds.go index 85c0a24e..9474a210 100644 --- a/ecdsa/keygen/rounds.go +++ b/ecdsa/keygen/rounds.go @@ -98,6 +98,30 @@ func (round *base) resetOK() { } } +// proofSession returns the immutable per-party proof transcript selection in +// variadic-call form. Legacy parties deliberately pass no session so the +// proof primitives reproduce their historical challenges byte for byte. +func (round *base) proofSession() [][]byte { + if round.ProtocolMode() == tss.ProtocolModeSecurityV2 { + return [][]byte{round.temp.ssid} + } + return nil +} + +// proofContext extends the security-v2 session with the producing party's +// index. Legacy proofs predate these per-party transcript contexts. +func (round *base) proofContext(index int) [][]byte { + if round.ProtocolMode() == tss.ProtocolModeSecurityV2 { + return [][]byte{ + common.AppendUint64ToBytesSlice( + round.temp.ssid, + uint64(index), + ), + } + } + return nil +} + // getSSID derives the session-binding identifier for keygen. // // Callers must invoke this exactly once, in round 1, and store the result in diff --git a/ecdsa/signing/local_party.go b/ecdsa/signing/local_party.go index 8fafe9b4..34284f3a 100644 --- a/ecdsa/signing/local_party.go +++ b/ecdsa/signing/local_party.go @@ -127,6 +127,7 @@ func NewLocalPartyWithKDD( fullBytesLen ...int, ) tss.Party { validatedFullBytesLen := validateFullBytesLen("NewLocalPartyWithKDD", msg, params, fullBytesLen) + params.FreezeProtocolMode() partyCount := len(params.Parties().IDs()) p := &LocalParty{ diff --git a/ecdsa/signing/local_party_test.go b/ecdsa/signing/local_party_test.go index 6ecd0d91..4d73794e 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -79,6 +79,7 @@ func newStoreMessageTestParty(t *testing.T) (*LocalParty, tss.SortedPartyIDs) { pIDs := tss.GenerateTestPartyIDs(2) params := tss.NewParameters(tss.S256(), tss.NewPeerContext(pIDs), pIDs[0], len(pIDs), 1) + params.SetProtocolMode(tss.ProtocolModeLegacy) keys := keygen.NewLocalPartySaveData(len(pIDs)) for i, id := range pIDs { keys.Ks[i] = id.KeyInt() @@ -88,6 +89,20 @@ func newStoreMessageTestParty(t *testing.T) (*LocalParty, tss.SortedPartyIDs) { } func TestE2EConcurrent(t *testing.T) { + for _, testCase := range []struct { + name string + mode tss.ProtocolMode + }{ + {"legacy", tss.ProtocolModeLegacy}, + {"security-v2", tss.ProtocolModeSecurityV2}, + } { + t.Run(testCase.name, func(t *testing.T) { + testE2EConcurrent(t, testCase.mode) + }) + } +} + +func testE2EConcurrent(t *testing.T, mode tss.ProtocolMode) { setUp("info") threshold := testThreshold @@ -116,7 +131,10 @@ func TestE2EConcurrent(t *testing.T) { 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) + params.SetProtocolMode(mode) + if mode == tss.ProtocolModeSecurityV2 { + params.SetSessionNonce(ceremonyNonce) + } P := NewLocalParty(msgInt, params, keys[i], outCh, endCh, len(msgData)).(*LocalParty) parties = append(parties, P) @@ -228,6 +246,7 @@ func TestE2EWithHDKeyDerivation(t *testing.T) { ceremonyNonce := big.NewInt(2) for i := 0; i < len(signPIDs); i++ { params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[i], len(signPIDs), threshold) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonce(ceremonyNonce) P := NewLocalPartyWithKDD(big.NewInt(42), params, keys[i], keyDerivationDelta, outCh, endCh, 32).(*LocalParty) @@ -316,6 +335,7 @@ func TestSigning_Start_RequiresSessionNonce(t *testing.T) { endCh := make(chan common.SignatureData, len(signPIDs)) params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[0], len(signPIDs), testThreshold) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) // Deliberately do NOT call params.SetSessionNonce — Start must fail closed. P := NewLocalParty(big.NewInt(42), params, keys[0], outCh, endCh, 32).(*LocalParty) diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index 779d7da6..7f9e43a4 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -48,21 +48,21 @@ func (round *round1) Start() *tss.Error { round.number = 1 round.started = true round.resetOK() - // 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 || 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() - if err != nil { - return round.WrapError(err) + if round.ProtocolMode() == tss.ProtocolModeSecurityV2 { + // Security-v2 fails closed if no SessionNonce is set. Legacy mode + // intentionally has no nonce and uses the historical untagged proof + // transcript. + nonce := round.Params().SessionNonce() + if nonce == nil || nonce.Sign() <= 0 { + return round.WrapError(errors.New("security-v2 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) + } + round.temp.ssid = ssid } - round.temp.ssid = ssid k := common.GetRandomPositiveInt(round.Params().EC().Params().N) gamma := common.GetRandomPositiveInt(round.Params().EC().Params().N) @@ -81,8 +81,15 @@ func (round *round1) Start() *tss.Error { if j == i { continue } - 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) + 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], + round.proofContext(j)..., + ) 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 e63a7760..e32e6fc6 100644 --- a/ecdsa/signing/round_2.go +++ b/ecdsa/signing/round_2.go @@ -12,7 +12,6 @@ import ( 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" ) @@ -31,7 +30,6 @@ 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.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) attributeBobMidErr := func(err error, Pj *tss.PartyID) *tss.Error { if errors.Is(err, mta.ErrRangeProofVerify) { return round.WrapError(errorspkg.Wrap(err, "peer RangeProofAlice rejected"), Pj) @@ -63,7 +61,7 @@ func (round *round2) Start() *tss.Error { round.key.NTildej[i], round.key.H1j[i], round.key.H2j[i], - contextI) + round.proofContext(i)...) // should be thread safe as these are pre-allocated round.temp.betas[j] = beta round.temp.c1jis[j] = c1ji @@ -94,7 +92,7 @@ func (round *round2) Start() *tss.Error { round.key.H1j[i], round.key.H2j[i], round.temp.bigWs[i], - contextI) + round.proofContext(i)...) round.temp.vs[j] = v round.temp.c2jis[j] = c2ji round.temp.pi2jis[j] = pi2ji diff --git a/ecdsa/signing/round_3.go b/ecdsa/signing/round_3.go index 92dfc864..0c1be6c5 100644 --- a/ecdsa/signing/round_3.go +++ b/ecdsa/signing/round_3.go @@ -38,7 +38,6 @@ func (round *round3) Start() *tss.Error { if j == i { continue } - contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // Alice_end go func(j int, Pj *tss.PartyID) { defer wg.Done() @@ -58,7 +57,7 @@ func (round *round3) Start() *tss.Error { new(big.Int).SetBytes(r2msg.GetC1()), round.key.NTildej[i], round.key.PaillierSK, - contextJ) + round.proofContext(j)...) alphas[j] = alphaIj if err != nil { errChs <- round.WrapError(err, Pj) @@ -84,7 +83,7 @@ func (round *round3) Start() *tss.Error { round.key.H1j[i], round.key.H2j[i], round.key.PaillierSK, - contextJ) + round.proofContext(j)...) us[j] = uIj if err != nil { errChs <- round.WrapError(err, Pj) diff --git a/ecdsa/signing/round_4.go b/ecdsa/signing/round_4.go index b8b8467b..6c38442d 100644 --- a/ecdsa/signing/round_4.go +++ b/ecdsa/signing/round_4.go @@ -45,8 +45,20 @@ func (round *round4) Start() *tss.Error { return round.WrapError(errors.New("theta inverse is nil")) } i := round.PartyID().Index - contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) - piGamma, err := schnorr.NewZKProofWithSession(contextI, round.temp.gamma, round.temp.pointGamma) + var piGamma *schnorr.ZKProof + var err error + if round.ProtocolMode() == tss.ProtocolModeLegacy { + piGamma, err = schnorr.NewZKProof( + round.temp.gamma, + round.temp.pointGamma, + ) + } else { + piGamma, err = schnorr.NewZKProofWithSession( + round.proofContext(i)[0], + 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 963378b9..30d0b504 100644 --- a/ecdsa/signing/round_5.go +++ b/ecdsa/signing/round_5.go @@ -46,8 +46,14 @@ func (round *round5) Start() *tss.Error { if err != nil { return round.WrapError(errors.New("failed to unmarshal bigGamma proof"), Pj) } - contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) - ok = proof.VerifyWithSession(contextJ, bigGammaJPoint) + if round.ProtocolMode() == tss.ProtocolModeLegacy { + ok = proof.Verify(bigGammaJPoint) + } else { + ok = proof.VerifyWithSession( + round.proofContext(j)[0], + 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 95da829a..3afd79c0 100644 --- a/ecdsa/signing/round_6.go +++ b/ecdsa/signing/round_6.go @@ -11,7 +11,6 @@ import ( 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" ) @@ -25,12 +24,37 @@ func (round *round6) Start() *tss.Error { round.resetOK() i := round.PartyID().Index - contextI := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) - piAi, err := schnorr.NewZKProofWithSession(contextI, round.temp.roi, round.temp.bigAi) + var piAi *schnorr.ZKProof + var err error + if round.ProtocolMode() == tss.ProtocolModeLegacy { + piAi, err = schnorr.NewZKProof(round.temp.roi, round.temp.bigAi) + } else { + piAi, err = schnorr.NewZKProofWithSession( + round.proofContext(i)[0], + round.temp.roi, + round.temp.bigAi, + ) + } if err != nil { return round.WrapError(errors2.Wrapf(err, "NewZKProof(roi, bigAi)")) } - piV, err := schnorr.NewZKVProofWithSession(contextI, round.temp.bigVi, round.temp.bigR, round.temp.si, round.temp.li) + var piV *schnorr.ZKVProof + if round.ProtocolMode() == tss.ProtocolModeLegacy { + piV, err = schnorr.NewZKVProof( + round.temp.bigVi, + round.temp.bigR, + round.temp.si, + round.temp.li, + ) + } else { + piV, err = schnorr.NewZKVProofWithSession( + round.proofContext(i)[0], + 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)")) } diff --git a/ecdsa/signing/round_7.go b/ecdsa/signing/round_7.go index fb2c86d3..b60bd9fb 100644 --- a/ecdsa/signing/round_7.go +++ b/ecdsa/signing/round_7.go @@ -51,13 +51,35 @@ func (round *round7) Start() *tss.Error { return round.WrapError(errors2.Wrapf(err, "NewECPoint(bigAj)"), Pj) } bigAjs[j] = bigAj - contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) pijA, err := r6msg.UnmarshalZKProof(round.Params().EC()) - if err != nil || !pijA.VerifyWithSession(contextJ, bigAj) { + validProofA := false + if err == nil { + if round.ProtocolMode() == tss.ProtocolModeLegacy { + validProofA = pijA.Verify(bigAj) + } else { + validProofA = pijA.VerifyWithSession( + round.proofContext(j)[0], + bigAj, + ) + } + } + if !validProofA { return round.WrapError(errors.New("schnorr verify for Aj failed"), Pj) } pijV, err := r6msg.UnmarshalZKVProof(round.Params().EC()) - if err != nil || !pijV.VerifyWithSession(contextJ, bigVj, round.temp.bigR) { + validProofV := false + if err == nil { + if round.ProtocolMode() == tss.ProtocolModeLegacy { + validProofV = pijV.Verify(bigVj, round.temp.bigR) + } else { + validProofV = pijV.VerifyWithSession( + round.proofContext(j)[0], + bigVj, + round.temp.bigR, + ) + } + } + if !validProofV { return round.WrapError(errors.New("vverify for Vj failed"), Pj) } } diff --git a/ecdsa/signing/round_9_test.go b/ecdsa/signing/round_9_test.go index a0963956..de8c985d 100644 --- a/ecdsa/signing/round_9_test.go +++ b/ecdsa/signing/round_9_test.go @@ -193,6 +193,7 @@ func TestSigning_Start_RejectsInvalidMessage(t *testing.T) { endCh := make(chan common.SignatureData, len(signPIDs)) params := tss.NewParameters(tss.S256(), p2pCtx, signPIDs[0], len(signPIDs), testThreshold) + params.SetProtocolMode(tss.ProtocolModeSecurityV2) params.SetSessionNonce(big.NewInt(1)) P := NewLocalParty(msg, params, keys[0], outCh, endCh, 32).(*LocalParty) diff --git a/ecdsa/signing/rounds.go b/ecdsa/signing/rounds.go index a5856689..88462948 100644 --- a/ecdsa/signing/rounds.go +++ b/ecdsa/signing/rounds.go @@ -125,6 +125,22 @@ func (round *base) resetOK() { } } +// proofContext returns the immutable transcript selection in variadic-call +// form. Legacy parties pass no session and therefore reproduce the historical +// untagged challenges. Security-v2 parties bind every proof to the ceremony +// SSID and the producing party. +func (round *base) proofContext(index int) [][]byte { + if round.ProtocolMode() == tss.ProtocolModeSecurityV2 { + return [][]byte{ + common.AppendUint64ToBytesSlice( + round.temp.ssid, + uint64(index), + ), + } + } + return nil +} + // getSSID derives the session-binding identifier for signing. // // Callers must invoke this exactly once, in round 1, and store the result in diff --git a/tss/params.go b/tss/params.go index 2245a056..38cbcfcf 100644 --- a/tss/params.go +++ b/tss/params.go @@ -17,6 +17,11 @@ import ( ) type ( + // ProtocolMode selects the wire-compatible GG20 proof transcript used by + // an ECDSA local party. It is configured per Parameters value before the + // party is constructed and frozen for that party's lifetime. + ProtocolMode uint8 + Parameters struct { ec elliptic.Curve partyID *PartyID @@ -25,15 +30,28 @@ type ( threshold int concurrency int safePrimeGenTimeout time.Duration - // sessionNonce provides per-session SSID uniqueness for GG20 proof - // binding. Keygen and signing require callers to coordinate a shared - // positive nonce before Start. + // sessionNonce provides per-session SSID uniqueness for security-v2 + // GG20 proof binding. Security-v2 keygen and signing require callers + // to coordinate a shared positive nonce before Start; legacy mode must + // leave it unset. sessionNonce *big.Int + // protocolMode is the explicit per-party proof-transcript mode. It has + // no default: callers must select legacy or security-v2 before + // constructing an ECDSA local party. + protocolMode ProtocolMode + protocolModeFrozen bool } ) const ( defaultSafePrimeGenTimeout = 5 * time.Minute + + // ProtocolModeLegacy reproduces the untagged GG20 proof transcript used + // before session binding was introduced. + ProtocolModeLegacy ProtocolMode = 1 + // ProtocolModeSecurityV2 requires and uses the session-bound, domain-tagged + // GG20 proof transcript. + ProtocolModeSecurityV2 ProtocolMode = 2 ) // Exported, used in `tss` client @@ -118,13 +136,61 @@ func (params *Parameters) SetSafePrimeGenTimeout(timeout time.Duration) { params.safePrimeGenTimeout = timeout } -// SessionNonce returns the optional per-session nonce used in proof challenges. +// ProtocolMode returns the explicit per-party proof-transcript mode. +func (params *Parameters) ProtocolMode() ProtocolMode { + return params.protocolMode +} + +// SetProtocolMode selects the proof transcript for the ECDSA local party that +// will be constructed from params. There is no implicit/default mode. +// +// A local party freezes this setting during construction. Changing it +// afterwards panics so an in-flight party can never switch transcripts. +func (params *Parameters) SetProtocolMode(mode ProtocolMode) { + if params.protocolModeFrozen { + panic("tss: protocol mode is immutable after local party construction") + } + switch mode { + case ProtocolModeLegacy, ProtocolModeSecurityV2: + default: + panic(fmt.Sprintf("tss: invalid protocol mode %d", mode)) + } + if params.protocolMode != 0 && params.protocolMode != mode { + panic("tss: protocol mode cannot be changed after selection") + } + params.protocolMode = mode +} + +// FreezeProtocolMode validates and freezes the transcript configuration. +// ECDSA local-party constructors call it before retaining params. +func (params *Parameters) FreezeProtocolMode() { + if params.protocolModeFrozen { + return + } + switch params.protocolMode { + case ProtocolModeLegacy: + if params.sessionNonce != nil { + panic("tss: legacy protocol mode must not set a session nonce") + } + case ProtocolModeSecurityV2: + default: + panic("tss: protocol mode must be selected before local party construction") + } + params.protocolModeFrozen = true +} + +// SessionNonce returns a defensive copy of the optional per-session nonce used +// in proof challenges. func (params *Parameters) SessionNonce() *big.Int { - return params.sessionNonce + if params.sessionNonce == nil { + return nil + } + return new(big.Int).Set(params.sessionNonce) } -// SetSessionNonce sets a per-session nonce that all parties in a protocol run -// must agree on. It must be called before Start. +// SetSessionNonce sets a per-session nonce that all security-v2 parties in a +// protocol run must agree on. It must be called before constructing the local +// party. Legacy parties must not set a nonce. // // Keygen and signing fail closed if no nonce is set. The previous zero // (keygen) and SHA512_256(messageBytes) (signing) fallbacks caused two @@ -132,9 +198,12 @@ func (params *Parameters) SessionNonce() *big.Int { // 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. Set the -// nonce before Start on the same goroutine that constructs the party; do not -// mutate Parameters concurrently with a running protocol. +// nonce before constructing the party on the same goroutine; do not mutate +// Parameters concurrently with a running protocol. func (params *Parameters) SetSessionNonce(nonce *big.Int) { + if params.protocolModeFrozen { + panic("tss: session nonce is immutable after local party construction") + } if nonce == nil || nonce.Sign() <= 0 { panic("tss: session nonce must be positive") } diff --git a/tss/params_test.go b/tss/params_test.go index c9fed7a1..9320a718 100644 --- a/tss/params_test.go +++ b/tss/params_test.go @@ -26,6 +26,80 @@ func TestSetSessionNonceCopiesInput(t *testing.T) { assert.Equal(t, big.NewInt(42), params.SessionNonce()) } +func TestSessionNonceReturnsCopy(t *testing.T) { + pIDs := GenerateTestPartyIDs(2) + params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], len(pIDs), 1) + params.SetSessionNonce(big.NewInt(42)) + + returned := params.SessionNonce() + returned.SetInt64(7) + + assert.Equal(t, big.NewInt(42), params.SessionNonce()) +} + +func TestProtocolModeMustBeExplicitAndImmutable(t *testing.T) { + pIDs := GenerateTestPartyIDs(2) + newParams := func() *Parameters { + return NewParameters( + S256(), + NewPeerContext(pIDs), + pIDs[0], + len(pIDs), + 1, + ) + } + + assert.Panics(t, func() { + newParams().FreezeProtocolMode() + }) + for _, invalid := range []ProtocolMode{0, 3, 255} { + assert.Panics(t, func() { + newParams().SetProtocolMode(invalid) + }) + } + + legacy := newParams() + legacy.SetProtocolMode(ProtocolModeLegacy) + assert.Equal(t, ProtocolModeLegacy, legacy.ProtocolMode()) + assert.Panics(t, func() { + legacy.SetProtocolMode(ProtocolModeSecurityV2) + }) + + legacy.FreezeProtocolMode() + assert.Panics(t, func() { + legacy.SetProtocolMode(ProtocolModeLegacy) + }) + assert.Panics(t, func() { + legacy.SetSessionNonce(big.NewInt(1)) + }) + + securityV2 := newParams() + securityV2.SetProtocolMode(ProtocolModeSecurityV2) + securityV2.SetSessionNonce(big.NewInt(1)) + securityV2.FreezeProtocolMode() + assert.Equal(t, ProtocolModeSecurityV2, securityV2.ProtocolMode()) + assert.Panics(t, func() { + securityV2.SetSessionNonce(big.NewInt(2)) + }) +} + +func TestLegacyProtocolModeRejectsSessionNonce(t *testing.T) { + pIDs := GenerateTestPartyIDs(2) + params := NewParameters( + S256(), + NewPeerContext(pIDs), + pIDs[0], + len(pIDs), + 1, + ) + params.SetProtocolMode(ProtocolModeLegacy) + params.SetSessionNonce(big.NewInt(1)) + + assert.Panics(t, func() { + params.FreezeProtocolMode() + }) +} + func TestSetSessionNonceBytesHashesSessionID(t *testing.T) { pIDs := GenerateTestPartyIDs(2) params := NewParameters(S256(), NewPeerContext(pIDs), pIDs[0], len(pIDs), 1)