hardening(signer): fail closed on stateless nonce primitives under the production profile#4129
Open
mswilkison wants to merge 1 commit into
Conversation
…ction The stateless FROST FFI primitives `generate_nonces_and_commitments` and `sign_share` in `frost_ops.rs` hand a one-time signing nonce pair out to the host and later accept it back as opaque `nonces_hex`. That leaves nonce custody -- and the single-use invariant -- entirely with the caller (the `SignShareRequest` contract even states the caller "is cryptographically responsible for single use"). A host bug or compromise that replays one `nonces_hex` across two distinct signing packages produces two Schnorr shares under the same nonce, which algebraically solves for the long-term secret share. The deterministic StartSignRound/FinalizeSignRound path is already fenced off in production by `enforce_transitional_signing_disabled_in_production`, but these stateless primitives gated only on the provenance attestation and had no production-profile guard -- a fail-closed asymmetry. A production signer could therefore be driven through the host-custody nonce path and inherit that catastrophic blast radius. Add `enforce_stateless_nonce_primitives_disabled_in_production`, mirroring the deterministic guard's style (same `signer_profile_is_production()` predicate, same `EngineError::LifecyclePolicyRejected` variant and message shape), and invoke it in both primitives immediately after the provenance gate -- before any secret key package or nonce is deserialized. Under the production profile both refuse with reason_code `stateless_nonce_primitives_disabled_in_production`; production signing must use the interactive FROST path (`interactive.rs`), where the engine keeps nonce custody and enforces durable single-use consumption markers. Non-production/test profiles retain current behavior. No current production caller reaches these paths (defense-in-depth before activation). Adds a focused test proving both primitives fail closed under the production profile with valid provenance -- feeding the same secret inputs that succeed under development -- and still work under the development profile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finding (MEDIUM, fail-closed asymmetry — defense-in-depth)
The stateless FROST FFI primitives
generate_nonces_and_commitmentsandsign_shareinpkg/tbtc/signer/src/engine/frost_ops.rshand a one-time signing nonce pair out to the host and later accept it back as opaquenonces_hex. Nonce custody — and the single-use invariant — is left entirely with the caller. TheSignShareRequestcontract even documents this: the caller "is cryptographically responsible for single use."A host bug or compromise that replays one
nonces_hexacross two distinct signing packages yields two Schnorr shares under the same nonce, which algebraically solves for the long-term secret share. Catastrophic blast radius if reached.The deterministic StartSignRound/FinalizeSignRound path is already fenced off in production by
enforce_transitional_signing_disabled_in_production. The stateless primitives, by contrast, gated only on the provenance attestation (enforce_provenance_gate) and had no production-profile guard — a fail-closed asymmetry. A production signer could therefore be driven through the host-custody nonce path and inherit that blast radius.No current production caller reaches these paths; this is defense-in-depth before activation.
Guard added
New
enforce_stateless_nonce_primitives_disabled_in_production(operation)infrost_ops.rs, mirroring the deterministic guard's style:signer_profile_is_production()EngineError::LifecyclePolicyRejectedstateless_nonce_primitives_disabled_in_productionUnder the production profile it fails closed (returns the error); non-production/test profiles keep current behavior.
The two call sites
Invoked immediately after
enforce_provenance_gate()?— before any secret key package or nonce is deserialized — in:generate_nonces_and_commitments(enforce_stateless_nonce_primitives_disabled_in_production("GenerateNoncesAndCommitments"))sign_share(enforce_stateless_nonce_primitives_disabled_in_production("SignShare"))The deterministic path is unchanged.
Test
engine::tests::stateless_nonce_primitives_reject_under_production_profile:LifecyclePolicyRejectedwith reason_codestateless_nonce_primitives_disabled_in_production, before any secret is deserialized (the gate is the second statement of each primitive).Validation (from
pkg/tbtc/signer)cargo fmt -- --check— cleancargo clippy --all-targets -- -D warnings— cleancargo test— 319 passed (lib), 25 passed (admission_checker bin), 1 passed (p2tr integration), 0 failed🤖 Generated with Claude Code