-
Notifications
You must be signed in to change notification settings - Fork 1
Integrate BNB tss-lib hardening #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
db3fca8
4758b79
f2e1b3f
78ca61d
93e3cb4
a71b1cd
92320a8
21efbe2
762507e
b6b48b2
78c9528
c541bda
0835914
7bf584b
d0d3aed
d2fe895
c62bf90
ae7075f
caa64fc
d4555c8
f2a959a
8c235ef
56e25da
16d848b
630a7e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| name: Go-fmt | ||
| on: | ||
| push: | ||
| branches: | ||
| - master | ||
| - release/* | ||
| pull_request: | ||
| branches: | ||
| - master | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| name: Go Test | ||
| on: | ||
| push: | ||
| branches: | ||
| - master | ||
| - release/* | ||
| pull_request: | ||
| branches: | ||
| - master | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # BNB Hardening Integration Report | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably something unintentionally committed? |
||
|
|
||
| ## Scope | ||
|
|
||
| - Threshold base: `2e712689cfbeefede15f95a0ec7112227d86f702` | ||
| - BNB upstream head compared: `3f677ff761fcf692edb0243a5d812930844d879a` | ||
| - Common ancestor: `afbe264b44b63155a864dbe0171040c66e442963` | ||
| - Goal: port applicable security and correctness hardening without replacing Threshold's Paillier/NTilde remediation or weakening `ModProof`/`FactorProof`. | ||
|
|
||
| ## Compatibility Notice | ||
|
|
||
| This is a protocol/wire compatibility break for proof transcripts. Proofs whose Fiat-Shamir challenges now use tagged hashing or session context will not verify across mixed old/new versions, even where the Go API remains source-compatible through variadic arguments. Operators should roll this out as a coordinated protocol upgrade rather than mixing parties from before and after this PR in the same keygen, signing, or resharing ceremony. | ||
|
|
||
| ## Ported Or Manually Adapted | ||
|
|
||
| - `3d95e54` / PR `#252`, ECDSA protocol security updates: manually adapted tagged challenges, MtA/range-proof validation, session-context plumbing, and proof boundary checks while preserving Threshold's existing Paillier/NTilde proof model. | ||
| - `1a14f3a` / PR `#256`, ECDSA proof session byte: manually adapted proof-session APIs for DLN, Schnorr, MtA, Paillier mod proof, and factor proof. Public callers remain source-compatible through variadic session parameters, but generated proof transcripts are not wire-compatible with old versions. | ||
| - `ff989bf` / PR `#257`, tagged hash encoding: ported length-delimited tagged hashing as `common.SHA512_256i_TAGGED`. | ||
| - `f3aad28` / PR `#276`, nil `String()` panic: ported `BaseParty.String()` nil-round guard. | ||
| - `409542e` / PR `#282`, round update correctness: ported the `round.ok` accumulation fix for all non-terminal ECDSA/EdDSA keygen, signing, and resharing rounds, plus the resharing party-0 broadcast nil guard. | ||
| - `9acd90b`, `2f294cf`, `6b92e7d`, `c0de534` / PR `#284`, leading-zero message signing: manually adapted for ECDSA and EdDSA with variadic `fullBytesLen` parameters that are now required at runtime and bounded to the curve order byte length. EdDSA now also hashes the full-length message bytes in round 3. | ||
| - `843de68` / PR `#291`, VSS threshold-size validation: ported `len(vs) == threshold+1` verification and added test coverage. | ||
| - `5d01446` / PR `#289`, range-proof update: ported MtA range-proof GCD, interval, lower-bound, non-one, and tagged challenge checks. | ||
| - `4878da5` / PR `#324`, VSS reconstruction fix: ported `threshold+1` reconstruction requirement and updated ECDSA/EdDSA keygen fixture tests. | ||
| - `b59ed36`, session context for DLN and MtA proofs: manually adapted with optional session contexts and focused replay/session-mismatch tests. | ||
| - `fc38979`, GG20 SSID uniqueness: ported `tss.Parameters.SessionNonce` / `SetSessionNonce`, added `SetSessionNonceBytes` (requires session IDs of at least 16 bytes), and wired ECDSA/EdDSA keygen/signing plus ECDSA resharing SSID derivation. Keygen, ECDSA resharing, and signing now require a positive `SetSessionNonce` and fail closed if it is not set — the previous zero fallback (keygen/resharing) and SHA512_256(messageBytes) fallback (signing) caused two ceremonies with otherwise-identical inputs to derive the same SSID, breaking the session-binding property the proofs rely on. | ||
| - `685c2af`, canonical EC coordinates: ported rejection of EC coordinates outside `[0, P)`. | ||
| - `5d0d0f3`, EdDSA nil-pointer fix: ported by checking `NewECPoint` errors before `EightInvEight()`. | ||
| - Post-review cleanup: party-specific proof contexts now append fixed-width uint64 party indexes so party 0 does not collapse to the bare SSID. The earlier signing default that derived an SSID nonce from full message bytes has been removed in favour of the fail-closed requirement above; the helpers that produced it (`messageBytes`, `messageSessionNonce`) are gone with it. | ||
|
|
||
| ## Already Covered Or Superseded | ||
|
|
||
| - `c84c096` / PR `#323`, modproof checker: Threshold's Paillier proof implementation already had the key Jacobi/non-prime validation from its GHSA-h24c-6p6p-m3vx remediation. Threshold's stronger `ModProof` coverage for both Paillier `N` and `NTilde` was kept. | ||
| - `e0e4299`, EdDSA keygen error aggregation: current Threshold code already had the corrected behavior. | ||
| - `0629cff`, `773b6af`, `b7b73a0`, `27922e0`, `4c83ace`, `c8136c9`, `8a87b22`, `f67a429`, `002397d`, `28d0622`, `bddf60d`: style, comment, logging, gofmt, minor optimization, or merge-only changes with no security behavior to port. | ||
|
|
||
| ## Skipped | ||
|
|
||
| - `faf1884`, `c23246e`: module path bumps to `/v2` and `/v3`; skipped to preserve Threshold compatibility. | ||
| - `fbb0ef7`: changes `SignatureData` channels to pointers; skipped as public API churn not required for the hardening. | ||
| - `b8d526d`, `8abf1d5`, `6c233c6`, `87f7e12`: dependency and random-source API churn; skipped except where existing Threshold APIs already supported the needed behavior. | ||
| - `7113b68`, `d0325a1`, `dca2ac4`: repository metadata, CI, or security-report housekeeping. | ||
| - `3709c25`, `7a10240`, `0735081`, `3f677ff` / PR `#328`: broad optional constant-time framework. Not ported in this pass because it adds a new dependency, broad Paillier/MtA rewrites, and is default-disabled upstream. Treat as a separate follow-up security project with benchmarking and side-channel review. | ||
| - Merge-only commits `b15a0cf`, `c76a1a5`, `b79b349`, `d6e2aa9`, `ba5b734`: no direct changes to port beyond their constituent commits above. | ||
|
|
||
| ## Semantic Differences From BNB | ||
|
|
||
| - Threshold's Paillier/NTilde `ModProof` and `FactorProof` remediation was retained. No BNB no-proof escape hatches were introduced. | ||
| - Session parameters were added as variadic arguments to preserve existing public call sites. This is API source-compatible for callers, but not wire-compatible for proof transcripts; callers must set `Parameters.SessionNonce()` before starting keygen, signing, or ECDSA resharing. | ||
| - ECDSA/EdDSA signing constructors still accept `fullBytesLen` as a variadic argument for source compatibility, but exactly one positive value is required at runtime so all signers agree on message byte width before the protocol starts. | ||
| - Keygen, signing, and ECDSA resharing SSIDs use `Parameters.SessionNonce()`. Callers must provide a unique agreed nonce, for example via `SetSessionNonceBytes`, for every ceremony. | ||
| - ECDSA resharing now broadcasts the locally-derived SSID in `DGRound1Message` so the new committee can reject old-committee broadcasts whose SSID differs from the local protocol context. | ||
| - `common.RejectionSample` keeps BNB's function name for porting clarity, but this implementation is modular reduction rather than a looping rejection sampler. | ||
| - Constant-time operations are not included and remain a residual follow-up. | ||
|
|
||
| ## Tests | ||
|
|
||
| - `go test ./crypto/... ./ecdsa/keygen ./ecdsa/signing ./eddsa/signing` passed. | ||
| - `go test ./eddsa/keygen ./eddsa/resharing` passed after updating EdDSA VSS threshold tests and resharing nil guard. | ||
| - `go test ./ecdsa/resharing` passed after the analogous resharing guard. | ||
| - `go test ./common ./crypto/paillier ./crypto/mta ./ecdsa/keygen ./ecdsa/resharing ./ecdsa/signing ./eddsa/keygen ./eddsa/signing ./eddsa/resharing` passed after review fixes. | ||
| - `go test ./...` passed. | ||
| - `go vet ./...` passed. | ||
|
|
||
| Added or updated focused tests cover: | ||
|
|
||
| - DLN, Schnorr, Paillier mod proof, factor proof, and MtA range proof session mismatch/replay failures. | ||
| - Non-invertible malformed factor-proof bases returning errors instead of panicking. | ||
| - MtA range-proof malformed ciphertext and proof-value boundary failures. | ||
| - ProofBobWC malformed lower-bound, zero-value, and curve-mismatch failures. | ||
| - ProofBob and ProofBobWC session mismatch/replay failures. | ||
| - VSS `threshold+1` verification/reconstruction behavior. | ||
| - Non-canonical EC coordinate rejection. | ||
| - ECDSA and EdDSA leading-zero message signing. | ||
|
|
||
| ## Residual Risks | ||
|
|
||
| - Applications must call `SetSessionNonce` or `SetSessionNonceBytes` before keygen, signing, and ECDSA resharing; those protocols now fail closed without it. | ||
| - The optional constant-time upstream work is not integrated. | ||
| - EdDSA resharing has no SSID-bound proof transcript in this port. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loose idea for follow-up work: Maybe a good move would be to remove all the code that is not used by
... and probably more. Getting rid of those packages greatly limits the potential attack surface, and will make the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in #5 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()) | ||
| } | ||
|
Comment on lines
+101
to
+106
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a bug, but a potential code robustness issue - That said, instead of suggesting |
||
|
|
||
| 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()) | ||
| } | ||
|
Comment on lines
+132
to
+134
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar comment here - |
||
| return new(big.Int).SetBytes(state.Sum(nil)) | ||
| } | ||
|
|
||
| func SHA512_256iOne(in *big.Int) *big.Int { | ||
| var data []byte | ||
| state := crypto.SHA512_256.New() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, there is a new upstream PR that may be relevant: bnb-chain#332
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #4