Skip to content

security: a transfer's memo, ticker and nonce are in no public object (pre-audit item 4) - #1102

Merged
cryptskii merged 15 commits into
mainfrom
security/minimum-disclosure
Oct 2, 2026
Merged

cryptskii merged 15 commits into
mainfrom
security/minimum-disclosure

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Summary

Pre-audit item 4, recorded in CONFORMANCE_GAPS.md §6.71. Owner ruling, 2026-10-02: "one shape, BLE carries terms".

The finding. A transfer's signed operation carried its memo, token ticker, nonce, mode, the recipient's public key and the recipient's id as text, all in the clear. Those bytes hash into the tip, ride the economic admission into the register, and are read again by later counterparties walking a credit's source. So the memo, and a nonce predictable from public identifiers (§6.67), outlived the sealed spool in every public object that carries the operation.

What changes.

  • One shape. Operation::Transfer is {to_device_id, amount, policy_commit, terms_commitment, signature, authority_policy}, canonical tag 37. Tag 3 is retired and never reused. recipient and to are deleted.
  • Terms. TransferTerms {token_id, nonce, mode, memo, salt}. The commitment is H(DSM/transfer-terms/v1; terms), under a fresh 256-bit salt; a salt under 128 bits is refused.
  • Carriage.
    • Online: OnlineTransferRequest.transfer_terms (field 15), inside the sealed spool payload.
    • BLE: BilateralPrepareRequest.transfer_terms (field 19), beside operation_data.
  • Opening. Every reader opens the terms against the signed commitment first:
    • recognition (recipient_dispatch): missing, undecodable or non-opening terms mean the transfer is not recognized and nothing is staged;
    • the canonical apply: the nonce it spends and the token come from the opened terms;
    • the BLE prepare (step_terms): refused before any session exists, and checked again when a stored session is restored;
    • the inbox, history and BLE events: they show the token and memo only from opened terms.
  • Assets by commit. The sender's debit, the BLE allocation spend and the BLE credit use the signed policy_commit, and each requires the opened ticker to resolve to it. The recovery egress gate names a transfer's lock from its commit, and blocks an asset it cannot name while any lock is held.
  • Storage. recipient_staged_transfer.terms_bytes and bilateral_sessions.terms_bytes; client schema 28.
  • Frontend binding. dsm_app_pb.ts regenerated.

Related Issues

Security pre-audit (#1096, #1099, #1100), item 4.

Testing

  • cargo clippy --all-targets -- -D warnings (root workspace), cargo fmt, real-code guard and ci/conformance_evidence.py all pass.
  • cargo test -p dsm passes, including the new transfer_terms tests.
  • cargo test -p dsm_vertical_validation passes.
  • The dsm_sdk lib suite passes: 1,078 of 1,079. The remaining one, pairing_smoke_identity_before_connection, fails the same way on main's binary when run alone; it depends on test order, and CI runs in that order.
  • New tests:
    • a_transfer_without_terms_that_open_it_is_not_recognized (online)
    • a_bearer_prepare_without_terms_that_open_it_is_refused (BLE)
    • no_node_holds_a_transfers_memo_ticker_or_nonce: scans every row of every node table for the memo, nonce, salt and length-prefixed ticker
    • a_transfer_settles_only_with_terms_that_open_it
  • Mutation controls, all killed:
    • the commitment check removed (three tests red);
    • online terms taken without opening;
    • BLE terms taken without opening;
    • the salt taken from public data.
  • The code-map pins need a repin from this PR's own map. That is in progress.

Notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u


Generated by Claude Code

cryptskii and others added 14 commits October 1, 2026 23:12
…truction v2)

Pre-audit item 17. Version 1 gave every leaf of a bottom tree one FORS key
(the key pair word carried the FORS tree number, never the leaf), hashed
WOTS+ compression under the hashtree's own addresses, shared addresses
between FORS leaves and the first internal level (heights off by one) and
between every PRF call and its chain, and left PK.seed out of PRF.

Owner ruling 2026-10-01: "Fix structure, keep BLAKE3." Every algorithm now
follows FIPS 205 with its 32-byte ADRS, seven address types and
setTypeAndClear; PRF, the tweakable hash, PRF_msg and H_msg each run under
their own BLAKE3 KDF context; H_msg yields FIPS m bytes; signing verifies
before it returns. Tests record every hash call by address and hold the
separation, the FORS-to-leaf binding and the PRF's public seed; frozen
vectors for SPX128f and SPX256f. A clean cut: every key and signature
changes.
…rithm id 0x0002

dsm::crypto::sphincs becomes the host's view of crates/dsm-sphincs, so the
host and the anchor firmware run one construction (no second copy to
drift). SPHINCS_PLUS_SPX256F is 0x0002, construction version 2; 0x0001 is
retired and undeclared, so nothing under version 1 is recognized. The
unused DSM/sphincs-kdf tag, the sphincs-trace feature and the duplicate
KAT file go; fixtures derive their algorithm id; the keygen digests are
regenerated.
The genesis parameters' independent encoding writes algorithm 0x0002; the
SoFi golden digests over a setup or precommit body (the setup reference,
the precommit id, and the single and multivault external commitments)
follow the body's algorithm id; the altered-algorithm decode writes the
retired id.
CONFORMANCE §6.70 records the ruling (owner, 2026-10-01: "Fix structure,
keep BLAKE3"), the fix, the tests, the eight mutation controls and the
vectors that moved, and keeps the owner's distinction: the structure
restores what a SPHINCS+-style proof assumes; BLAKE3 remains a non-FIPS,
custom instantiation. MR-DSM-0259 re-verified; matrix rows for the
structure; SECURITY.md's limitations rewritten for version 2; the
evidence index covers dsm_sphincs; the anchor crates' lockfiles take the
new dependency edge (only this change's lines).
Two conflicts, the conformance record and the verification matrix: each
side appended at the same place (main's §6.69 and item 11 rows, this
branch's §6.70 and SPHINCS+ rows). Both kept, §6.69 before §6.70.
Conformance evidence passes; the real-code guard passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
…y commit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
… (pre-audit item 4)

Owner ruling 2026-10-02, one shape, BLE carries terms. The signed transfer
keeps the recipient, amount, policy commit and authority policy, plus a
salted commitment to its terms (token, nonce, mode, memo). The terms ride
inside the sealed spool request online and beside the operation on BLE; a
recipient refuses a transfer whose terms are missing or do not open the
commitment. Tag 3 is retired; the transfer is tag 37. CONFORMANCE §6.71.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
The map built locally at de67d9d. Every moved pin moved for its code alone;
its evidence ran on this tree and passed: dsm (all suites), dsm_storage_node
(all), and the 47 dsm_sdk tests the moved pins cite.
make requirement-map-intent: 635 PINNED, 0 failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
CodeQL (rust/hard-coded-cryptographic-value) flagged two flows into the new
terms constructor's nonce: the salt-freshness test's literal nonce, and the
fee transfer's OS-random nonce (the zeroed buffer OsRng fills). The fee
transfer's nonce was redundant: nothing spends it, and its fresh terms salt
already makes each fee transfer's operation bytes its own. Both now pass an
empty nonce, as the offline and generic transfers do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
Comment thread dsm_client/deterministic_state_machine/dsm/src/types/operations.rs Fixed
Map built locally at e60df85; each moved pin's evidence ran and passed (the
dsm suites and the 38 dsm_sdk tests they cite). make requirement-map-intent:
635 PINNED, 0 failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
@cryptskii
cryptskii merged commit 4b42507 into main Oct 2, 2026
27 checks passed
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.

3 participants