196 lines
7.6 KiB
Rust
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());
|
|
}
|