// ============================================================ // EXAMPLE: device_signer // PURPOSE: Walk the device-signer provisioning pattern end to end. // Enroll -> sign -> rotate -> revoke. Demonstrates how an // application keeps a signer_registry alongside trusty's // chain tables, using a second rusqlite::Connection to // the same file. // // See `docs/device-signer-provisioning.md` for the conceptual doc. // // RUN: cargo run -p trusty-sqlite --example device_signer // ============================================================ use rusqlite::{params, Connection}; use serde_json::{json, Value}; use std::path::PathBuf; use trusty_core::{Ed25519Signer, Ed25519Verifier, SecretKey, Signer, Verifier}; use trusty_sqlite::Trusty; fn tmp_db_path() -> PathBuf { let mut p = std::env::temp_dir(); let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_nanos()) .unwrap_or(0); p.push(format!("trusty_device_signer_demo_{nanos}.db")); p } fn now_iso() -> String { chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) } fn init_registry(conn: &Connection) { conn.execute_batch( "CREATE TABLE IF NOT EXISTS signer_registry ( device_id TEXT PRIMARY KEY, public_key TEXT NOT NULL, enrolled_at TEXT NOT NULL, rotated_at TEXT, revoked_at TEXT, note TEXT ); CREATE INDEX IF NOT EXISTS idx_sr_pk ON signer_registry(public_key);", ) .expect("registry schema"); } fn enroll(conn: &Connection, device_id: &str, pk_hex: &str, note: &str) { conn.execute( "INSERT INTO signer_registry (device_id, public_key, enrolled_at, note) VALUES (?1, ?2, ?3, ?4)", params![device_id, pk_hex, now_iso(), note], ) .expect("enroll"); println!(" [enrolled] device={device_id} pk={pk_hex:.16}... ({note})"); } fn rotate(conn: &Connection, old_device_id: &str, new_device_id: &str, new_pk_hex: &str) { conn.execute( "UPDATE signer_registry SET rotated_at = ?1 WHERE device_id = ?2", params![now_iso(), old_device_id], ) .expect("mark rotated"); enroll(conn, new_device_id, new_pk_hex, "post-rotation"); println!(" [rotated] {old_device_id} -> {new_device_id}"); } fn revoke(conn: &Connection, device_id: &str, reason: &str) { conn.execute( "UPDATE signer_registry SET revoked_at = ?1, note = ?2 WHERE device_id = ?3", params![now_iso(), format!("REVOKED: {reason}"), device_id], ) .expect("revoke"); println!(" [revoked] device={device_id} reason={reason}"); } fn trust_status_for_pk(conn: &Connection, pk_hex: &str) -> String { conn.query_row( "SELECT device_id, enrolled_at, rotated_at, revoked_at FROM signer_registry WHERE public_key = ?1", params![pk_hex], |r| { let dev: String = r.get(0)?; let enrolled: String = r.get(1)?; let rotated: Option = r.get(2)?; let revoked: Option = r.get(3)?; Ok(match (rotated, revoked) { (_, Some(rev)) => format!("REVOKED({dev}, at={rev})"), (Some(rot), _) => format!("ROTATED({dev}, at={rot}) -- old-key rows still verify"), _ => format!("ACTIVE({dev}, since={enrolled})"), }) }, ) .unwrap_or_else(|_| "UNKNOWN (pk not in registry)".to_string()) } fn sign_and_append(t: &mut Trusty, entity: &str, data: &Value, signer: &Ed25519Signer) { let r = t .append_signed(entity, data, "sensor://demo", signer) .expect("signed append"); println!( " [appended] seq={} row_hash={:.16}... pk={:.16}...", r.sequence, r.row_hash, hex::encode(signer.public_key_bytes()) ); } fn main() { let path = tmp_db_path(); println!("trusty device-signer provisioning demo"); println!("db: {}\n", path.display()); // Trusty owns one Connection; the app opens a second one for the // registry. WAL mode (set by trusty-sqlite's schema) allows this // cleanly. let mut t = Trusty::open(&path, SecretKey::new(*b"0123456789abcdef")).expect("open trusty"); let app_conn = Connection::open(&path).expect("open app conn"); init_registry(&app_conn); // --- 1. Enroll device_001 ------------------------------------- println!("step 1: enroll device_001"); let signer_v1 = Ed25519Signer::generate(); let pk_v1_hex = hex::encode(signer_v1.public_key_bytes()); enroll(&app_conn, "device_001", &pk_v1_hex, "initial enrollment"); // --- 2. Sign and append with v1 key --------------------------- println!("\nstep 2: append two signed rows with v1 key"); sign_and_append(&mut t, "sensor_a", &json!({"temp": 21.4}), &signer_v1); sign_and_append(&mut t, "sensor_a", &json!({"temp": 21.5}), &signer_v1); // --- 3. Rotate: new key, new registry entry ------------------- println!("\nstep 3: rotate device_001 -> device_001_v2"); let signer_v2 = Ed25519Signer::generate(); let pk_v2_hex = hex::encode(signer_v2.public_key_bytes()); rotate(&app_conn, "device_001", "device_001_v2", &pk_v2_hex); // --- 4. Append more rows with v2 key -------------------------- println!("\nstep 4: append two more signed rows with v2 key"); sign_and_append(&mut t, "sensor_a", &json!({"temp": 21.7}), &signer_v2); sign_and_append(&mut t, "sensor_a", &json!({"temp": 21.9}), &signer_v2); // --- 5. Revoke v1 (simulated compromise) ---------------------- println!("\nstep 5: simulated compromise of v1 secret, revoke it"); revoke(&app_conn, "device_001", "secret leaked from retired device"); // --- 6. Verify chain: math is still sound --------------------- println!("\nstep 6: chain verify -- cryptography is unaffected by registry policy"); let report = t.verify("sensor_a").expect("verify"); println!( " chain verify: ok={} row_count={} (revocation is app-policy, not chain-policy)", report.rows_valid, report.row_count ); // --- 7. Registry overlay: per-row trust report ---------------- println!("\nstep 7: registry-overlay trust report for each chain row"); let bundle = t.export_bundle("sensor_a", None, None).expect("bundle"); for row in &bundle.rows { if let Some(raw) = &row.raw_payload { let parsed: Value = serde_json::from_str(raw).expect("payload json"); let pk_hex = parsed .get("signatures") .and_then(|s| s.as_array()) .and_then(|a| a.first()) .and_then(|o| o.get("pk")) .and_then(|v| v.as_str()); match pk_hex { Some(pk) => { println!( " seq={} event={} -> {}", row.sequence, row.event_type, trust_status_for_pk(&app_conn, pk) ); } None => { println!( " seq={} event={} -> (unsigned v1 row)", row.sequence, row.event_type ); } } } } // --- 8. Sanity: Ed25519Verifier round-trips on an isolated sig println!("\nstep 8: sanity -- Ed25519Verifier on the v2 public key works end-to-end"); let msg = b"independent verifier check"; let sig = signer_v2.sign(msg); Ed25519Verifier::verify(&signer_v2.public_key_bytes(), msg, &sig).expect("independent verify"); println!(" standalone verify: ok"); println!("\ndone. (cleanup: rm {})", path.display()); }