Skip to content

[DRAFT - decision-gated] feat(tbtc/signer): add TEE hardening checker stack#4007

Draft
mswilkison wants to merge 5 commits into
extraction/frost-signer-mirror-2026-05-26from
extraction/frost-signer-tee-stack-2026-05-26
Draft

[DRAFT - decision-gated] feat(tbtc/signer): add TEE hardening checker stack#4007
mswilkison wants to merge 5 commits into
extraction/frost-signer-mirror-2026-05-26from
extraction/frost-signer-tee-stack-2026-05-26

Conversation

@mswilkison

@mswilkison mswilkison commented May 26, 2026

Copy link
Copy Markdown
Contributor

⚠️ DRAFT — decision-gated

This PR adds the optional TEE signer-hardening readiness stack on top of
PR #4005, which
lands the base FROST/ROAST Rust signer at pkg/tbtc/signer/.

It must NOT be undrafted or merged until:

  1. PR feat(tbtc/signer): mirror FROST/ROAST Rust signer from tBTC monorepo #4005 merges.
  2. Threshold's policy decision on TEE enforcement for signers is explicit and documented.
  3. This branch is rebased onto canonical main after feat(tbtc/signer): mirror FROST/ROAST Rust signer from tBTC monorepo #4005 merges.

Canonical repo note

This PR was initially sourced from
tlabs-xyz/tbtc#88, but
threshold-network/keep-core is now the canonical repo for the signer. The
monorepo/source-PR reference is provenance only; follow-up fixes in this PR are
ordinary keep-core changes and are not expected to remain byte-for-byte mirrors
of the old source branch.

Scope

Four-phase TEE checker stack:

  • Phase A — governance registry + audit-event validation (tee_registry_checker)
  • Phase B — verifier/keyset/token/revocation validation (tee_token_checker)
  • Phase C — runtime token + denylist + vendor-diversity enforcement (tee_runtime_checker)
  • Phase D — enforcement modes + break-glass controls (tee_enforcement_checker)

Files

Path Count
pkg/tbtc/signer/src/bin/tee_*.rs 4 Rust binaries
pkg/tbtc/signer/scripts/tee-*.sample.json 12 sample configs
pkg/tbtc/signer/README.md 1 README update
pkg/tbtc/signer/docs/tee-whitelisted-signer-activation-gate-record.md + tee-whitelisted-signer-enforcement-plan.md 2 docs

Follow-up fixes included

  • Rebases/merges the branch onto the current feat(tbtc/signer): mirror FROST/ROAST Rust signer from tBTC monorepo #4005 signer base so the full
    signer Rust checks workflow is present.
  • Keeps the feat(tbtc/signer): mirror FROST/ROAST Rust signer from tBTC monorepo #4005 README path fixes and production-profile/state-key
    documentation, and updates the new TEE sections to use pkg/tbtc/signer/.
  • Rejects verifier keysets that reuse the same Schnorr public key under
    different key_ids, closing the threshold-quorum bypass.
  • Bounds reused break-glass incident tickets by the original history activation
    time and policy TTL, so an old ticket cannot refresh its waiver window.
  • Applies the same hard ceilings for attestation age and denylist staleness
    across the registry/token/runtime checkers.
  • Makes TEE checker clock reads fail closed if the system clock is before the
    Unix epoch.

Initial provenance

Verification

Local verification on the latest pushed head:

cargo fmt --manifest-path pkg/tbtc/signer/Cargo.toml -- --check
cargo clippy --manifest-path pkg/tbtc/signer/Cargo.toml --all-targets -- -D warnings
TBTC_SIGNER_STATE_PATH=/tmp/tbtc-signer-ci-state-pr4007-local.json \
  cargo test --manifest-path pkg/tbtc/signer/Cargo.toml

Plan context

This is an optional, decision-gated addition to the FROST extraction. Unlike the
mandatory extraction/mirror PRs, this PR's merge depends on the Threshold TEE
policy decision, not just operational readiness.

🤖 Generated with Claude Code

