From 3b0f991c1dbe98ea573e5029c8d7a11c43c4134d Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:04:39 -0400 Subject: [PATCH] fix(sdk): the late-init BLE signer must be the identity AK, never a random keypair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bilateral σ_A/σ_B signatures are signed by, and verified against, the device genesis-v2 identity AK — the same key the QR/BLE pairing pins as the contact AK (derive_device_ak_keypair(wallet_seed, genesis, slot=0, policy); spec TRACEABILITY.md, ADR 0002 row 9). There is no separate bilateral/session signing key. But `ensure_bluetooth_manager_and_sync_contact` minted a RANDOM `SignatureKeyPair::new()` for the BilateralTransactionManager it late-registers. When a QR contact is added before `init_dsm_sdk` registers the canonical AK-keyed manager, this fallback wins the global signer `OnceLock` race (first-writer-wins) and the canonical manager is discarded ("already registered"). σ is then signed with a random key the peer never pinned, so every bilateral transfer fails σ verification ("invalid counterparty signature (σ_B) — response lost"). Fix: derive the signing keypair from the cached wallet seed exactly as the canonical path (init.rs), and REFUSE to register (Ok(false)) when the seed is locked — rather than minting a non-AK key that could hijack the signer slot. Both managers then hold the byte-identical identity AK, so whichever wins the race signs σ with the pinned AK. Exposed by the rows 6/7 fix (#627): once σ is correctly verified against the pinned AK instead of the overwritten wire key, the random-signer mismatch became fatal. The old overwrite masked it by promoting the random wire key into the trust-root slot. Hardware-proven (8XK<->9FF, release .so): before the fix σ_B failed ("invalid counterparty signature (σ_B) — response lost"); after, σ_B verifies and the flow advances through confirm + §11.1 per-step-EK verification (PASS). Full offline-bearer settlement is unrelated and still needs the Pico anchor appliance (fails closed at MissingRelease; balances unchanged). --- .../dsm_sdk/src/bluetooth/mod.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/bluetooth/mod.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/bluetooth/mod.rs index 52b03a97..2bd865ad 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/bluetooth/mod.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/bluetooth/mod.rs @@ -324,13 +324,29 @@ pub async fn ensure_bluetooth_manager_and_sync_contact( use dsm::core::contact_manager::DsmContactManager; use dsm::core::bilateral_transaction_manager::BilateralTransactionManager; // Note: Health state tracking removed - always proceed - use dsm::crypto::signatures::SignatureKeyPair; use tokio::sync::RwLock as TokioRwLock; let storage_nodes: Vec = vec![dsm::types::identifiers::NodeId::new("n")]; let contact_manager = DsmContactManager::new(dev_fixed, storage_nodes); - let keypair = SignatureKeyPair::new().map_err(|e| format!("keypair generation failed: {e}"))?; + // The bilateral σ signer MUST be the device identity AK — the same key the QR pins and σ is + // verified against (`derive_device_ak_keypair(seed, genesis, 0, policy)`), NEVER a random + // keypair. This late-init manager can win the global-signer `OnceLock` race (first-writer-wins) + // against the canonical AK-keyed manager from `init_dsm_sdk`; if it holds a random key, σ_A/σ_B + // are signed with a key the peer never pinned and every transfer fails σ verification. Derive + // the AK from the cached wallet seed exactly as the canonical path; if the seed is locked, + // REFUSE to register rather than mint a non-AK key that would hijack the signer slot. + let keypair = match crate::sdk::recovery_sdk::RecoverySDK::get_cached_wallet_seed() { + Some(seed) => crate::init::derive_device_signing_keypair(&seed, &gen_fixed) + .map_err(|e| format!("device signing keypair derivation failed: {e}"))?, + None => { + log::warn!( + "[BLE] ensure_bluetooth_manager_and_sync_contact: wallet seed not unlocked; \ + refusing to late-init the BLE signer with a non-AK key" + ); + return Ok(false); + } + }; let chain_tip_store = Arc::new(crate::sdk::chain_tip_store::SqliteChainTipStore::new()); let manager = BilateralTransactionManager::new_with_chain_tip_store( contact_manager,