Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pkg/tbtc/signer/docs/phase-7-interactive-session-spec-freeze.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ self-contained):
mode is the only mode here: no legacy-shape fallback), checks
policy gates and provenance, registers the session. Idempotent
by full-request fingerprint; conflicting reopen fails closed.
**The member's key package is resolved from the session's own DKG
state (run_dkg), NOT carried in the request** — so the session
must already exist with completed DKG, and no signing secret
crosses the FFI/host boundary (section 4). This is a correction to
an earlier draft of this spec that had Open accept the key package
in the request; accepting it would have left key shares outside
the engine and defeated the sidecar's signing-secret boundary. A
request `threshold` is still carried but must equal the DKG
threshold. As a consequence, an interactive session always rides a
DKG-populated session and never creates registry entries of its
own.
2. `InteractiveRound1` — fresh nonces + commitments as in section
4. Per (session, attempt, member) at most one live handle;
repeat calls return the same commitments (idempotent) until
Expand Down
16 changes: 16 additions & 0 deletions pkg/tbtc/signer/include/frost_tbtc.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ TbtcSignerResult frost_tbtc_finalize_sign_round(const uint8_t* request_ptr, size
TbtcSignerResult frost_tbtc_build_taproot_tx(const uint8_t* request_ptr, size_t request_len);
TbtcSignerResult frost_tbtc_refresh_shares(const uint8_t* request_ptr, size_t request_len);

/*
* Phase 7.1 hardened interactive signing session.
*
* Unlike the stateless nonce contract above, secret nonces NEVER cross this
* boundary in either direction: the engine generates, holds, consumes, and
* zeroizes them internally, keyed by (session_id, attempt_id). The caller
* exchanges only public commitments, signing packages, and signature shares.
* frost_tbtc_interactive_round2 verifies the coordinator's signing package in
* full and consumes the attempt's nonces exactly once; a repeat call for a
* consumed attempt fails closed with the `consumed_nonce_replay` error code.
*/
TbtcSignerResult frost_tbtc_interactive_session_open(const uint8_t* request_ptr, size_t request_len);
TbtcSignerResult frost_tbtc_interactive_round1(const uint8_t* request_ptr, size_t request_len);
TbtcSignerResult frost_tbtc_interactive_round2(const uint8_t* request_ptr, size_t request_len);
TbtcSignerResult frost_tbtc_interactive_session_abort(const uint8_t* request_ptr, size_t request_len);

#ifdef __cplusplus
}
#endif
Expand Down
109 changes: 109 additions & 0 deletions pkg/tbtc/signer/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,87 @@ pub struct SignShareResult {
pub signature_share: NativeFrostSignatureShare,
}

