Files
Trusty/docs/device-signer-provisioning.md

197 lines
6.5 KiB
Markdown

# 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.