Skip to content

hardening(signer): fail closed on stateless nonce primitives under the production profile#4129

Open
mswilkison wants to merge 1 commit into
extraction/frost-signer-mirror-2026-05-26from
hardening/signer-stateless-nonce-fail-closed
Open

hardening(signer): fail closed on stateless nonce primitives under the production profile#4129
mswilkison wants to merge 1 commit into
extraction/frost-signer-mirror-2026-05-26from
hardening/signer-stateless-nonce-fail-closed

Conversation

@mswilkison

Copy link
Copy Markdown
Contributor

Finding (MEDIUM, fail-closed asymmetry — defense-in-depth)

The stateless FROST FFI primitives generate_nonces_and_commitments and sign_share in pkg/tbtc/signer/src/engine/frost_ops.rs hand a one-time signing nonce pair out to the host and later accept it back as opaque nonces_hex. Nonce custody — and the single-use invariant — is left entirely with the caller. The SignShareRequest contract even documents this: the caller "is cryptographically responsible for single use."

A host bug or compromise that replays one nonces_hex across 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) in frost_ops.rs, mirroring the deterministic guard's style:

  • same predicate: signer_profile_is_production()
  • same error type/shape: EngineError::LifecyclePolicyRejected
  • reason_code: stateless_nonce_primitives_disabled_in_production

Under 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:

  • Under the development profile, builds real key packages + nonces + signing package and confirms both primitives succeed.
  • Flips to the production profile (with valid provenance so the new gate — not the provenance gate — is what fires) and feeds the same secret inputs: both primitives return LifecyclePolicyRejected with reason_code stateless_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 — clean
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo test — 319 passed (lib), 25 passed (admission_checker bin), 1 passed (p2tr integration), 0 failed

🤖 Generated with Claude Code

…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>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3a1624f-b7b8-41b5-a576-c18e5bd56613

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hardening/signer-stateless-nonce-fail-closed

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant