Skip to content

fix(sdk): late-init BLE signer must use the identity AK, not a random keypair (fixes bilateral σ verification) - #629

Merged
cryptskii merged 1 commit into
mainfrom
fix/bilateral-late-init-signer-uses-ak
Aug 9, 2026
Merged

fix(sdk): late-init BLE signer must use the identity AK, not a random keypair (fixes bilateral σ verification)#629
cryptskii merged 1 commit into
mainfrom
fix/bilateral-late-init-signer-uses-ak

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

The bug

The bilateral σ_A/σ_B signatures must be signed by — and verified against — the device's 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 and no cert layer on the σ path.

But ensure_bluetooth_manager_and_sync_contact (dsm_sdk/src/bluetooth/mod.rs) 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 → every bilateral transfer fails σ verification: invalid counterparty signature (σ_B) — response lost.

Why it surfaced now

The rows 6/7 fix (#627) made the receiver verify σ against the pinned AK instead of the wire-overwritten key. The old behavior — overwriting contact.public_key with the wire signing key on every prepare — masked this bug by promoting the random wire key into the trust-root slot so σ verified tautologically. With rows 6/7 correct, the random-signer mismatch became fatal.

This was found by a full source provenance trace of all three key roles (σ-signer, wire key, QR-pinned AK), which showed they are all meant to be the byte-identical identity AK.

The 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 can hijack the signer slot. Both managers then hold the byte-identical identity AK, so whichever wins the OnceLock race signs σ with the pinned AK.

let keypair = match RecoverySDK::get_cached_wallet_seed() {
    Some(seed) => derive_device_signing_keypair(&seed, &gen_fixed)?,   // the identity AK
    None => return Ok(false),                                          // no random-key hijack
};

Hardware evidence (8XK ↔ 9FF, release .so)

Direct before/after on the rig:

  • Before: Invalid operation: invalid counterparty signature (σ_B) — response lost (transfer dies at step 2).
  • After: Session moved to Accepted phasesend_bilateral_confirm ... ConfirmPending → receiver §11.1 per-step EK A-side verification PASS (reaches step 3, post-crypto). No "signing AK differs" warning — the wire signing key now equals the pinned AK.

Scope / evidence boundary

…andom keypair

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).
@cryptskii
cryptskii merged commit b493db6 into main Aug 9, 2026
12 checks passed
@cryptskii
cryptskii deleted the fix/bilateral-late-init-signer-uses-ak branch August 9, 2026 01:28
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