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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..41d8361bc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,240 @@ +# Changelog + +All notable changes to this fork (`threshold-network/tss-lib`) are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +This fork follows the upstream [`bnb-chain/tss-lib`](https://github.com/bnb-chain/tss-lib) +SemVer line for provenance but has not yet published its own tagged release; all changes +below are therefore listed under `[Unreleased]`. + +Provenance notation. Each entry carries two kinds of reference: +- **Upstream source** — `BNB #NNN` / `BNB ` is the upstream pull request or commit the + change was adapted from. Most were **manually adapted**, not cherry-picked, so behavior may + differ from upstream. `threshold-original` means there is no direct upstream counterpart. +- **Fork PR** — `PR #N` is the `threshold-network/tss-lib` pull request that introduced the + change into this fork, for traceability. + +--- + +## [Unreleased] — BNB hardening integration + +Security and correctness hardening ported or manually adapted from `bnb-chain/tss-lib`, +without replacing Threshold's existing Paillier/NTilde `ModProof`/`FactorProof` remediation. + +- Threshold base: `2e712689cfbeefede15f95a0ec7112227d86f702` +- BNB upstream head compared: `3f677ff761fcf692edb0243a5d812930844d879a` + +This unreleased set is delivered through a stack of fork pull requests. **Every entry below +belongs to PR #2 (the base BNB hardening integration) unless it is tagged with another +`PR #N`.** Composing PRs: +- **PR #2** — base BNB hardening integration. + +### ⚠️ Compatibility — read before upgrading + +**This release is a protocol/wire compatibility break and must be rolled out as a +coordinated protocol upgrade.** Fiat-Shamir proof challenges now use tagged hashing, +session context, and fixed-width message encoding. Parties running pre-upgrade code +**cannot** interoperate with upgraded parties in the same keygen, signing, or resharing +ceremony, even though the Go API remains source-compatible for nearly all call sites. + +Do not mix pre- and post-upgrade parties in one ceremony. All participants (and, for +resharing, **both** committees) must run the upgraded build simultaneously. + +Two new caller obligations are enforced at runtime (see Breaking Changes 1 and 2): +1. Set a per-ceremony session nonce before `Start()`. +2. Pass a positive `fullBytesLen` to every signing constructor. + +### Breaking changes + +#### 1. Session nonce is now mandatory and fails closed +- **What:** ECDSA keygen, ECDSA signing, ECDSA resharing, EdDSA keygen, and EdDSA signing + now require a positive session nonce. Each protocol's `Start()` (round 1) returns an + error if `Parameters.SetSessionNonce` / `SetSessionNonceBytes` was not called, e.g. + `"keygen requires tss.Parameters.SetSessionNonce(...) before Start"` + (`ecdsa/keygen/round_1.go`, `ecdsa/signing/round_1.go`, + `ecdsa/resharing/round_1_old_step_1.go`, `eddsa/keygen/round_1.go`, + `eddsa/signing/round_1.go`). The nonce is folded into the SSID that binds every proof + transcript. **EdDSA resharing is intentionally excluded** — it has no SSID-bound + transcript in this port (see Residual risks). +- **Break type:** Runtime (previously-succeeding honest callers now error) **and** wire + (proofs are now SSID-bound, so transcripts differ from pre-upgrade peers). +- **Motivation:** Without a unique per-ceremony SSID folded into every Fiat-Shamir + challenge, two ceremonies over otherwise-identical inputs derive the same SSID, enabling + cross-run transcript splicing / proof replay. Fail-closed prevents silently running + without session binding. +- **Provenance:** `BNB fc38979` (SSID uniqueness, `Parameters.SessionNonce`), with the + fail-closed-with-no-fallback decision being `threshold-original`. (Note: the threshold + base had no SSID machinery at all; the "previous zero / `SHA512_256(messageBytes)` + fallback" described in upstream history never shipped in this fork's base.) +- **Migration:** Before `Start()`, on the constructing goroutine, call + `params.SetSessionNonce()` or + `params.SetSessionNonceBytes(<>=16-byte high-entropy session ID>)`. All parties in a run + must agree on the same value. + +#### 2. `fullBytesLen` is required at runtime for signing +- **What:** ECDSA and EdDSA signing constructors (`NewLocalParty`, `NewLocalPartyWithKDD`) + accept `fullBytesLen` as a **variadic** argument for source compatibility, but exactly + one **positive** value is now required at construction time and is bounded to + `[ceil(msg.BitLen()/8), curveOrderBytes]`. Passing none, zero, multiple, or an + out-of-range value panics in the constructor (`ecdsa/signing/local_party.go`, + `eddsa/signing/local_party.go`). +- **Break type:** Runtime (the variadic signature still compiles unchanged, but unupdated + callers panic at runtime). +- **Motivation:** Pins a fixed, ceremony-wide message byte width so leading zero bytes are + preserved. The previous minimal `big.Int.Bytes()` encoding silently dropped high-order + zero bytes, so distinct parties could hash different preimages for the "same" message. +- **Provenance:** `BNB #284` (`9acd90b`, `2f294cf`, `6b92e7d`, `c0de534`). +- **Migration:** Pass a positive `fullBytesLen` (the fixed message/hash width, e.g. `32`) + to every signing constructor call. The value must be identical across all signers. + +#### 3. EdDSA round-3 hashes the full-length message +- **What:** EdDSA signing round 3 now hashes the message left-padded to `fullBytesLen` + (`m.FillBytes`) instead of the minimal `m.Bytes()` when deriving `lambda` + (`eddsa/signing/round_3.go`). This changes each signer's `s` share, hence the final + signature scalar, whenever `fullBytesLen` exceeds the message's minimal byte length. +- **Break type:** Wire/protocol (cross-version EdDSA signers compute incompatible shares + and produce an invalid aggregate signature). +- **Motivation:** Canonical fixed-width message encoding; removes the leading-zero + ambiguity of minimal encoding. +- **Provenance:** `BNB #284`. +- **Migration:** Upgrade all EdDSA signers in lockstep with an identical `fullBytesLen`. + +#### 4. Tagged-hash / session-bound Fiat-Shamir challenges (DLN, Schnorr, MtA, range proof) +- **What:** Challenge derivation for DLN (`crypto/dlnproof`), Schnorr + (`crypto/schnorr`), MtA `ProofBob`/`ProofBobWC` (`crypto/mta/proofs.go`), and + `RangeProofAlice` (`crypto/mta/range_proof.go`) now uses length-delimited tagged hashing + (`common.SHA512_256i_TAGGED`) plus optional session context. **The challenge bytes change + unconditionally** — even on the default/nil-session path — because the underlying hash + construction itself changed. Per-party proof contexts also append a fixed-width `uint64` + party index so party 0 no longer collapses to the bare SSID. +- **Break type:** Wire/protocol (old and new proofs do not cross-verify). +- **Motivation:** Domain separation binds each proof to its session/sub-protocol context, + defeating cross-protocol and cross-session proof replay. The MtA path additionally binds + `NTilde, h1, h2` into the transcript so a malicious verifier cannot swap ring-Pedersen + parameters. +- **Provenance:** `BNB #252` (`3d95e54`), `BNB #256` (`1a14f3a`), `BNB #257` (`ff989bf`, + tagged hashing), `BNB b59ed36` (DLN/MtA session context); party-index append is + `threshold-original`. +- **Migration:** Coordinated network-wide upgrade; no mixed old/new parties. Any persisted + pre-upgrade proofs are not re-verifiable. + +#### 5. Tagged Fiat-Shamir for Paillier ModProof / FactorProof (active on the protocol path) +- **What:** `ModProof`/`ModVerify` and `FactorProof`/`FactorVerify` + (`crypto/paillier/mod_proof.go`, `factor_proof.go`) gain an optional session tag. When a + session tag **is** supplied they use tagged hashing (`common.HashToNTagged`, sized to the + modulus to avoid challenge bias) and are **not** wire-compatible with pre-upgrade peers. + With **no** session tag the challenge bytes are unchanged (backward-compatible default). +- **Break type:** Wire/protocol **only when a session tag is supplied**. +- **Motivation:** Domain separation for the Paillier proofs without weakening Threshold's + existing `N`/`NTilde` `ModProof`/`FactorProof` remediation. Threshold's stronger coverage + was retained; no BNB no-proof escape hatches were introduced. +- **Provenance:** `BNB #252`, `BNB #257`; the in-tree round code now passes session tags, + so in practice this is active on the protocol path. +- **Migration:** Covered by the coordinated upgrade in Breaking Change 1/4. + +#### 6. ECDSA resharing broadcasts and validates an SSID (`DGRound1Message`) +- **What:** `DGRound1Message` gains a new wire field `bytes ssid = 4` + (`protob/ecdsa-resharing.proto`). The new committee rejects any DGRound1 broadcast whose + SSID does not equal its locally-derived SSID, with culprit attribution + (`ecdsa/resharing/round_1_old_step_1.go`), and `ValidateBasic` now requires a 32-byte + SSID (`ecdsa/resharing/messages.go`). The exported constructor `NewDGRound1Message` gains + a required `ssid []byte` parameter. +- **Break type:** Wire/protocol (old and new resharing parties cannot interoperate) **and + source/compile** — this is the **only** exported signature in the whole integration that + changed in a source-breaking way; external callers constructing `DGRound1Message` + directly must update. +- **Motivation:** Lets the new committee detect a corrupted old-committee party that + broadcasts an inconsistent SSID (committee-substitution / context-disagreement + detection). +- **Provenance:** `threshold-original`, derived from the `BNB fc38979` session work. +- **Migration:** Upgrade both committees in lockstep; set a session nonce (Breaking Change + 1); update any direct `NewDGRound1Message` callers to pass `ssid`. + +> Aside from `NewDGRound1Message` (Breaking Change 6), every session / `fullBytesLen` +> parameter was added as a trailing variadic argument, so all other existing call sites +> compile unchanged. The breaks above are runtime/wire, not compile-time. This was verified +> by diffing every exported signature between the base and HEAD. + +### Security & correctness hardening (non-breaking) + +These tighten validation against malformed or malicious input, or fix latent bugs, without +rejecting input that an honest caller would previously have produced. + +- **MtA / range / factor / mod proof boundary checks:** GCD, interval, lower/upper-bound, + non-one/non-zero, ciphertext-coprimality, and curve-mismatch checks now reject malformed + or adversarial proofs (returning errors instead of panicking on, e.g., a nil `U` or a + cross-curve point). Honest proofs are unaffected. The MtA `betaPrm` sampling range was + narrowed (`q^5` instead of `N`) to match the new verifier bounds; this changes + intermediate ciphertext/proof wire values but preserves the `alpha + beta ≡ a·b mod q` + MtA output. _Provenance: `BNB #252`, `BNB #289` (`5d01446`)._ +- **VSS commitment-vector length check:** `feldman_vss.Verify` now requires + `len(vs) == threshold+1`, turning a potential out-of-range panic on a short/long + adversarial commitment vector into a clean `false`. _Provenance: `BNB #291` (`843de68`)._ +- **VSS reconstruction off-by-one fix:** `feldman_vss.ReConstruct` now requires + `threshold+1` shares (was `threshold`) and guards the empty-slice case. The previous + behavior silently reconstructed an **incorrect** secret from `threshold` shares. + Behavior change: a caller passing exactly `threshold` shares now receives + `ErrNumSharesBelowThreshold` instead of a wrong value. No in-tree non-test caller is + affected. _Provenance: `BNB #324` (`4878da5`)._ +- **ECDSA `SignatureData.M` is now full-length-padded:** ECDSA signing finalize emits the + message and computes the verify preimage with `FillBytes(fullBytesLen)` instead of minimal + `m.Bytes()` (`ecdsa/signing/finalize.go`). This is an output-format change only — ECDSA + scalar math uses `m` as an integer, so it is not an interop break — but an operator + diffing the emitted `data.M` across the upgrade will see padded bytes. _Provenance: + `BNB #284`._ +- **Canonical EC coordinate rejection:** EC point construction/deserialization + (`crypto/ecpoint.go`, backing `NewECPoint`, `GobDecode`, `UnmarshalJSON`) now rejects + coordinates outside `[0, P)`. Honest callers never produce out-of-range coordinates; + this hardens against malicious peer input. _Provenance: `BNB 685c2af`._ +- **`round.ok` accumulation fix:** all non-terminal ECDSA/EdDSA keygen, signing, and + resharing rounds now accumulate per-party readiness across the whole message set instead + of bailing on the first not-ready party, fixing inconsistent bookkeeping under + out-of-order message delivery. Internal only; no wire or API change. _Provenance: + `BNB #282` (`409542e`)._ +- **Nil-guards:** `BaseParty.String()` returns `"No more rounds"` instead of panicking + after completion (`BNB #276`, `f3aad28`); the EdDSA resharing round-1 party-0 broadcast + and EdDSA keygen `NewECPoint`-error path are guarded against nil dereference + (`BNB #282`, `BNB 5d0d0f3`). + +### Added + +- `common.SHA512_256i_TAGGED` and `common.HashToNTagged` — length-delimited, domain- + separated tagged hashing primitives. _Provenance: `BNB #257`._ +- `tss.Parameters.SessionNonce`, `SetSessionNonce`, `SetSessionNonceBytes` — session-nonce + API. `SetSessionNonce` rejects non-positive nonces; `SetSessionNonceBytes` requires a + session ID of at least 16 bytes. _Provenance: `BNB fc38979`._ +- `common.IsInInterval`, `common.AppendUint64ToBytesSlice`, + `common.AppendBigIntToBytesSlice` (the last currently unused), and `tss.SameCurve` — + helpers backing the hardened range checks and session/transcript context construction. +- `schnorr.NewZKProofWithSession`, `NewZKVProofWithSession`, `VerifyWithSession` — session- + aware Schnorr proof overloads (the original signatures are retained and delegate with a + nil session). + +### Notes + +- `common.RejectionSample` keeps the upstream name for porting clarity but is a modular + reduction, not a looping rejection sampler. +- Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation + (GHSA-h24c-6p6p-m3vx) was retained; the upstream modproof checker (`BNB #323`) was + already covered. + +### Not ported / deferred + +- Module path bumps to `/v2`, `/v3` (`BNB faf1884`, `c23246e`) — skipped to preserve + Threshold compatibility; the module path remains `github.com/bnb-chain/tss-lib`. +- `SignatureData` channel-to-pointer change (`BNB fbb0ef7`) — public API churn not needed + for hardening. +- Optional constant-time framework (`BNB #328`) — adds a dependency and broad + Paillier/MtA rewrites, default-disabled upstream; deferred to a separate follow-up with + benchmarking and side-channel review. +- Dependency / random-source API churn and repository/CI/metadata housekeeping + (`BNB b8d526d`, `8abf1d5`, `6c233c6`, `87f7e12`, `7113b68`, `d0325a1`, `dca2ac4`). + +### Residual risks + +- Applications **must** call `SetSessionNonce`/`SetSessionNonceBytes` before keygen, + signing, and ECDSA resharing; those protocols fail closed without it. +- EdDSA resharing has no SSID-bound proof transcript in this port. +- The optional constant-time work is not integrated. + +[Unreleased]: https://github.com/threshold-network/tss-lib/compare/2e712689...HEAD diff --git a/README.md b/README.md index 6342c2f89..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 ... @@ -146,6 +147,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 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. 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 +165,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/common/hash.go b/common/hash.go index e26b2fc6d..2b4a795ff 100644 --- a/common/hash.go +++ b/common/hash.go @@ -93,6 +93,48 @@ 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. +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 { + panic("SHA512_256i_TAGGED Write(tag) failed: " + err.Error()) + } + if _, err := state.Write(tagBz); err != nil { + panic("SHA512_256i_TAGGED Write(tag) failed: " + err.Error()) + } + + inLen := len(in) + 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 { + panic("SHA512_256i_TAGGED Write(data) failed: " + err.Error()) + } + 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_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.go b/common/hash_utils.go index 12a0037dd..616a1ffd4 100644 --- a/common/hash_utils.go +++ b/common/hash_utils.go @@ -21,6 +21,14 @@ func LiterallyJustMod(q *big.Int, eHash *big.Int) *big.Int { // e' = eHash return e } +// 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) +} + // Return a big.Int between 0 and N func HashToN(N *big.Int, in ...*big.Int) *big.Int { bitCnt := N.BitLen() @@ -43,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/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/common/int.go b/common/int.go index e6762534d..ad487c517 100644 --- a/common/int.go +++ b/common/int.go @@ -7,6 +7,7 @@ package common import ( + "encoding/binary" "math/big" ) @@ -100,6 +101,24 @@ 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()...) +} + +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/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") +} diff --git a/crypto/dlnproof/proof.go b/crypto/dlnproof/proof.go index b5d5f7749..446e92c7d 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 } @@ -75,19 +77,21 @@ func (p *Proof) Verify(h1, h2, N *big.Int) 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 } } 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 +109,16 @@ func (p *Proof) Verify(h1, h2, N *big.Int) bool { return true } +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] +} + 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/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/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..51e6e3a6a 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,62 @@ 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 || + 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) 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 + } + 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 +258,47 @@ 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.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 - { + { // 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,16 +357,27 @@ 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 + } + if len(session[0]) == 0 { + panic("mta: proof session tag must be non-empty") + } + return session[0] } func (pf *ProofBob) ValidateBasic() bool { - return pf.Z != nil && + return pf != nil && + pf.Z != nil && pf.ZPrm != nil && pf.T != nil && pf.V != nil && @@ -300,7 +390,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.go b/crypto/mta/range_proof.go index 0cff6703d..c7ee82c6b 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,75 @@ 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) + q3NTilde := new(big.Int).Mul(q3, NTilde) + maxS2 := new(big.Int).Lsh(q3NTilde, 1) + + 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.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 + } + if pf.S1.Cmp(pf.S2) == 0 { + return false + } // 3. if pf.S1.Cmp(q3) == 1 { return false } + if pf.S2.Cmp(maxS2) > 0 { + 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..92add050f 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 @@ -47,3 +53,153 @@ func TestProveRangeAlice(t *testing.T) { ok := proof.Verify(tss.EC(), pk, NTildei, h1i, h2i, c) 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 + + 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") + + 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") +} + +// 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") +} 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/mta/share_protocol_test.go b/crypto/mta/share_protocol_test.go index 313163e16..5bff62c99 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 @@ -84,6 +114,32 @@ 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") + + 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") + + 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 +150,54 @@ func TestShareProtocolWC(t *testing.T) { aTimesBPlusBetaModQ := new(big.Int).Mod(aTimesBPlusBeta, q) 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{ + 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 b376b7388..2302abf60 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,15 +82,31 @@ 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") } 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) + 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 +147,20 @@ 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 { + 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) + } + // 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..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" @@ -25,16 +24,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,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) - assert.NoError(t, err2) - - var err3 error - badPrivateKey, badPublicKey, err3 = GenerateBadKeyPair(ctx, testPaillierKeyLength) - assert.NoError(t, 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) @@ -91,6 +80,31 @@ 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 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)) @@ -117,6 +131,88 @@ 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 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) diff --git a/crypto/paillier/mod_proof.go b/crypto/paillier/mod_proof.go index 4c6822a58..f39c48c3f 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") } @@ -83,22 +83,22 @@ func (pf ModProof) ModVerify(N *big.Int) (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) + 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) @@ -124,12 +124,29 @@ func (pf ModProof) ModVerify(N *big.Int) (bool, error) { return true, nil } -// Standard Fiat-Shamir transform -func ModChallenge(N, w *big.Int) [PARAM_M]*big.Int { +// 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 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 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 + } + 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...) } return y diff --git a/crypto/paillier/mod_proof_test.go b/crypto/paillier/mod_proof_test.go index 737f53184..4596c58af 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" ) @@ -30,6 +32,75 @@ 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 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 +// 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 +// 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() @@ -83,6 +154,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 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..8ba7a0454 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,10 @@ 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 len(shares) == 0 { + return nil, ErrNumSharesBelowThreshold + } + 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..8d8fb4ea2 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" @@ -34,12 +35,65 @@ const ( testThreshold = TestThreshold ) +func TestSSIDIncludesSessionNonce(t *testing.T) { + pIDs := tss.GenerateTestPartyIDs(3) + + 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) +} + +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) } } +// 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") @@ -47,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 { @@ -87,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 { @@ -134,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 { @@ -192,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 { @@ -277,9 +336,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..f68476282 100644 --- a/ecdsa/keygen/round_1.go +++ b/ecdsa/keygen/round_1.go @@ -84,6 +84,19 @@ 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) + } + 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 := preParams.H1i, @@ -93,10 +106,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 +122,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 +171,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..ab13831d2 100644 --- a/ecdsa/keygen/round_2.go +++ b/ecdsa/keygen/round_2.go @@ -71,31 +71,32 @@ 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) 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 +129,7 @@ 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 { @@ -135,8 +137,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 +163,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..0134aeee8 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.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // 6-8. go func(j int, ch chan<- vssOut) { // 4-9. @@ -96,21 +97,25 @@ 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} + 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) + 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} @@ -216,17 +221,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..9d9c18583 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...).FillBytes(make([]byte, 32)) +} 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/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.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/local_party_test.go b/ecdsa/resharing/local_party_test.go index 8607e200c..a1981e431 100644 --- a/ecdsa/resharing/local_party_test.go +++ b/ecdsa/resharing/local_party_test.go @@ -10,7 +10,9 @@ import ( "crypto/ecdsa" "fmt" "math/big" + "reflect" "runtime" + "strings" "sync/atomic" "testing" @@ -37,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") @@ -71,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 @@ -169,10 +206,18 @@ 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) + } + }() + signCeremonyNonce := big.NewInt(1) for j, signPID := range signPIDs { params := tss.NewParameters(tss.S256(), signP2pCtx, signPID, len(signPIDs), newThreshold) - P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh).(*signing.LocalParty) + params.SetSessionNonce(signCeremonyNonce) + 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 { @@ -206,7 +251,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 +264,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 +276,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/messages.go b/ecdsa/resharing/messages.go index d9a65b515..b17a3bf93 100644 --- a/ecdsa/resharing/messages.go +++ b/ecdsa/resharing/messages.go @@ -32,6 +32,8 @@ var ( } ) +const paillierBitsLen = 2048 + // ----- // func NewDGRound1Message( @@ -39,6 +41,7 @@ func NewDGRound1Message( from *tss.PartyID, ecdsaPub *crypto.ECPoint, vct cmt.HashCommitment, + ssid []byte, ) tss.ParsedMessage { meta := tss.MessageRouting{ From: from, @@ -50,6 +53,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 +63,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) && + len(m.Ssid) == 32 } func (m *DGRound1Message) UnmarshalECDSAPub(ec elliptic.Curve) (*crypto.ECPoint, error) { @@ -129,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() && @@ -137,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 new file mode 100644 index 000000000..9f089d3ff --- /dev/null +++ b/ecdsa/resharing/messages_test.go @@ -0,0 +1,260 @@ +// 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 ( + "math/big" + "strings" + "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" +) + +// 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: make([]byte, 32), + } + 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") + } + + 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") + } +} + +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 +// 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") + } +} + +// 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 6358512da..7876b595a 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,24 @@ 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. + // + // 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 || 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() + if !round.ReSharingParams().IsOldCommittee() { return nil } @@ -66,10 +86,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 @@ -89,19 +110,30 @@ 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 + } + // 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()) } - round.oldOK[j] = true // 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()) } @@ -111,8 +143,9 @@ 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 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..1abc5c977 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,10 @@ func (round *round2) Start() *tss.Error { round.started = true round.resetOK() // resets both round.oldOK and round.newOK round.allOldOK() + // 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 @@ -69,10 +74,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.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) + modProof := preParams.PaillierSK.ModProof(contextI) // NTildei = (2p+1) * (2q+1) // phi(NTildei) = ((2p+1) - 1) * ((2q+1) - 1) = 2p * 2q @@ -85,7 +91,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 +138,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 +146,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 +164,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 +176,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_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 74e220e84..56d118c09 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.AppendUint64ToBytesSlice(round.temp.ssid, 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.AppendUint64ToBytesSlice(round.temp.ssid, 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 @@ -255,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 { @@ -262,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 } @@ -278,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 5d01bd64e..394691de1 100644 --- a/ecdsa/resharing/round_5_new_step_3.go +++ b/ecdsa/resharing/round_5_new_step_3.go @@ -49,6 +49,7 @@ func (round *round5) Start() *tss.Error { if common.Eq(Pi.KeyInt(), Pj.KeyInt()) { continue } + contextJ := common.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) go func(j int, ch chan<- proofOut) { r4msg1 := round.temp.dgRound4Message1s[j].Content().(*DGRound4Message1) @@ -59,21 +60,25 @@ 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} + 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) + 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} @@ -119,6 +124,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 { @@ -126,14 +132,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/resharing/rounds.go b/ecdsa/resharing/rounds.go index 20b2f5e7c..3a820943a 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...).FillBytes(make([]byte, 32)) +} diff --git a/ecdsa/signing/finalize.go b/ecdsa/signing/finalize.go index b2fc670f2..4b2c0c6e8 100644 --- a/ecdsa/signing/finalize.go +++ b/ecdsa/signing/finalize.go @@ -61,14 +61,16 @@ 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() + 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..9fe9f2741 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,11 +103,20 @@ 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 +// NewLocalPartyWithKDD returns a party with key derivation delta for HD support. +// +// 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, @@ -111,7 +124,10 @@ func NewLocalPartyWithKDD( keyDerivationDelta *big.Int, out chan<- tss.Message, end chan<- common.SignatureData, + fullBytesLen ...int, ) tss.Party { + validatedFullBytesLen := validateFullBytesLen("NewLocalPartyWithKDD", msg, params, fullBytesLen) + partyCount := len(params.Parties().IDs()) p := &LocalParty{ BaseParty: new(tss.BaseParty), @@ -136,6 +152,7 @@ func NewLocalPartyWithKDD( // temp data init p.temp.keyDerivationDelta = keyDerivationDelta p.temp.m = msg + 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) @@ -147,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 51b838737..306accf65 100644 --- a/ecdsa/signing/local_party_test.go +++ b/ecdsa/signing/local_party_test.go @@ -8,9 +8,11 @@ package signing import ( "crypto/ecdsa" + "encoding/hex" "fmt" "math/big" "runtime" + "strings" "sync/atomic" "testing" @@ -56,11 +58,17 @@ 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 + 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(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 { @@ -120,8 +128,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, parties[0].data.M) t.Log("ECDSA signing test done.") // END ECDSA verify @@ -166,10 +175,12 @@ 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) + 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 { @@ -239,6 +250,121 @@ 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, 32).(*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_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_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 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 TestNewLocalPartyWithKDD_FullBytesLen_Required(t *testing.T) { + defer func() { + r := recover() + if r == nil { + 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 is required") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = 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) { s := big.NewInt(123456789) normalizedS := padToLengthBytesInPlace(s.Bytes(), 32) diff --git a/ecdsa/signing/round_1.go b/ecdsa/signing/round_1.go index 920930db2..f4f938d4e 100644 --- a/ecdsa/signing/round_1.go +++ b/ecdsa/signing/round_1.go @@ -45,6 +45,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) + } + round.temp.ssid = ssid k := common.GetRandomPositiveInt(round.Params().EC().Params().N) gamma := common.GetRandomPositiveInt(round.Params().EC().Params().N) @@ -63,7 +78,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.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)) } @@ -80,20 +96,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/ecdsa/signing/round_2.go b/ecdsa/signing/round_2.go index 79702e0dd..4433ef125 100644 --- a/ecdsa/signing/round_2.go +++ b/ecdsa/signing/round_2.go @@ -12,6 +12,7 @@ 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" ) @@ -30,6 +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.AppendUint64ToBytesSlice(round.temp.ssid, uint64(i)) for j, Pj := range round.Parties().IDs() { if j == i { continue @@ -54,7 +56,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 +87,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 +120,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..92dfc8648 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.AppendUint64ToBytesSlice(round.temp.ssid, 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..8955a72eb 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.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)")) } @@ -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..963378b94 100644 --- a/ecdsa/signing/round_5.go +++ b/ecdsa/signing/round_5.go @@ -46,7 +46,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.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) + ok = proof.VerifyWithSession(contextJ, bigGammaJPoint) if !ok { return round.WrapError(errors.New("failed to prove bigGamma"), Pj) } @@ -96,16 +97,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..95da829a0 100644 --- a/ecdsa/signing/round_6.go +++ b/ecdsa/signing/round_6.go @@ -11,6 +11,7 @@ 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" ) @@ -23,11 +24,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.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)")) } - 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 +42,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..fb2c86d30 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.AppendUint64ToBytesSlice(round.temp.ssid, 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..82f7d6b63 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...).FillBytes(make([]byte, 32)), nil +} 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/local_party_test.go b/eddsa/keygen/local_party_test.go index 0a5adf1d2..d499ec7eb 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 { @@ -147,9 +174,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()) @@ -159,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() @@ -169,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 } @@ -180,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.") diff --git a/eddsa/keygen/round_1.go b/eddsa/keygen/round_1.go index a799d27c3..ed8d0af3c 100644 --- a/eddsa/keygen/round_1.go +++ b/eddsa/keygen/round_1.go @@ -38,6 +38,22 @@ func (round *round1) Start() *tss.Error { Pi := round.PartyID() i := Pi.Index + // 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) + } + round.temp.ssidNonce = new(big.Int).Set(nonce) + 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 @@ -89,17 +105,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..e5abf12d3 100644 --- a/eddsa/keygen/round_2.go +++ b/eddsa/keygen/round_2.go @@ -11,6 +11,7 @@ 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" ) @@ -45,7 +46,8 @@ func (round *round2) Start() *tss.Error { } // 5. compute Schnorr prove - pii, err := schnorr.NewZKProof(round.temp.ui, round.temp.vs[0]) + 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)")) } @@ -69,21 +71,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/keygen/round_3.go b/eddsa/keygen/round_3.go index 7a82f8831..8dfa37ff9 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.AppendUint64ToBytesSlice(round.temp.ssid, uint64(j)) // 6-9. go func(j int, ch chan<- vssOut) { // 4-10. @@ -79,20 +80,19 @@ 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} 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..33e87ecc5 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...).FillBytes(make([]byte, 32)), nil +} diff --git a/eddsa/resharing/local_party_test.go b/eddsa/resharing/local_party_test.go index 4aa8f85cf..726dd118d 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,10 +163,18 @@ 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) + } + }() + signCeremonyNonce := big.NewInt(1) for j, signPID := range signPIDs { params := tss.NewParameters(tss.Edwards(), signP2pCtx, signPID, len(signPIDs), newThreshold) - P := signing.NewLocalParty(big.NewInt(42), params, signKeys[j], signOutCh, signEndCh).(*signing.LocalParty) + params.SetSessionNonce(signCeremonyNonce) + 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 { @@ -198,7 +207,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,13 +220,14 @@ signing: Y: pkY, } - newSig, err := edwards.ParseSignature(signData.Signature) + newSig, err := edwards.ParseSignature(signData.signature) if err != nil { 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.") @@ -228,3 +238,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_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_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/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..f865ae28c 100644 --- a/eddsa/signing/finalize.go +++ b/eddsa/signing/finalize.go @@ -43,7 +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() - round.data.M = round.temp.m.Bytes() + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + round.data.M = mBytes pk := edwards.PublicKey{ Curve: round.Params().EC(), @@ -51,7 +53,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..cf06efff0 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 @@ -59,16 +60,29 @@ type ( // round 3 r *big.Int + + ssid []byte + ssidNonce *big.Int } ) +// 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, key keygen.LocalPartySaveData, out chan<- tss.Message, end chan<- common.SignatureData, + fullBytesLen ...int, ) tss.Party { + validatedFullBytesLen := validateFullBytesLen("NewLocalParty", msg, params, fullBytesLen) + partyCount := len(params.Parties().IDs()) p := &LocalParty{ BaseParty: new(tss.BaseParty), @@ -86,10 +100,33 @@ func NewLocalParty( // temp data init p.temp.m = msg + 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 35a18eee4..8b2757052 100644 --- a/eddsa/signing/local_party_test.go +++ b/eddsa/signing/local_party_test.go @@ -7,8 +7,10 @@ package signing import ( + "encoding/hex" "fmt" "math/big" + "strings" "sync/atomic" "testing" @@ -59,12 +61,16 @@ 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 + 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).(*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 { @@ -132,8 +138,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, parties[0].data.M) t.Log("EDDSA signing test done.") // END EDDSA verify @@ -142,3 +149,115 @@ 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, 32).(*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_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_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 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) +} + +func TestNewLocalParty_FullBytesLen_Required(t *testing.T) { + defer func() { + r := recover() + if r == nil { + 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 is required") { + t.Fatalf("unexpected panic message: %v", err) + } + }() + _ = 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 7af1d8078..5df6c3397 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,22 @@ func (round *round1) Start() *tss.Error { 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) + } + round.temp.ssid = ssid + // 1. select ri ri := common.GetRandomPositiveInt(round.Params().EC().Params().N) @@ -56,16 +73,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..ea8c6bd11 100644 --- a/eddsa/signing/round_2.go +++ b/eddsa/signing/round_2.go @@ -11,6 +11,7 @@ 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" ) @@ -32,7 +33,8 @@ func (round *round2) Start() *tss.Error { } // 2. compute Schnorr prove - pir, err := schnorr.NewZKProof(round.temp.ri, round.temp.pointRi) + 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)")) } @@ -53,16 +55,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..a5d7d3e39 100644 --- a/eddsa/signing/round_3.go +++ b/eddsa/signing/round_3.go @@ -12,6 +12,7 @@ import ( "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 +39,7 @@ func (round *round3) Start() *tss.Error { continue } + 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()} @@ -50,15 +52,15 @@ 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) } - ok = proof.Verify(Rj) + ok = proof.VerifyWithSession(contextJ, Rj) if !ok { return round.WrapError(errors.New("failed to prove Rj"), Pj) } @@ -77,7 +79,9 @@ func (round *round3) Start() *tss.Error { h.Reset() h.Write(encodedR[:]) h.Write(encodedPubKey[:]) - h.Write(round.temp.m.Bytes()) + mBytes := make([]byte, round.temp.fullBytesLen) + round.temp.m.FillBytes(mBytes) + h.Write(mBytes) var lambda [64]byte h.Sum(lambda[:0]) @@ -101,16 +105,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/eddsa/signing/rounds.go b/eddsa/signing/rounds.go index 57adcd8d7..5917ff10a 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...).FillBytes(make([]byte, 32)), nil +} 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; } /* 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..d961c264e 100644 --- a/tss/params.go +++ b/tss/params.go @@ -8,8 +8,11 @@ package tss import ( "crypto/elliptic" + "math/big" "runtime" "time" + + "github.com/bnb-chain/tss-lib/common" ) type ( @@ -21,6 +24,10 @@ type ( threshold int concurrency int safePrimeGenTimeout time.Duration + // 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 } ReSharingParameters struct { @@ -85,6 +92,49 @@ 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. It must be called before Start. +// +// 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. 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 || 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 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") + } + 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..358fce167 --- /dev/null +++ b/tss/params_test.go @@ -0,0 +1,68 @@ +// 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-with-128-bits") + + params.SetSessionNonceBytes(sessionID) + + expected := new(big.Int).SetBytes(common.SHA512_256(sessionID)) + assert.Equal(t, expected, params.SessionNonce()) +} + +func TestSetSessionNonceBytesRejectsShortSessionID(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{}) + }) + 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)) + }) +} 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" } // -----