// Phase 7.1 hardened interactive signing session (frozen spec
// docs/phase-7-interactive-session-spec-freeze.md, section 5). Unlike
// the stateless primitives above, secret nonces NEVER appear in these
// requests or results: the engine generates, holds, consumes, and
// zeroizes them internally, keyed by (session_id, attempt_id).

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveSessionOpenRequest {
pub session_id: String,
pub member_identifier: u16,
pub message_hex: String,
pub key_group: String,
/// Signing threshold; must equal the session's DKG threshold. The
/// key material itself is resolved from the engine's DKG state and
/// is never carried in this request - no signing secret crosses the
/// FFI (frozen spec section 4).
pub threshold: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub taproot_merkle_root_hex: Option<String>,
/// Required: interactive sessions are strict-mode only; there is
/// no legacy-shape fallback on this path.
pub attempt_context: AttemptContext,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveSessionOpenResult {
pub session_id: String,
pub attempt_id: String,
pub idempotent: bool,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveRound1Request {
pub session_id: String,
pub attempt_id: String,
pub member_identifier: u16,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveRound1Result {
/// The member's public signing commitments. Idempotent until the
/// attempt's nonces are consumed; the secret nonces they
/// correspond to never leave the engine.
pub commitments_hex: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveRound2Request {
pub session_id: String,
pub attempt_id: String,
pub member_identifier: u16,
/// The coordinator's signing package (the chosen responsive
/// subset's commitment list). Verified in full - membership,
/// subset-of-included, exact threshold size, message binding, and
/// byte-identity of this member's own commitment entry - BEFORE
/// the nonces are consumed.
pub signing_package_hex: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveRound2Result {
pub session_id: String,
pub attempt_id: String,
pub signature_share_hex: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveSessionAbortRequest {
pub session_id: String,
/// When set, abort only if the live attempt matches; when unset,
/// abort whatever attempt is live for the session.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attempt_id: Option<String>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct InteractiveSessionAbortResult {
pub session_id: String,
pub aborted: bool,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct AggregateRequest {
pub signing_package_hex: String,
Expand Down Expand Up @@ -521,6 +602,30 @@ pub struct SignerHardeningMetricsResult {
pub finalize_sign_round_latency_samples: u64,
pub refresh_shares_latency_p95_ms: u64,
pub refresh_shares_latency_samples: u64,
#[serde(default)]
pub interactive_session_open_calls_total: u64,
#[serde(default)]
pub interactive_session_open_success_total: u64,
#[serde(default)]
pub interactive_round1_calls_total: u64,
#[serde(default)]
pub interactive_round1_success_total: u64,
#[serde(default)]
pub interactive_round2_calls_total: u64,
#[serde(default)]
pub interactive_round2_success_total: u64,
#[serde(default)]
pub interactive_session_abort_calls_total: u64,
#[serde(default)]
pub interactive_session_abort_success_total: u64,
#[serde(default)]
pub interactive_round1_latency_p95_ms: u64,
#[serde(default)]
pub interactive_round1_latency_samples: u64,
#[serde(default)]
pub interactive_round2_latency_p95_ms: u64,
#[serde(default)]
pub interactive_round2_latency_samples: u64,
pub last_updated_unix: u64,
}

Expand Down Expand Up @@ -565,6 +670,10 @@ pub struct InitSignerConfigRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_sessions: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_live_interactive_sessions: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interactive_session_ttl_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_key_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_key_command: Option<String>,
Expand Down
14 changes: 14 additions & 0 deletions pkg/tbtc/signer/src/engine/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ pub(crate) const TBTC_SIGNER_MAX_SESSIONS_ENV: &str = "TBTC_SIGNER_MAX_SESSIONS"

pub(crate) const TBTC_SIGNER_DEFAULT_MAX_SESSIONS: usize = 1024;

// Phase 7.1 interactive session bounds. Live interactive sessions hold
// secret nonces in memory, so they get a dedicated, smaller cap than
// the overall session registry, and a TTL after which an abandoned
// attempt's nonces are destroyed (expiry has abort semantics).
pub(crate) const TBTC_SIGNER_MAX_LIVE_INTERACTIVE_SESSIONS_ENV: &str =
"TBTC_SIGNER_MAX_LIVE_INTERACTIVE_SESSIONS";

pub(crate) const TBTC_SIGNER_DEFAULT_MAX_LIVE_INTERACTIVE_SESSIONS: usize = 64;

pub(crate) const TBTC_SIGNER_INTERACTIVE_SESSION_TTL_SECONDS_ENV: &str =
"TBTC_SIGNER_INTERACTIVE_SESSION_TTL_SECONDS";

pub(crate) const TBTC_SIGNER_DEFAULT_INTERACTIVE_SESSION_TTL_SECONDS: u64 = 3600;

pub(crate) const TBTC_SIGNER_STATE_LOCKFILE_SUFFIX: &str = ".lock";

pub(crate) const TBTC_SIGNER_ALLOW_BOOTSTRAP_ENV: &str = "TBTC_SIGNER_ALLOW_BOOTSTRAP";
Expand Down
10 changes: 10 additions & 0 deletions pkg/tbtc/signer/src/engine/init_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,16 @@ pub(crate) fn config_values_from_request(
TBTC_SIGNER_MAX_SESSIONS_ENV,
request.max_sessions,
);
insert_u64(
&mut values,
TBTC_SIGNER_MAX_LIVE_INTERACTIVE_SESSIONS_ENV,
request.max_live_interactive_sessions,
);
insert_u64(
&mut values,
TBTC_SIGNER_INTERACTIVE_SESSION_TTL_SECONDS_ENV,
request.interactive_session_ttl_seconds,
);
insert_u64(
&mut values,
TBTC_SIGNER_STATE_KEY_COMMAND_TIMEOUT_SECS_ENV,
Expand Down
Loading
Loading