BLE P0 (release-blocking): authenticate the BLE-delivered ML-KEM key against the pinned AK before caching (ADR 0002 rows 8/9) - #628
Merged
Conversation
…d AK before caching ADR 0002 matrix rows 8/9: the bilateral BLE prepare handlers cached the peer's wire-supplied ML-KEM (Kyber) key with NO authentication — the key was covered by no signature the receiver verified against the pinned AK (the commit-phase `bilateral-sign` transcript signs only `domain || commitment_hash`, which excludes the Kyber key). An attacker who could deliver a prepare frame naming a victim's device_id could seed an attacker Kyber key first-write-wins, so every future per-step-EK receipt encapsulated to the attacker's key. Fix — a DETACHED binding, reusing the ONE identity-binding primitive already used by storage-fetch and repair (never the commit transcript, which is transaction authorization, not peer-identity authentication): - proto: `sender_kyber_binding_sig` (BilateralPrepareRequest, field 17) and `responder_kyber_binding_sig` (BilateralPrepareResponse, field 10); - sender: `build_local_kyber_identity_binding()` signs `binding_digest(device_id, genesis, kyber_pk)` with the local AK, riding both the request and the response (and the non-Android host response path); - receiver: `verify_and_cache_peer_kyber` verifies the detached binding via `verify_kyber_identity_binding` against the PINNED contact AK — never the wire signing key, never the wire device_id/genesis — BEFORE `bind_contact_kyber_key_if_absent`. Fail-closed on every rejection (empty, forged, wrong-AK, substituted-key, old-domain/non-canonical, no contact, no pinned AK), leaving the prior Kyber cache untouched; no implicit TOFU. `bilateral-sign` is unchanged. Proven by `verify_and_cache_peer_kyber_verifies_before_caching`: all eight rejection cases leave the cache empty and the valid binding caches; mutation- verified (disabling the verify turns the test red, then restored). Skeleton/echo/ test constructors of the prepare messages carry an empty binding (fail-closed at the receiver). Building a prepare now reads the global AppState identity to sign the binding, so `bilateral_reject_tests` is set to test-isolation mode (in-memory AppState default -> empty binding, fail-soft); production initialises AppState at startup. RELEASE-BLOCKING: requires two-phone hardware validation before merge (this is a BLE wire-format change; DSM beta redeploys both peers together, no back-compat).
…se path The rows 8/9 API-widening sweep missed the #[cfg(all(target_os="android", feature="jni"))] BilateralPrepareResponse builder in bilateral_impl.rs: a host `cargo check --all-targets` compiles the NON-android cfg branch, so the missing `responder_kyber_binding_sig` field only surfaced in the aarch64 cargo-ndk device build. The android+jni responder now populates responder_kyber_public_key + responder_kyber_binding_sig via build_local_kyber_identity_binding, exactly like the BLE handler and the non-android host path, so the real on-device response carries the detached binding the receiver verifies against the pinned AK. Hardware-proven (8XK<->9FF, release .so): the detached Kyber binding verifies in BOTH BLE directions and the cached key is consumed successfully by the §11.1 per-step EK verification (PASS).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this closes
ADR 0002 matrix rows 8/9 — the last open rows. The bilateral BLE prepare handlers cached the peer's wire-supplied ML-KEM (Kyber) key with no authentication: no signature the receiver verified against the pinned AK covered that key. (The commit-phase
bilateral-signtranscript signs onlydomain ‖ commitment_hash— it excludes the Kyber key, and it is transaction authorization, not peer-identity authentication.)Exploit it closes: anyone who could deliver a prepare frame naming a victim's
device_idcould seed an attacker Kyber key first-write-wins; every subsequent §11.1 per-step-EK receipt then encapsulated to the attacker's key.The fix — a detached binding, reusing the one identity primitive
Chosen deliberately over folding the key into the
bilateral-signtranscript (that would couple peer-key authentication to the commit-phase signature — wrong layer boundary). This reuses the sameverify_kyber_identity_bindingprimitive already used by storage-fetch and repair, so DSM has one way to authenticate a Kyber key.proto/dsm_app.proto):sender_kyber_binding_sig(BilateralPrepareRequestfield 17),responder_kyber_binding_sig(BilateralPrepareResponsefield 10).build_local_kyber_identity_binding()signsbinding_digest(device_id, genesis, kyber_pk)under the local AK, riding the request, the response, and the non-Android host response path.verify_and_cache_peer_kyber): verifies the detached binding against the PINNED contact AK — never the wire signing key, never the wiredevice_id/genesis— beforebind_contact_kyber_key_if_absent.bilateral-signis unchanged.Behavior
Every rejection leaves the prior Kyber cache untouched. Skeleton/echo/test constructors of the prepare messages carry an empty binding (fail-closed at the receiver).
Proof
verify_and_cache_peer_kyber_verifies_before_caching(inbilateral_ble_handler.rs) drives all eight rows above against a real test DB with a real SPHINCS+-signed binding: every rejection asserts the cache stayed empty; the valid case asserts it cached. Mutation-verified — disabling the binding verification turns the test red (then restored).Gates (local)
cargo fmt --all -- --check✅ ·cargo clippy --all-targets -- -D warnings✅ ·scripts/guard_protos.sh✅ (single source of truth) ·ci/production_safety_checks.sh✅cargo test -p dsm_sdk/cargo test -p dsm— results below.Still required before merge
Two-phone hardware validation — flash both devices, complete a real bilateral BLE transfer, confirm the receiver caches the peer Kyber key (binding verifies) and that a tampered/absent binding fails closed. Not runnable in CI.