# Device-Signer Provisioning Pattern for enrolling, using, rotating, and revoking Ed25519 signing keys on devices (IoT sensors, edge nodes, agents) that append data to a trusty chain. --- ## Problem Many trusty deployments have more than one source of writes: - A carbon-credit SaaS ingesting readings from hundreds of IoT sensors. - An AI-agent audit log receiving tool-call records from many running agents. - A monitoring/logging API collecting events from thousands of customer services. Each source needs a persistent cryptographic identity. The chain needs to record those signatures in a way external verifiers can check. The operator needs a way to recognize an enrolled device, retire a compromised one, or rotate a scheduled key. Trusty keeps chain verification self-contained (signature + embedded public key per row). Trust *policy* — which devices are still trusted today — lives in the operator's application layer, not the chain. --- ## Core Idea Trusty's v2 payload embeds each row's Ed25519 public key **inline** in the signature object (see `docs/payload-v2.md` §4). This means: - A proof bundle is **cryptographically self-verifying**: a consumer with the payload bytes can check `verify(pk, payload, sig)` without any registry lookup. - Whether a particular pk *should have been trusted* at append time is a separate question. That question is answered by an application-level **signer registry**. Two layers, cleanly separated: ``` +------------------------------------+ | Chain layer (trusty): | | raw_payload, row_hash, | | anchor_hash, signatures[pk,sig] | | -> verifies math, not policy | +------------------------------------+ | App layer (you): | | signer_registry table | | device_id, pk, enrolled_at, | | revoked_at, metadata | | -> verifies policy (was this | | device trusted at time T?) | +------------------------------------+ ``` --- ## Lifecycle ### 1. Enroll On the device: ```rust use trusty_core::{Ed25519Signer, Signer}; let signer = Ed25519Signer::generate(); // OS RNG let secret = signer.secret_bytes(); // persist to // device secure // storage let pk_hex = hex::encode(signer.public_key_bytes()); // send to // operator ``` On the operator side (app), insert into a registry: ```sql INSERT INTO signer_registry (device_id, public_key, enrolled_at) VALUES (?, ?, ?); ``` The secret never leaves the device. Only the public key is transmitted. ### 2. Sign and append ```rust let signer = Ed25519Signer::from_secret_bytes(&secret)?; trusty.append_signed("device_42", &data, "sensor://42", &signer)?; ``` The resulting row carries the signature with the pk embedded inline. ### 3. Rotate When a device's key reaches its scheduled lifetime (or for routine hygiene), generate a new keypair on the device and register a new entry (or update the existing one with a new pk + `rotated_at` timestamp). **Old rows remain fully verifiable** — the old pk lives inline in those rows. Only new rows use the new key. ### 4. Revoke If a device is decommissioned, lost, or suspected of compromise: ```sql UPDATE signer_registry SET revoked_at = ? WHERE device_id = ?; ``` Revocation is **not a chain event**. Revoked rows still carry valid signatures (the math is unchanged). The app decides to distrust them: - A proof-bundle consumer joins the bundle's rows against the registry and flags any row where the pk's `revoked_at` precedes the row's `detected_at`. - An auditor treats revoked-key rows as quarantined, not missing. --- ## Compromise Scenarios Per `docs/threat-model.md` §6 (per-key compromise), trusty's surface already addresses the crypto side. The registry pattern closes the policy side. | Scenario | Chain behavior | Registry response | |--------------------------------------- |---------------------------------|---------------------------------------| | Device secret leaked; attacker signs | Row sig verifies mathematically | Revoke pk; downstream flags the row | | Old key rotated out | Old-key rows still verify | Mark `rotated_at`; retain the entry | | Device destroyed | Future writes just stop | Mark `revoked_at` with reason | | Clone/replay across devices | Chain sequence gate detects it | Registry has no further role | --- ## Registry Schema (Reference, Not Mandatory) Trusty does **not** prescribe a registry schema. Apps vary: a carbon- credit platform cares about `device_type` and `location`; a monitoring SaaS cares about `customer_id` and `plan`; an agent log cares about `agent_version` and `operator_account`. A minimal schema: ```sql CREATE TABLE signer_registry ( device_id TEXT PRIMARY KEY, public_key TEXT NOT NULL, -- hex-encoded 32 bytes enrolled_at TEXT NOT NULL, -- ISO-8601 rotated_at TEXT, -- NULL until rotated revoked_at TEXT, -- NULL until revoked note TEXT ); CREATE INDEX idx_sr_pk ON signer_registry(public_key); ``` The pk index exists so a verifier can look up registry status given a pk extracted from a chain row. --- ## Worked Example See `crates/trusty-sqlite/examples/device_signer.rs` for a runnable demo that walks enroll → sign → rotate → revoke end to end on the same database file trusty uses, using a second `rusqlite::Connection` for the registry table. Run: ``` cargo run -p trusty-sqlite --example device_signer ``` --- ## What Trusty Does NOT Do (and Why) - **No built-in registry.** Schema needs vary per product; a prescribed schema would either over-serve (unused fields) or under-serve (missing fields). Apps roll their own table. - **No revocation as chain event.** Chain rows are immutable by design. Introducing a "this pk is revoked" row would conflate cryptographic truth (the sig is valid) with operational policy (we no longer trust it). Those belong in different places. - **No rotation ceremony.** Each v2 row carries its own pk inline; switching to a new signer at append time is a one-line change on the device. No chain-side mechanism needed. The `Signer` trait accepts any backing implementation (HSM, TPM, cloud KMS, file-backed). `Ed25519Signer` is the default software impl; production deployments commonly substitute a hardware-backed signer.