…k from tBTC monorepo

⚠️ DRAFT — DECISION-GATED. This PR mirrors the source PR
tlabs-xyz/tbtc#88 which is itself a decision-gated draft. It must NOT
merge until the Threshold policy decision to require TEE operation
for signers is explicit.

Stacked on top of [PR #4005](https://github.com/threshold-network/
keep-core/pull/4005) which lands the base FROST/ROAST Rust signer at
pkg/tbtc/signer/. This PR adds the optional TEE signer-hardening
checker stack on top.

Phases A-D (per source PR #88)
- Phase A: governance registry + audit-event validation
  (tee_registry_checker)
- Phase B: verifier/keyset/token/revocation validation
  (tee_token_checker)
- Phase C: runtime token + denylist + vendor-diversity enforcement
  (tee_runtime_checker)
- Phase D: enforcement modes + break-glass controls
  (tee_enforcement_checker)

Files (19 total — all mirror status)
- 4 Rust binaries at pkg/tbtc/signer/src/bin/
- 12 sample config JSON files at pkg/tbtc/signer/scripts/
- 1 README.md update at pkg/tbtc/signer/
- 2 docs at pkg/tbtc/signer/docs/ (tee-whitelisted-signer-activation-
  gate-record.md + tee-whitelisted-signer-enforcement-plan.md)

Provenance
- Source repository: tlabs-xyz/tbtc
- Source PR: #88 (decision-gated, draft)
- Source commit (PR #88 HEAD): 31f0aa2b8efbf8ac42909f8e8d4ce67248cb478e
- Source branch: feat/tee-phase-a-governance-registry (stacked on
  feat/frost-schnorr-migration)
- Base for THIS PR: extraction/frost-signer-mirror-2026-05-26 (PR #4005)
- Source PR includes stacked phases B, C, D from PRs #89, #90, #91
  merged into PR #88's branch

Decision gate
The source PR #88 description is explicit: this stays in draft until
the runtime TEE policy decision is made. The same constraint applies
to this canonical mirror — DO NOT MERGE before:
1. PR #4005 (base FROST signer) lands
2. Threshold policy decision on TEE enforcement for signers is
   explicit and documented (per the source PR's "Decision-Gated"
   framing)

Verification (per plan v38 §7.2 mirror status)
For each file:
- sha256(git show 31f0aa2b8e:<sourcePath>) ==
  sha256(file at this PR head at <targetPath>)

The 19 files port as byte-for-byte mirrors with path normalization:
- tools/tbtc-signer/* → pkg/tbtc/signer/*
- docs/frost-migration/tee-* → pkg/tbtc/signer/docs/tee-*

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

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: b4c1d0e6-a5e8-4b62-9c56-39395c2ef990

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 extraction/frost-signer-tee-stack-2026-05-26

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

mswilkison added a commit that referenced this pull request May 26, 2026
Resolves CI failures on PR #4005 (signer mirror):

1. TLA model checks: run_tla_models.sh lacked executable bit at
   canonical HEAD. CI ran the script directly (no `bash` prefix),
   which fails with `Permission denied`. Fixed via
   `git update-index --chmod=+x`.

2. Signer formal invariants: engine.rs's
   formal_verification_roast_attempt_context_shared_vectors_match_
   expected_values test referenced vectors at a path stale from
   the umbrella's docs/frost-migration/test-vectors/ layout. The
   manifest places the vector at the canonical-signer test/vectors/
   subdir (pkg/tbtc/signer/test/vectors/roast-attempt-context-v1.json
   per the source-to-target map). Updated the
   `PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(...)` argument from
   `../../docs/frost-migration/test-vectors/roast-attempt-context-v1.json`
   (umbrella-relative) to `test/vectors/roast-attempt-context-v1.json`
   (signer-CARGO_MANIFEST_DIR-relative, where the vector actually
   lives at canonical HEAD).

Verified locally:
- ls -l shows executable bit set on run_tla_models.sh
- engine.rs path now resolves to the correct mirror location
- Vector exists at pkg/tbtc/signer/test/vectors/roast-attempt-context-v1.json

Same fix needs to be applied to PR #4007 (stacked on #4005)
in a follow-up commit on its branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mswilkison mswilkison changed the title [DRAFT - decision-gated] feat(tbtc/signer): mirror TEE hardening stack from tBTC monorepo [DRAFT - decision-gated] feat(tbtc/signer): add TEE hardening checker stack May 27, 2026
mswilkison added a commit that referenced this pull request Jun 12, 2026
## What

Phase 7.0: the spec-freeze candidate for the hardened interactive
two-round FROST signing session — the production signing path —
assembling the already-settled decisions (t-of-included-native per
decisions 5/6, sidecar-shaped API per decision 2, OS-randomness-only
production signing) into a precise contract, plus the deletion plan for
the frozen transitional deterministic-nonce path.

## The load-bearing design change: nonce custody moves inside the engine

Verified current state: the stateless primitives (`frost_ops.rs`) return
serialized `SigningNonces` to the Go host and accept them back at
`sign_share` — secret nonces cross the FFI twice, live in host memory
between rounds, and single-use is enforced by caller discipline only.
Calling `sign_share` twice with one nonce pair is the canonical FROST
key-extraction failure and nothing prevents it today.

The session layer (spec §4): nonces are generated and held in
session-scoped engine memory behind an opaque handle, consumed
atomically (durable consumption marker before the share leaves the
engine), zeroized on use, and **never persisted and never exported**.
Restart loses in-flight nonces by construction (attempt fails safe); the
cloned-state nonce-reuse class becomes structurally impossible; and
after Phase 7 no secret signing material transits the FFI in either
direction — which is also the audit story for that boundary.

## Also specified

- **Session API** (§5):
`InteractiveSessionOpen/Round1/Round2/Aggregate/Abort`,
idempotent-or-fail-closed, strict-mode attempt contexts only,
consumed-registry semantics carried from the coarse path,
transport-agnostic for the dlopen→sidecar swap.
- **t-of-included semantics** (§6): engine-side subset verification at
Round2 (own membership, subset-of-included, size t) so safety never
depends on coordinator honesty; signing packages ride #4040-style
signed-body envelopes extending the #4044 equivocation-evidence
retention; silent members cost zero attempts.
- **Deletion trigger made precise** (§7): three conditions defining
"interactive production path validated e2e" (Phase-5-equivalent suites
incl. consumed-nonce-marker persist-fault cases; a real testnet
t-of-included finalize through the full retry machinery; pinned
cross-language vectors). The `nonce.rs` freeze marker now points at this
section — the only code change in this PR, comment-only on the frozen
file.
- **Reserved, not built** (§8): bounded n−t+1 concurrency hooks
(attempt-scoped keys/handles).
- **Phasing** (§9): PR-sized 7.0–7.6 with repo-side mapping; #4007
sidecar scoping folds into 7.0 as an addendum.
- **Open questions with proposed defaults** (§10): package-distribution
channel, round-1 transport shape, responsive-subset policy, markers-only
durability — to be decided at freeze sign-off and recorded in the
Decision Log.

## Freeze process

Status is Proposed; it freezes on signer + keep-core owner sign-off per
§11, with the §10 defaults ratified or overridden in the gates-doc
Decision Log. The audit scope statement should reference this document
and name the §5 API as in-scope (decision 1 interaction).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
mswilkison added a commit that referenced this pull request Jun 13, 2026
Section 1 said the host "holds no signing secrets at any time," but
section 3 maps the transitional DKG calls unchanged and the frozen
Phase 7 spec still has the DKG APIs returning/accepting
secret_package_hex through the host until the DKG-custody follow-up.
So in deployments that run DKG through this transport the host still
sees DKG secret material (review finding). Section 1 now scopes the
property to the signing path and states explicitly that #4007 must
treat the host<->sidecar signing interface as a secret boundary but
NOT the DKG interface until DKG custody moves inside the sidecar - a
precondition for the sidecar being a complete secret boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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