Repository navigation
fix: address PR #37 review findings - #41
Conversation
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M003 [P3] common/constant_time.go:292: GetCTModInt keeps an exported, unbounded, never-evicted global cache keyed by modulus - M004 [P3] common/constant_time.go:310: NewCTModIntWithPhi duplicates NewCTModInt's body and has one caller - M007 [P3] common/validation.go:21-30,39-41: The 65,536-bit modulus ceiling does not bound verifier CPU as its comment claims (ProbablyPrime is ... - M008 [P3] common/validation.go:33-48: Modulus-policy check is a two-call protocol (ceiling before primality) hand-ordered at each of 6 verifiers - M010 [P3] common/constant_time.go:13-14,34-38: common/constant_time.go header: garbled COVERAGE sentence, change-narration with no issue link, stale Encrypt ... - M014 [P3] common/constant_time_test.go:431-555: TestMulCTTimingConsistency, TestModInverseCTTimingConsistency and TestExpCTTimingConsistency can never fail - M017 [P3] common/random.go:80,98,123-130: New nil-return contracts are undocumented - M019 [P3] common/validation.go:13-18,24-26,33-38: common/validation.go modulus-policy comments: illogical Min rationale and false O(bitLen) claims - M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling - M093 [P3] ecdsa/signing/round_5.go:77: CHANGELOG, constant_time.go and round_5.go disagree on whether rx is secret; it is derived from public values ...
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M002 [P3] common/constant_time.go:207-215: The secret-wipe behavior advertised by 873b8ad has no assertion - M020 [P3] crypto/paillier/mod_proof.go:96: ModProof exponent wipe is not deferred, so a panic skips it; its doc comment says it also runs on abort - M021 [P2] crypto/paillier/paillier.go:194: Constant-time exponents are padded to N.BitLen() (2048) instead of public protocol bounds: HomoMult and the ... - M023 [P3] crypto/paillier/key_reuse_regression_test.go:81-119: TestKeyReuseAfterKeyValuesReplaced and TestByValueKeyCopyIsIndependent use the public-key N² cache only for ... - M026 [P3] crypto/paillier/mod_proof.go:368-405: Four standalone QR helpers are test-only copies of the context methods - M027 [P3] crypto/paillier/paillier.go:121-123,170-180,233-239: No test references ErrMessageTooLong or ErrMessageMalFormed - M035 [P3] crypto/paillier/mod_proof.go:65-70,232-236: mod_proof.go comments narrate the change history instead of describing the current design - M037 [P3] crypto/paillier/paillier.go:157,193,196: runtime.KeepAlive(publicKey) appears three times with no comment - M038 [P3] crypto/paillier/paillier.go:200-212: HomoAdd lacks the checkPaillierModulus guard the other Paillier operations gained - M039 [P3] crypto/paillier/paillier.go:259-271: The ErrMalformedKey doc says a non-invertible decryption coefficient is reported "consistently by both" modes - M040 [P3] crypto/paillier/paillier.go:284-285: The Proof doc runs two sentences together with no break: "...square-free from 3.1" then "It panics if..." - M050 [P2] crypto/mta/share_protocol_test.go:183-203: "Overwide ... must fail before exponentiation" MtA tests cannot tell the cheap bound check from the later ... - M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M021 [P2] crypto/paillier/paillier.go:194: Constant-time exponents are padded to N.BitLen() (2048) instead of public protocol bounds: HomoMult and the ... - M042 [P2] crypto/mta/proof_bob_verifier.go:74-79: Nothing tests the upper edge of the opt-in historical-compat bound T1 < (q+1)*N - M043 [P2] crypto/mta/legacy_bob_compatibility_test.go:119-138: The security-v2 Bob T1 bound (q^7 + 1) is never exercised by a test - M044 [P3] crypto/mta/proof_bob_verifier.go:44-60: Bob and RangeAlice verifiers duplicate the same modulus/generator/ciphertext preamble - M045 [P3] crypto/mta/proof_bob_verifier.go:84-95: The default tight Bob bound is justified with the wrong prover, and "legacy" names two different provers - M046 [P3] crypto/mta/proofs.go:40-52,61-62,76-78,116: Constant-time path panics on even/nil/degenerate moduli in exported provers; 3b609ae no-panic claim overstated - M048 [P3] crypto/mta/share_protocol.go:105-185: Four near-identical "verify, then decrypt, then reduce mod q" functions - M049 [P3] crypto/mta/share_protocol.go:144-148: AliceEndLegacy doc comment has a truncated sentence about the historicalBobCompat=false case - M050 [P2] crypto/mta/share_protocol_test.go:183-203: "Overwide ... must fail before exponentiation" MtA tests cannot tell the cheap bound check from the later ... - M056 [P3] crypto/mta/proofs.go:92: Review-process labels appear with no definition in production comments, CHANGELOG, READMEs and CI: "PRIOR", ... - M058 [P3] crypto/mta/share_protocol.go:149-185: AliceEndLegacy and AliceEndWCLegacy with historicalBobCompat=false rejecting a historical-witness proof, and ... - M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M059 [P2] crypto/dlnproof/proof.go:92-97: The #34 modulus policy (2048..65536 bits, odd, composite) is tested at one verifier and only for the ceiling - M060 [P2] crypto/dlnproof/proof_test.go:37-95: DLN Verify reject tests use N=23, so the 2048-bit floor rejects first and the named T/Alpha/nil checks are ... - M062 [P3] crypto/ckd/child_key_derivation.go:108-123: ckd curve type switch returns a nil-coordinate key with nil error for non-v2 secp256k1 curve objects
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M075 [P2] ecdsa/keygen/local_party.go:71: No test shows the keygen/signing constructors enforce the explicit protocol-mode rule (the headline runtime ... - M077 [P2] ecdsa/keygen/rounds.go:104-121: Legacy keygen transcript selection (proofSession/proofContext return nil) is not pinned by any test - M079 [P3] ecdsa/keygen/prepare.go:129: Keygen ring-Pedersen trapdoor inverse (beta = alpha^-1 mod pq) stays variable-time, contrary to Breaking ... - M083 [P3] ecdsa/keygen/rounds.go:125-131: The keygen getSSID doc still says "Callers must invoke this exactly once, in round 1", but legacy mode never ... - M088 [P3] ecdsa/signing/local_party.go:~110-122: The constructor docs omit the new caller obligation, which is the main compatibility break - M097 [P3] ecdsa/signing/signing_boundaries_test.go:32: Mixed-mode ceremonies (a legacy party among security-v2 peers, or the reverse) are not tested
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M075 [P2] ecdsa/keygen/local_party.go:71: No test shows the keygen/signing constructors enforce the explicit protocol-mode rule (the headline runtime ... - M084 [P2] ecdsa/signing/finalize.go:82: No test inspects the delivered *common.SignatureData (breaking change 9): R/S/recovery id/M width and the ... - M085 [P2] ecdsa/signing/round_5.go:102-111: The #36 store-before-emit ordering in signing rounds 5 and 7 has no regression test - M086 [P3] ecdsa/signing/local_party.go:156: The constructor copies msg (lines 157-159) to stop caller mutation changing the signing context, but ... - M088 [P3] ecdsa/signing/local_party.go:~110-122: The constructor docs omit the new caller obligation, which is the main compatibility break - M093 [P3] ecdsa/signing/round_5.go:77: CHANGELOG, constant_time.go and round_5.go disagree on whether rx is secret; it is derived from public values ... - M094 [P2] ecdsa/signing/round_ct_wiring_test.go:27: The signing "CT wiring" tests cannot fail if the constant-time branch is removed - M096 [P3] ecdsa/signing/signing_boundaries_test.go:297-376: The m = q / q-1 boundaries are covered only in legacy mode and not for the SSID or full-ceremony effect - M097 [P3] ecdsa/signing/signing_boundaries_test.go:32: Mixed-mode ceremonies (a legacy party among security-v2 peers, or the reverse) are not tested - M104 [P3] ecdsa/signing/signing_boundaries_test.go:361-367: The m = q-1 accept subtest receives from out with select { case m := <-out: } and no timeout
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M056 [P3] crypto/mta/proofs.go:92: Review-process labels appear with no definition in production comments, CHANGELOG, READMEs and CI: "PRIOR", ... - M122 [P2] testdata/legacy_transcript/verify.sh:22-37: The CI "bidirectional" oracle never checks that HEAD legacy provers produce proofs the historical verifier ... - M123 [P3] testdata/legacy_transcript/README.md: Mixed-binary evidence is narrower than claimed: no live mixed keygen, stops at round 8 - M124 [P3] testdata/legacy_transcript/README.md:30-46: The README describes the proof-source digest as SHA-256 over the listed repository files, which implies a ... - M125 [P3] testdata/legacy_transcript/oracle/main.go:225-227: The oracle vector holds no wide-witness historical Bob/BobWC proof, the shape the compat toggle exists for - M127 [P3] testdata/legacy_transcript/README.md:93,145,160: The fixture shape is called "2-of-20" in some places and "2-of-2" in others - M128 [P3] testdata/legacy_transcript/verify_mixed_interop.sh:16-17: The script header says the compat-on party accepts "the identical exchange" - M129 [P2] .github/workflows/test.yml:99-103: The live mixed-binary harness (verify_mixed_interop.sh) and its own tests never run in CI - M130 [P3] .github/workflows/test.yml:42,74,96: Documented Go 1.25.7 minimum is never built in CI
Fix the findings below from the review of PR #37 (ID, severity, review location: issue). - M001 [P2] common/constant_time.go:14-40: CHANGELOG and PR risk text overclaim constant-time coverage: MtA proof blinds and secret EC scalar ... - M015 [P3] common/int.go:104: CHANGELOG "Added" says IsInInterval and tss.SameCurve back the hardened range checks; IsInInterval has no ... - M024 [P2] crypto/paillier/mod_proof.go:184-199: CHANGELOG Breaking Change 4 says ModProof/FactorProof use HashToNTagged; no production code calls it - M056 [P3] crypto/mta/proofs.go:92: Review-process labels appear with no definition in production comments, CHANGELOG, READMEs and CI: "PRIOR", ... - M062 [P3] crypto/ckd/child_key_derivation.go:108-123: ckd curve type switch returns a nil-coordinate key with nil error for non-v2 secp256k1 curve objects - M067 [P3] crypto/commitments/commitment.go:55-71: CHANGELOG says canonical-generator checks were added in crypto/commitments; that package has none - M088 [P3] ecdsa/signing/local_party.go:~110-122: The constructor docs omit the new caller obligation, which is the main compatibility break - M093 [P3] ecdsa/signing/round_5.go:77: CHANGELOG, constant_time.go and round_5.go disagree on whether rx is secret; it is derived from public values ... - M113 [P3] tss/params.go:160-166: SetLegacyHistoricalBobCompatibility(false) panics outside legacy mode, contrary to the documented contract - M116 [P3] tss/params.go:226-227: SetSessionNonce godoc says keygen/signing fail closed without a nonce; true only in security-v2 - M123 [P3] testdata/legacy_transcript/README.md: Mixed-binary evidence is narrower than claimed: no live mixed keygen, stops at round 8 - M132 [P3] go.mod:1: Proposed release tag v2.0.0-threshold.1 is not consumable as a Go module version: module path has no /v2 ... - M136 [P2] README.md:63-98,145-150: README example reuses one Parameters (and session nonce) for keygen and signing, and never says Parameters is ... - M138 [P3] CHANGELOG.md:322-328: Breaking Changes 4 and 5 were not updated for dual-mode, and they contradict #3 (lines 335-350) and the ... - M140 [P3] CHANGELOG.md:478-480: CHANGELOG break-ledger blockquote: wrong verification baseline and incomplete list - M141 [P3] CHANGELOG.md:91-99: CHANGELOG claims four caller obligations are enforced at runtime; obligation 4 (and parts of 2/3) are not - M149 [P3] README.md:83: README keygen example passes *LocalPreParams where NewLocalParty takes LocalPreParams by value
- Samplers in common return nil instead of panicking when the limit is wider than the 5000-bit cap; MtA provers return an error then. The provers' own width pre-check and its mirrored constant are removed. - Replace paillier HomoMultWithBitLen with HomoMultBounded(m, c1, bound), which takes a public bound and rejects m outside [0, bound). New ErrInvalidBound sentinel. BobMid/BobMidWC pass q. - Signing round 2 checks local gamma and w are in [0, q) before the per-peer fan-out and blames no peer on failure. - Drop the six redundant ceiling pre-checks; IsUsableUnknownOrderModulus already checks width before ProbablyPrime. Unexport the helper. Modulus errors print the bit length, not the full value. - ckd decodes non-secp256k1 keys with elliptic.UnmarshalCompressed. - ModProof test hook proves the deferred exponent wipe runs on panic.
…y CI and harness comments
- DLN, Bob/Alice bound, verifier-gate and ModVerify policy tests use equation-valid inputs, so only the named check can reject them. - Exact T1 edges for default, compat and session bounds; S2/T2 shift tests; q-1 witness edges in constant-time mode. - Signing delivery check mutates all results before comparing; errors before the expected round fail the test; E2E helper has a deadline; constant-time E2E variants become one mode x CT table; KDD delta copy test. - Rename review-named test files after their behavior; replace hand- written panic helpers with testify; drop review labels.
|
Follow-ups from the second review pass (the repo has issues turned off, so they are recorded here): 1. Legacy MtA prover samples tau below q*NTilde: confirm zero-knowledge impactIn Context:
Ask: confirm with a cryptographer how much the narrow range weakens zero knowledge in practice, and decide whether to widen it in legacy mode (changing the 2. perf(mta): use Gamma^gamma = 1 + gamma*N in the Bob prover
Using it in the prover (and in the matching verifier term, if one exists) removes one ~2k-bit modular exponentiation. Proof bytes do not change. Estimated saving: about 8-10 ms per Ask: implement after #37 merges, measure with |
Follow-up to #37. It fixes the confirmed findings from a multi-lens review of the
devintegration (9ff573cagainstmaster). The review found no P0 or P1 issues. It confirmed 68 findings: 16 P2 and 52 P3. This PR fixes 65 of them. Three are not applied because they conflict with another fix or with a recorded decision; the reasons are listed below. A second multi-lens review of this PR found more gaps; they are fixed in the later commits and summarized under "Second review pass".Changes callers can see
paillier.PublicKey.HomoMultBounded(m, c1, bound)takes a public exclusive bound, rejectsmoutside[0, bound)(ErrMessageTooLong) and a bad bound (ErrInvalidBound), and pads the constant-time exponent tobound.BitLen().HomoMult(m, c1)isHomoMultBounded(m, c1, N).BobMid/BobMidWCpassq. The Bob and Alice provers padxandmtoq.BitLen()instead of the 2048-bit Paillier width;ykeeps its Paillier-width domain and padding. Proof bytes do not change: the new oracle leg below regenerates the historical vectors byte for byte. Measured with the newBenchmarkBobMid(constant-time on, security-v2 session,-benchtime=15x -count=3 -cpu=1):BobMid73.6 ms -> 58.7 ms (-20%),BobMidWC72.9 -> 58.7 ms, one full MtA exchange about -10%. This helps merge gate 3 but does not close it by itself.BobMid/BobMidWCrejectboutside[0, q).ProveBob/ProveBobWCrejectxoutside[0, q).ProveRangeAlicerejectsmoutside[0, q). The provers return errors instead of panicking for degeneratepk,N,NTildeorXinputs.common.GetRandomPositiveIntand the other samplers return nil instead of panicking when the limit is wider than the 5000-bit sampler cap, so the provers return an error for anyNTildethat is too wide. Signing round 2 checks the localgammaandware in[0, q)before the per-peer MtA work and blames no peer if they are not.paillier.HomoAddvalidates the key modulus the same wayEncrypt,HomoMultandDecryptalready do.common.IsUsableUnknownOrderModulusenforces the 65,536-bit ceiling itself, before the primality test. The six separate ceiling pre-checks in the verifiers are removed and the helper is unexported. Verifier errors print the modulus bit length, not its value.ckd.NewExtendedKeyFromStringpicks its parser by curve parameters (crypto.SameCurve). Other curves are decoded withelliptic.UnmarshalCompressed(a P-256 key now round-trips); undecodable key data returns an error instead of a key with nil coordinates.beta = alpha^-1 mod pqwith the constant-time inverse when constant-time operations are on. The reduction ofalphamodpqbefore the inverse is still variable-time; this is recorded as a known gap.keyDerivationDelta, the same way they already copy the message.ModProofwipes its pre-encoded secret exponents through adefer, so the wipe also runs when proof generation panics. A test hook proves the wipe runs on panic.CI and the legacy transcript checks
verify.shgains a third leg. It regenerates the vectors with the HEAD legacy provers, verifies them with the pinned historical2e712689verifier, and checks they equalprior_v2.5.2.jsonoutside provenance. Before this, CI only checked historical proofs against HEAD verifiers.Mixed-binary interopjob: it runsverify_mixed_interop.sh(the live exchange with the historical binary), plus vet and tests for the harness.go test ./...skipstestdata/, so this code never ran in CI before.Go minimum buildjob: it builds and vets with Go 1.25.7 andGOTOOLCHAIN=local.Mixed-binary interopandGo minimum buildare required checks ondevandmaster.Tests
New or rewritten tests cover:
(q+1)*N, the security-v2q^7bound, thehistoricalBobCompat=falsepath, and one Bob response bound shifted past its limit with the verification equation still holding;*common.SignatureData: fields, low-S, recovery id and ownership;BobMid/BobMidWCatb = qandb = -1;NTilde(error, no panic);HomoMultBoundedbound edges, andx,m,batq-1in constant-time mode;ringPedersenBetahelper in both timing modes;keyDerivationDelta;m = q-1boundary in both modes;Three log-only timing tests and the signing "CT wiring" test are removed: none of them could fail.
Docs
CHANGELOG and README corrections:
2e712689baseline;Parametersand one nonce per ceremony, and its keygen call compiles;/v2;rxis a public value;Not applied
873b8addesign and add the deferred wipe and its test.mu) is contradicted by the fix(paillier): stop caching private-key decryption state; review follow-ups #38 measurement: cachingLambdaNandmugave no Decrypt gain (70.7 vs 70.6 ms). Author decision 1 removed that cache.tautoq^3*NTilde): not applied, but now recorded as a known gap in the CHANGELOG. The legacy range matches the deployed historical prover2e712689and keeps legacy proofs byte-identical; verifiers do not depend on it. GG18 specifiesq^3*NTildeto hidee*sigmastatistically; security-v2 uses that range.Not in this repo: #37's description
Some findings point at text in #37's description. The "Known risks" bullet should add the MtA blinds and secp256k1 scalar multiplication, and should drop the round-5
rxreduction, which handles a public value. Next step 4.1's example tagv2.0.0-threshold.1cannot be consumed under the current module path; av1.xtag works.Second review pass
A multi-lens review of this PR (correctness, security, simplicity, contrarian, performance, docs, test coverage, spec compliance, intent) found these gaps. All are fixed in the later commits unless marked otherwise.
NTildeagainst the sampler cap, but the provers sample belowq^3*NTilde, so a 4233-5000-bitNTildestill panicked (reproduced). Fixed at the root in the samplers.q^7bound-edge tests, the verifier modulus-gate tests, the ModProof deferred-wipe test, thexpadding atq-1, and the keygenbetatest all still passed with the guarded check removed. Each is rewritten with equation-valid inputs or exact edges, and each mutation now fails a test. The DLN "non-unit" row now uses a real factor ofN.HomoMultWithBitLenlet a caller pass a secret-derived width; replaced byHomoMultBounded.BobMidwas blamed on every peer; now checked once in round 2 with no culprits.alphamodpqand legacytau; constructor panic lists are complete; review labels removed.bob_bounds_test.go,protocol_mode_test.go,signature_delivery_test.go.FactorProof,ModProofandNewDLNProofuse sampler output without a nil check. They have no error return; the panic is not new; the wire path pins moduli to 2048 bits.Verification
At the PR head after the second review pass (local, Go 1.27.1):
gofmt -l: clean.go vetandgo build ./...: pass.go test -count=1 -timeout 60m $(go list ./...): all packages pass in 229 s (keygen 228 s, signing 86 s, paillier 46 s, mta 37 s)../testdata/legacy_transcript/verify.sh: all three legs pass;canonical_legacy_vectors_equal=true../testdata/legacy_transcript/verify_mixed_interop.sh: exit 0. Reject run: default rejected at round 3 with 2 culprit entries. Accept run: accepted, both peers reach round 8. The homogeneous control reaches round 8.The first-pass run (Go 1.26.8, before the second review pass) also passed
go vet/go testfor./testdata/legacy_transcript/mixed_interopand targeted-raceruns of the concurrency tests in signing, keygen and paillier; these were not re-run.Findings and where each is addressed
common/constant_time.go:14-40common/constant_time.go:207-215common/constant_time.go:292common/constant_time.go:310common/validation.go:21-30,39-41common/validation.go:33-48common/constant_time.go:13-14,34-38common/constant_time_test.go:431-555common/int.go:104common/random.go:80,98,123-130common/validation.go:13-18,24-26,33-38crypto/paillier/mod_proof.go:96crypto/paillier/paillier.go:194crypto/paillier/key_reuse_regression_test.go:81-119crypto/paillier/mod_proof.go:184-199crypto/paillier/mod_proof.go:244-298,96,87,307,328crypto/paillier/mod_proof.go:368-405crypto/paillier/paillier.go:121-123,170-180,233-239crypto/paillier/paillier.go:252-259crypto/paillier/mod_proof.go:65-70,232-236crypto/paillier/paillier.go:157,193,196crypto/paillier/paillier.go:200-212crypto/paillier/paillier.go:259-271crypto/paillier/paillier.go:284-285crypto/mta/proof_bob_verifier.go:74-79crypto/mta/legacy_bob_compatibility_test.go:119-138crypto/mta/proof_bob_verifier.go:44-60crypto/mta/proof_bob_verifier.go:84-95crypto/mta/proofs.go:40-52,61-62,76-78,116crypto/mta/share_protocol.go:105-185crypto/mta/share_protocol.go:144-148crypto/mta/share_protocol_test.go:183-203crypto/mta/proofs.go:70-79crypto/mta/proofs.go:92crypto/mta/share_protocol.go:149-185crypto/dlnproof/proof.go:92-97crypto/dlnproof/proof_test.go:37-95crypto/ckd/child_key_derivation.go:108-123crypto/commitments/commitment.go:55-71ecdsa/keygen/local_party.go:71ecdsa/keygen/rounds.go:104-121ecdsa/keygen/prepare.go:129ecdsa/keygen/rounds.go:125-131ecdsa/signing/finalize.go:82ecdsa/signing/round_5.go:102-111ecdsa/signing/local_party.go:156ecdsa/signing/local_party.go:~110-122ecdsa/signing/round_5.go:77ecdsa/signing/round_ct_wiring_test.go:27ecdsa/signing/signing_boundaries_test.go:297-376ecdsa/signing/signing_boundaries_test.go:32ecdsa/signing/signing_boundaries_test.go:361-367tss/params.go:160-166tss/params.go:226-227testdata/legacy_transcript/verify.sh:22-37testdata/legacy_transcript/README.mdtestdata/legacy_transcript/README.md:30-46testdata/legacy_transcript/oracle/main.go:225-227testdata/legacy_transcript/README.md:93,145,160testdata/legacy_transcript/verify_mixed_interop.sh:16-17.github/workflows/test.yml:99-103.github/workflows/test.yml:42,74,96go.mod:1README.md:63-98,145-150CHANGELOG.md:322-328CHANGELOG.md:478-480CHANGELOG.md:91-99README.md:83