feat: secure-element offline-bearer DeviceAnchor (host-side Phase 5)#493
Open
cryptskii wants to merge 12 commits into
Open
feat: secure-element offline-bearer DeviceAnchor (host-side Phase 5)#493cryptskii wants to merge 12 commits into
cryptskii wants to merge 12 commits into
Conversation
Adds research-only native capture + on-device collectors used to test whether the C-DBRW orbit timing channel separates same-make/same-model devices (3x Galaxy A16, mt6789). NOT a production path; no K_DBRW or enrollment-schema change. - siliconfp.cpp: captureOrbitRaw() exposing the walk-altering knobs (rotation r, mu injection cadence k, per-core lane via verified pin) and returning raw per-probe CNTVCT timing + per-probe perf-cycle deltas (for a two-clock ratio) + mu histogram, for host-side extractor sweeps. - SiliconFingerprintNative.kt: captureOrbitRaw binding. - SiliconFpExtractorSweepTest / SiliconFpScaleSweepTest: arg-driven collectors (rotation x cadence x lane x altitude). Finding (3-device, no-pair-collapse gate, shuffle control): the timing channel does NOT provide same-model separation — matched warm-state pair collapses to ~1.08x; large gaps were transient thermal/DVFS state (fresh device), shrinking as state matched. Die-to-die variation is real but below this channel's noise floor; finer instruments (perf_event_open) are SELinux-denied to untrusted_app. Same-model anti-cloning rests on K_DBRW + per-device salt; the orbit remains a SoC-family / liveness signal. Full evidence in .claude/traces/sessions/TRACE-2026-06-11-001.json.
Secure-element attestation (Trezor Safe 7 / TROPIC01) becomes the controlling path for the optional offline-bearer authority tier, replacing C-DBRW silicon binding (retired per the dsm_anticlone ceiling theorem: no software-only physical channel can bind a transition against an off-device clone). - crypto/classical_verify.rs: scoped classical verifiers (Ed25519 TROPIC01, ECDSA-P256/SHA256 OPTIGA), fenced to external attestation; DSM's own crypto stays PQ (BLAKE3/SPHINCS+). - attestation/: frame_authenticate_device, dsm_island_challenge, verify_island_attestation (X.509 chain walk to pinned Safe 7 production roots; dev devices rejected), per-transition verify_island_intent_signature, SettlementPath decision (failure falls back to online, never a hard reject). - types/device_state.rs: IslandAttestation folded into compute_chain_tip append-only-when-present (zero canonical change for non-attested tips); OfflineBearerAttestation capability (deny-unless-proven, reset-on-recovery). - recovery/: faithful RelationshipChainStateProto codec carries island_attestation. - proto/dsm_app.proto + frontend dsm_app_pb.ts: IslandAttestationProto (field 11). - frontend: remove the C-DBRW dev screen + dbrw trust-protocol UI. - formal: reconcile lean4/tla code-correspondence with current code. Offline-bearer gate is implemented but activation-gated pending host transport. 1564 lib tests + clippy clean.
…to mod/signatures; update android manifest; prune stale docs
…(Phase 5) Offline-bearer transfers now require a hardware-island signature that is folded into the §16.6 successor tip, fail-closed. Non-attested transfers stay byte-identical (append-only-when-present encoding, proven by strict-prefix KAT). - attestation: dsm_ui_transcript (consent-oracle binding), dsm_offline_bearer payload hash, compute_anchor_set_id / canonical_anchor_set, OfflineBearerMode, compute_anchor_proof_hash (digest form — reconstructable from the receipt), canonical_signature_bundle; ui_transcript folded into the island challenge - crypto: AnchorTransport trait (+ Ed25519 verify helper, gated MockAnchorTransport) - types/operations: AuthorityPolicy on Operation::Transfer with append-only canonical encode/symmetric decode (trigger is per-Operation, NOT value_capability, NOT inside pre_commit) - bilateral: compute_successor_tip_attested (append-only; None delegates to the unchanged tip), run_offline_bearer_gate (6 fail-closed rejects), anchor_proof_hash_from_receipt, finalize wiring (no DeviceState mutation; caller flips Attested + supplies value_capability); manager anchor_transport field - device_state: IslandAttestation widened (id_anchor_set, ui_transcript_hash, policy_id) for receipt reconstructability; compute_chain_tip fold unchanged - proto + chain_segment: IslandAttestationProto fields 4/5, both codec sites - tests: 6-case gate unit test + finalize_with_offline_bearer_required_attests_or_ fails_closed e2e (admitted->attested, no-anchor->hard reject, byte-identity KAT) dsm lib 1600/0, dsm_sdk lib 1498/0. Known follow-up before offline-bearer activation: the dsm_sdk storage codec (storage/codecs.rs, smart_commitment_sdk.rs op_fingerprint) does not yet persist authority_policy (encode ignores it, decode sets None) — harmless while the mode is dormant, but offline-bearer ops will not round-trip through that codec until it is extended.
…one-per-device frontier (host)
Track 0 + Track 1 host side of the secmon-gated offline-bearer anchor.
- 9-field stateful receipt: proto IslandAttestationProto fields 6-12 + IslandAttestation
+ chain_segment codec, append-only. compute_chain_tip fold unchanged
(id_island/signature/policy_id only) -> byte-identity preserved.
- Challenge binding: dsm_island_challenge folds a receipt_commit over
{anchor_pubkey_hash, firmware_hash, policy_hash, parent_root, successor_root,
state_number}; a tampered field fails verification. firmware_hash==enrolled is
enforced implicitly (verify uses the pinned record.firmware_hash).
- Anchor frontier is ONE per device (keyed by id_anchor, NOT per relationship):
parent_root = the device's single stored_root; successor_root =
BLAKE3(parent_root || op || state); the gate reads + CAS-advances the single
frontier, fail-closed on a forked/non-monotonic advance.
- ChainTipStore gains get/set_anchor_frontier (default no-op; SDK/Noop untouched).
1602 dsm lib tests green, dsm_sdk builds clean, 0 regressions. Firmware app (Track 2),
secmon gate (Track 3), and hardware enroll (Track 4) pending.
…pt verifier
The adversarial gate review found the offline-bearer anchor check is SENDER-only and the
receiver never pins/re-verifies the enrolled identity: verify_anchor_signature folds the
DEVICE-self-reported firmware_hash circularly, and the host frontier CAS defaults to a no-op. So a
reflashed firmware with a fresh self-provisioned identity (or an erase-then-reprovision re-genesis)
would be accepted by a receiver. This adds the receiver-side invariant as tested code.
crypto/anchor_enrollment.rs:
- AnchorEnrollment: the pinned admission record {device_id, record(pubkey/firmware_id/
screen_template_id/firmware_hash), policy_hash, frontier_root, frontier_state}.
- verify_admitted_offline_bearer(enrollment, att, req): fail-closed — (1) anti-reprovision /
pinned pubkey, (2) pinned firmware_hash + policy_hash, (3) anchor signature verified against the
PINNED record (non-circular: folds the pinned firmware_hash, not the device-reported one), (4)
frontier parent==pinned + state+1 + fresh successor.
- AnchorEnrollmentStore trait + InMemoryAnchorEnrollmentStore: admit / get / CAS advance_frontier
(cross-receiver fork detector; errors on an unadmitted device).
Tests: 8/8 — accept genuine; reject unknown/clone identity, wrong firmware_hash, wrong
policy_hash, tampered successor_root, wrong parent vs pinned frontier; CAS serializes + rejects a
replay; advance on an unadmitted device errors. Additive only; existing 1602 lib tests unaffected.
NOT YET WIRED into the live bilateral protocol (deliberate, separate step): the attestation is
produced in the sender's finalize after the receiver accepted and the receiver-bound message carries
none. Activation = carry the IslandAttestation to the receiver before goods-release + populate the
store at admission + call the verifier in the accept path. The device's on-chip Track-2 frontier
already serializes signing; this is the receiver identity pin + cross-receiver mirror. End-to-end
offline-bearer anti-clone is NOT complete until the wire-carry + call site land.
…saction manager BilateralTransactionManager now owns an AnchorEnrollmentStore and exposes the receiver-side enforcement API: - admit_anchor(sender_device_id, record, policy_hash, initial_root, initial_state): pin a counterparty's anchor through the authority path (never implicitly from a receipt). - verify_incoming_offline_bearer_commit(sender_device_id, op, sender_current_tip, target_state_number, entropy, att): fail-closed receiver check + CAS frontier advance. - with_enrollment_store builder; default empty store (un-admitted sender -> reject). verify_offline_bearer_receipt (free fn next to run_offline_bearer_gate so the request reconstruction cannot drift): rebuilds the EXACT AnchorSignRequest the device signed (value_cap=Yes and mode=Required are gate invariants; expiry_tick==target_state_number; nonce==shared entropy; rel_key is symmetric), verifies it against the pinned identity/firmware/policy/frontier, then CAS-advances. Test receiver_pins_anchor_and_rejects_unadmitted_and_replay proves a genuine gate attestation is accepted by the receiver reconstruction (byte-identity sender<->receiver), and that a replay and an un-admitted sender are rejected. Full lib suite green (1610 tests).
…firm path Carries the offline-bearer anchor attestation to the receiver and verifies it where the receiver actually finalizes (the 3-step BLE confirm), closing the receiver half of the wiring: - proto BilateralConfirmRequest gains island_attestation (IslandAttestationProto) + anchor_expiry_tick. - handle_confirm_request: for OFFLINE_BEARER_REQUIRED transfers, decode the attestation and call BilateralTransactionManager::verify_incoming_offline_bearer_commit against the receiver's pinned enrollment (anchor pubkey + measured firmware hash + policy) and CAS-advance the pinned frontier, using the already-derived shared tip h_n and pre_entropy. Fail-closed: a missing attestation, an un-admitted/fresh identity, a non-enrolled firmware, or a forked/replayed frontier rejects the transfer. Ordinary transfers are unaffected (the gate predicate is false). - island_attestation_from_generated bridges the SDK's generated proto module to the core domain type. - sender confirm builder sets the new fields (None/0 today). Honest status: this is the RECEIVER half, fail-closed and inert by design until the two hardware-gated ends land — the sender produces an attestation only with the Safe 7 element signing transport (Track 4), and admission (the receiver pinning a counterparty anchor via admit_anchor) needs the identity exchanged at pairing, which needs the sender to hold one. Until then offline-bearer transfers reject rather than accept, the safe direction. dsm_sdk builds clean; envelope tests green.
… end-to-end, no hardware Adds the dsm_sdk 'mock-anchor' feature (-> dsm/test-mock) that stands in for the Safe 7 element so the whole anti-clone path runs on real phones over real BLE, mock element only: - init.rs + bluetooth/mod.rs: wire an in-process MockAnchorTransport (seeded deterministically from the device id via mock_anchor_seed) into BOTH production manager constructions. - bilateral_ble_handler: the sender forces a plain transfer to OFFLINE_BEARER_REQUIRED (shared maybe_force_offline_bearer_for_test, applied on BOTH the prepare request the receiver decodes AND the precommit, so both parties hash the same op), produces the attestation in send_bilateral_confirm (new manager attest_offline_bearer_for_commitment), and carries it. The receiver derives the sender's deterministic mock identity, auto-admits it (admit_anchor_if_absent — stands in for admission-at-pairing), and verifies + CAS-advances. - SqliteChainTipStore: in-memory anchor frontier (Arc<Mutex>) so the single per-device frontier advances across multiple transfers within a session (test added). Production builds (no mock-anchor) are byte-unchanged: no transport -> the sender produces no attestation and OFFLINE_BEARER_REQUIRED transfers fail closed; the forced-policy + auto-admit bodies compile to nothing. Both configs build clean; dsm manager tests (14) + the SDK frontier CAS test green. Test caveats (mock shortcuts, gated behind the feature): the auto-admit derives the counterparty identity from its device id (real admission is the authority path at pairing); the anchor + enrollment frontiers are in-memory (reset on app restart -> do a test run without restarting); an aborted transfer after the sender's frontier advanced desyncs until both re-admit. The attestation is verified as a side artifact (not yet folded into the shared chain tip).
… the element is mocked) Replaces the mock-anchor test shortcuts with production logic. The ONLY thing that stays mocked is the AnchorTransport (the element signing key — needs the ST-Link V3 to run on real silicon); everything else is unconditional production on both sides: - Policy: BilateralTransactionManager::apply_offline_bearer_policy stamps OFFLINE_BEARER_REQUIRED on a plain transfer IFF this device holds an anchor (manager::anchor_identity() is Some), pinning its own anchor set with the canonical dsm_offline_bearer_policy_id. No anchor -> plain transfer (online-checked fallback). Not feature-gated — the only feature-gated thing is wiring the mock transport in. Applied identically on the prepare request the receiver decodes AND the precommit. - Admission: the sender carries its REAL anchor identity (AnchorIdentityProto, new) in BilateralPrepareRequest. The receiver PINS it through the relationship-establishment authority path (handle_prepare_request -> admit_anchor_if_absent, idempotent), then verifies every confirm against that pinned identity + enrolled firmware hash. The device-id-derived auto-admit shortcut is DELETED. A clone/reflash presenting a different identity is rejected; no identity carried -> fail-closed. Swap-in path: when the ST-Link arrives, replace MockAnchorTransport with the real Safe 7 transport at the two manager-construction sites; the identity exchange, pinning, policy, gate, verify, and frontier CAS are unchanged. Both build configs compile clean. REMAINING production item: the sender frontier (chain_tip_store) and the receiver enrollment+frontier are still in-memory — SQLite persistence is the next commit so they survive an app restart.
…t (production durability) Replaces the in-memory anchor stores with SQLite, so the offline-bearer anti-clone state survives an app restart. An in-memory receiver frontier was a real gap: a restart reset it and a consumed-parent replay could go through. - Schema (client_db): anchor_frontiers (sender's per-device monotonic frontier, keyed by id_anchor) and anchor_enrollments (receiver's pinned identity + firmware hash + policy + frontier, keyed by counterparty device id). Created via CREATE TABLE IF NOT EXISTS in the existing schema-init. - client_db/anchor_persist.rs: get/CAS-set for the frontier; get/admit/CAS-advance for the enrollment. Read+write are atomic under the held DB connection lock. - SqliteChainTipStore: anchor-frontier methods now hit anchor_persist (the in-memory map is gone). - SqliteAnchorEnrollmentStore (new): implements the dsm AnchorEnrollmentStore trait against SQLite; wired into BOTH production manager constructions via with_enrollment_store (unconditional — the receiver must persist regardless of whether the element is mock or real). With this, the offline-bearer anti-clone is production on both sides except the element transport: identity exchange + pinning (authority path), policy, gate, verify, and BOTH frontiers persist. The only mocked piece is MockAnchorTransport behind cfg(mock-anchor); swap it for the real Safe 7 transport when the ST-Link arrives and nothing else changes. Both build configs compile clean.
…age, not anchor-related) The android+bluetooth-gated JNI path had not been recompiled since the domain-tags / jni-state refactor (#386 et al.) and no longer compiled — surfaced when the offline-bearer anchor work forced the first android rebuild. None of this is anchor logic; it just makes 'cargo ndk build --features jni,bluetooth' compile again so the .so can be built: - unified_protobuf_bridge.rs: restore the moved imports on the BLE path (BleTransportDelegate, jni::state::{parse_hex_32, DEVICE_ID_TO_ADDR}, client_db::get_contact_chain_tip, jni::objects:: {JObject, JValue}, tokio::runtime::Handle); rename the used param; add the Operation::Transfer fields (policy_commit, authority_policy) the metadata-hint construction was missing after those fields were added to the type. - handlers/bilateral_impl.rs: import DsmError on the android BLE chunk-send error path. - init.rs: drop a dead SignatureKeyPair import in the android bilateral-manager init (keypair comes from derive_device_signing_keypair). All gated to the android+bluetooth cfg; host builds unchanged.
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.
Summary
Wires the offline-bearer DeviceAnchor gate into bilateral
finalize, so an offline-bearer transfer requires a hardware secure-element (Safe 7 / TROPIC01) island signature, folded append-only into the §16.6 successor tip, fail-closed. This is the host half of the anti-clone feature: a software clone on different hardware cannot continue a value lineage because it cannot produce the island attestation.Non-attested transfers are byte-identical to before (append-only-when-present encoding;
None→ identical bytes, proven by a strict-prefix KAT). The mode is dormant until a caller injects an anchor + sets the OBR policy.What's in it
dsm_ui_transcript(consent-oracle binding), offline-bearer payload hash,compute_anchor_set_id/canonical_anchor_set,compute_anchor_proof_hash(digest form — reconstructable from the receipt),canonical_signature_bundle;ui_transcriptfolded into the island challenge.AnchorTransporttrait + Ed25519verify_anchor_signature+ gatedMockAnchorTransport.AuthorityPolicyonOperation::Transfer, append-only canonical encode / symmetric decode. Trigger is per-Operation— notvalue_capability, not insidepre_commit.compute_successor_tip_attested(append-only;Nonedelegates to the unchanged tip),run_offline_bearer_gate(6 fail-closed rejects),anchor_proof_hash_from_receipt, finalize wiring (noDeviceStatemutation — the caller flipsAttestedand suppliesvalue_capability).IslandAttestationwidened (id_anchor_set,ui_transcript_hash,policy_id) for receipt reconstructability;compute_chain_tipfold unchanged.IslandAttestationProtofields 4/5, both codec sites.Tests
finalize_with_offline_bearer_required_attests_or_fails_closede2e (admitted-anchor → attested; no-anchor → hard reject; byte-identity KAT).Known follow-up (before activation)
The dsm_sdk storage codec (
storage/codecs.rs,smart_commitment_sdk.rsop_fingerprint) does not yet persistauthority_policy(encode ignores it, decode setsNone). Harmless while the mode is dormant, but offline-bearer ops won't round-trip through that codec until it's extended.Device side (separate repo)
The Safe 7 firmware (
dsm_anchorapp) cut-1a is proven on the T3W1 emulator (2/2 device tests: get_identity, sign + host-verify, tamper-reject) and lives in thetrezor-firmwarefork — not in this PR.