Give the device a key, and sign consent with it #18

Merged
LNK merged 1 commit from claude/device-key into main 2026-09-04 21:31:28 +00:00
Contributor

The consent record's signature field held the string unsigned-dev because no device key existed.

What changes. The device has an Ed25519 key derived from the store's data key, so it lives under the same custody and is stable across restarts. Consent is signed over the record's canonical JSON; the consent event on the log is the signed record with its signature; the public key travels in every batch header and shows on the Account tab. The server's adapter verifies each consent line against the header's key and refuses one that does not verify. A header with no key, from an older client, is not checked and the code says so.

Proof. Canonical JSON pinned to one literal on both sides; a signed consent verifies and stops verifying when anything in it moves; the key survives reopening the store; the consent on the log verifies against the store's key; the header carries the key; the adapter refuses a bad consent line. The verifier accepting everything fails the test, checked by making it. Linux verifier green on the pinned toolchain; the Windows crates cross-check clean; 67 adapter tests pass.

The consent record's `signature` field held the string `unsigned-dev` because no device key existed. **What changes.** The device has an Ed25519 key derived from the store's data key, so it lives under the same custody and is stable across restarts. Consent is signed over the record's canonical JSON; the consent event on the log is the signed record with its signature; the public key travels in every batch header and shows on the Account tab. The server's adapter verifies each consent line against the header's key and refuses one that does not verify. A header with no key, from an older client, is not checked and the code says so. **Proof.** Canonical JSON pinned to one literal on both sides; a signed consent verifies and stops verifying when anything in it moves; the key survives reopening the store; the consent on the log verifies against the store's key; the header carries the key; the adapter refuses a bad consent line. The verifier accepting everything fails the test, checked by making it. Linux verifier green on the pinned toolchain; the Windows crates cross-check clean; 67 adapter tests pass.
The consent schema has always carried a `signature`: "over the record by the device key, so
consent cannot be back-dated". The field held the string `unsigned-dev`, because no device
key existed. A record that says it is signed and is not is worse than one that says nothing:
it is a claim a reader stops checking.

The device has a key now, and it is derived rather than generated. The store's data key is
already the thing the client keeps under its best custody — wrapped to the user by DPAPI where
that exists, gone when the folder is deleted, the same across restarts — so the signing key
is an Ed25519 key derived from it under a fixed purpose, HMAC-SHA256, and inherits all three
properties with no second file to lose or leave behind. The public half travels in every
batch header as `device.public_key`, and the console shows its first sixteen characters
beside the device id.

What is signed is the consent record's canonical JSON — keys sorted at every level, no
whitespace, arrays in order — because a signature is only over the bytes that were signed,
and two serialisers of the same record must agree on them. The rule is written once in the
store's `identity` module and once in the server's adapter, and both are pinned to the same
literal by their tests. The consent event on the log is exactly the signed record with the
signature beside it, from one function, so the bytes the device signed are the bytes the
server checks.

The server checks them. A consent line arriving under a header that names the device's key
is verified against it, and one that does not verify is refused: a grant flipped, a date
moved, another device's key, or the old placeholder. A header with no key — an older
client — is not checked, because there is nothing to check against, and the code says so
rather than passing silently.

Tests: canonical JSON sorts keys at every level and matches the shared literal, on both
sides; a signed consent verifies and stops verifying when a grant is flipped, the date moves,
the key is another device's, or the signature is the placeholder; the key is the same across
two opens of the same store and distinct from the data key; the consent on the gate's log
verifies against the store's key; the batch header carries the key consent was signed with;
and the adapter refuses a consent line that does not verify and checks nothing without a key.
The verify test fails against a verifier that accepts everything — checked, by making it one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LNK merged commit b32e23feb9 into main 2026-09-04 21:31:28 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mikebrandon/here-telemetry-rnd!18
No description provided.