Repository navigation
Conversation
|
|
1e9b6e9 to
bcf118a
Compare
|
@claude review always |
This comment was marked as outdated.
This comment was marked as outdated.
15a3678 to
7f281e8
Compare
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
7f281e8 to
b043dee
Compare
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
ba73606 to
9642a04
Compare
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
9642a04 to
3036ebc
Compare
|
@claude review |
|
Claude finished @vibhavgo's task in 33s —— View job Review of 3036ebcI read the full diff of Earlier findings
Minor notes (optional)
No changes requested from my side. |
3036ebc to
f2807d4
Compare
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
f2807d4 to
1413b87
Compare
|
@claude review |
|
Claude finished @vibhavgo's task in 30s —— View job Review of 1413b87I read Earlier findings
Optional notes (no change needed)
No blocking issues from my side. |
94e5c29 to
19a7b55
Compare
|
@claude review |
|
Claude finished @vibhavgo's task in 25s —— View job Review of 19a7b55I re-read No new blocking issues in what I read.
Optional notes
No changes requested from my side on the express part. |
19a7b55 to
ba95d2d
Compare
An attacker reaching the signer's tssshare port could replay one encryptedOShare with different signables or D-shares: since sign() computes s = m*k + r*omicron with r derived from oShare.Gamma + dShare.Gamma, two signatures solve for k and omicron, and with an attacker-supplied Paillier modulus yield the user's key share. wShare/oShare envelopes now carry adata binding <state>:<createdEpochMs>:<sha256(signable)>: <derivationPath>, validated (binding and age, with v1 escape decoding) before decryption; counterparty Paillier moduli (challenge n, A-share n) must match the signer's own bitgoNShare.n; the express signer marks MuDelta/S ciphertexts consumed exactly once in a file-backed registry that survives restarts, keyed on the canonical decoded bytes that decryption consumes (v1 iv truncated to the effective CCM nonce), with consumption recorded only after the envelope fully authenticates so junk requests never reach the registry. Legacy relay bodies and legacy direct callers keep working; the oShare inherits the wShare's binding when no txRequest is available. References: WAL-2459
ba95d2d to
bab26c9
Compare
Summary
Closes the GG18 (ECDSA MPCv1) external-signer replay hole: an attacker who can reach the signer's
tssshareport could replay oneencryptedOSharewith different signables or D-shares, producing twosvalues that solve fork/omicronand — with an attacker-supplied Paillier modulus — the user's key share.Linear: WAL-2459
Changes
EcdsaUtils):wShare/oShareenvelopes created by the signer carryadata = <domain separator>:sha256(signable):derivationPathand later rounds validate it with the existingvalidateAdatabefore decryption — an envelope can only be replayed against the exact signable it was created for. Message signing is bound defensively.bitgoChallenge.n(K and MuDelta rounds) andaShareFromBitgo.n(MuDelta) must match the signer's own keygen-timebitgoNShare.n.consumedSharesRegistry.tsmarks each MuDeltawShare/ S-roundoShareciphertext consumed exactly once, before the share runs (fail-closed). File-backed so protection survives signer restarts; ENOENT = clean first run, read/parse failures fail closed; entries pruned on persist (30-day TTL); keys exclude attacker-controlled fields (e.g.walletId) so relabeled replays are rejected.createOfflineMuDeltaShare/CustomMuDeltaShareGeneratingFunctionaccept the new fields optionally; legacy relay bodies (top-leveltxRequest, route-injectedprv) still get full adata + modulus validation; legacy direct SDK callers keep unchanged behavior. Old relay + new signer and new relay + old signer interoperate.signEcdsaTssUsingExternalSigner) MuDelta call mirrors the K/S param shape; relay generators unchanged (they spread full params).Test Plan
yarn run unit-test --scope bitgo -- -- --grep "createOffline"— 14 passingyarn run unit-test --scope @bitgo/express -- -- --grep "externalSign"— full flow + replay rejections passyarnbuild clean;tsc --noEmitand eslint clean on all touched packagesOperational requirements & documented residuals
Compat note:
SigningMaterial.bitgoNShareis a required type field, so the K-roundbitgoNShareassert breaks no supported caller — material lacking it cannot type-check and cannot complete GG18 signing (it is consumed bykeyDerive).Registry location is configurable (
--signerRegistryFilePath/BITGO_SIGNER_REGISTRY_FILE_PATH, default<signerFileSystemPath>.consumedShares.json). The signer process never assumes write access there: if the location is not writable, a warning is logged and replay protection degrades to in-memory only (lost on restart) — no startup or request failures. Deployments wanting full restart protection should point the flag at a writable location.One signer process per registry file is required.
ConsumedShareRegistry.load()reads the file once per process andpersist()rewrites it from the in-memory map, so sharing one registry file across replicas can lose records (cross-process locking is out of scope).Legacy direct SDK callers that pass neither
txRequestnorprvskip Paillier-modulus validation (same as v1 behavior); the one-time consumption registry remains the replay defense on the signer route, which always has both (route-injected).A relay retry after a lost signer response restarts the session from K (fail-closed); registry entries and envelope adata share a 30-day TTL, so pruning never drops an entry that could still be accepted.
Node 27-nightly WASM compatibility failures in the EdDSA suites are pre-existing and unrelated to this diff.