Trusty: provenance library workspace (initial import)
This commit is contained in:
196
docs/device-signer-provisioning.md
Normal file
196
docs/device-signer-provisioning.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user