Files

196 lines
7.6 KiB
Rust

// ============================================================
// 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<String> = r.get(2)?;
let revoked: Option<String> = 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());
}