Trusty: provenance library workspace (initial import)

This commit is contained in:
2026-09-09 18:31:00 -04:00
commit 693f3bef1e
73 changed files with 26295 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
// ============================================================
// EXAMPLE: bench_append
// PURPOSE: Measure fsync-amortization speedup of append_batch over
// a loop of append() calls on a file-backed SQLite DB.
// Intended for ad-hoc inspection, not CI regression
// tracking (no criterion / no gate). Prints rows/sec +
// speedup ratio.
//
// RUN: cargo run -p trusty-sqlite --release --example bench_append
// (release mode matters: debug rusqlite is much slower)
// ============================================================
use serde_json::json;
use std::time::Instant;
use trusty_core::SecretKey;
use trusty_sqlite::{RowSpec, Trusty};
const N: usize = 1000;
fn tmp_db_path(tag: &str) -> std::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_bench_{tag}_{nanos}.db"));
p
}
fn key() -> SecretKey {
SecretKey::new(*b"0123456789abcdef")
}
fn bench_single_row(path: &std::path::Path) -> f64 {
let mut t = Trusty::open(path, key()).expect("open");
// Warmup: one genesis so we are measuring mutations, not first-row setup.
t.append("acme", &json!({"warmup": true}), "s")
.expect("warmup");
let start = Instant::now();
for i in 0..N {
t.append("acme", &json!({"i": i}), "s").expect("append ok");
}
let elapsed = start.elapsed().as_secs_f64();
N as f64 / elapsed
}
fn bench_batch(path: &std::path::Path) -> f64 {
let mut t = Trusty::open(path, key()).expect("open");
t.append("acme", &json!({"warmup": true}), "s")
.expect("warmup");
let values: Vec<serde_json::Value> = (0..N).map(|i| json!({"i": i})).collect();
let specs: Vec<RowSpec> = values
.iter()
.map(|v| RowSpec {
entity_id: "acme",
data: v,
source_url: "s",
})
.collect();
let start = Instant::now();
t.append_batch(&specs).expect("batch ok");
let elapsed = start.elapsed().as_secs_f64();
N as f64 / elapsed
}
fn main() {
let p1 = tmp_db_path("single");
let p2 = tmp_db_path("batch");
println!("trusty-sqlite append benchmark (N = {N}, file-backed, WAL + FULL sync)");
println!("single-row db : {}", p1.display());
println!("batch db : {}", p2.display());
println!();
let single_rps = bench_single_row(&p1);
let batch_rps = bench_batch(&p2);
let speedup = batch_rps / single_rps;
println!("single-row append : {single_rps:>10.1} rows/sec");
println!("append_batch : {batch_rps:>10.1} rows/sec");
println!("speedup : {speedup:>10.2}x");
println!();
println!("(cleanup: rm {} {})", p1.display(), p2.display());
}

View File

@@ -0,0 +1,195 @@
// ============================================================
// 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());
}

View File

@@ -0,0 +1,186 @@
// ============================================================
// EXAMPLE: twin_coupling
// PURPOSE: Demonstrate the data-convention pattern for linking a
// digital-twin computation row back to the sensor rows
// that fed it. Sensor chain and twin chain are independent
// trusty entities. The twin row carries a `source_refs`
// array in its `data` field so an external verifier can
// check the linkage from two ProofBundles alone -- no
// extra tables, no schema changes.
//
// See `docs/twin-coupling.md` for the conceptual doc.
//
// RUN: cargo run -p trusty-sqlite --example twin_coupling
// ============================================================
use serde_json::{json, Value};
use std::path::PathBuf;
use trusty_core::SecretKey;
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_twin_coupling_demo_{nanos}.db"));
p
}
fn main() {
let path = tmp_db_path();
println!("trusty twin-coupling demo");
println!("db: {}\n", path.display());
let sip_key = SecretKey::new(*b"0123456789abcdef");
let mut t = Trusty::open(&path, sip_key.clone()).expect("open trusty");
// ----------------------------------------------------------------
// step 1: append sensor readings to the sensor chain
// ----------------------------------------------------------------
println!("step 1: append three sensor readings");
let r0 = t
.append("sensor-001", &json!({"temp_c": 21.1}), "iot://sensor-001")
.expect("append r0");
println!(
" seq={} chain_id={:.16}... row_hash={}",
r0.sequence, r0.chain_id, r0.row_hash
);
let r1 = t
.append("sensor-001", &json!({"temp_c": 21.4}), "iot://sensor-001")
.expect("append r1");
println!(
" seq={} chain_id={:.16}... row_hash={}",
r1.sequence, r1.chain_id, r1.row_hash
);
let r2 = t
.append("sensor-001", &json!({"temp_c": 21.6}), "iot://sensor-001")
.expect("append r2");
println!(
" seq={} chain_id={:.16}... row_hash={}",
r2.sequence, r2.chain_id, r2.row_hash
);
// ----------------------------------------------------------------
// step 2: twin computation cites the three sensor rows by
// chain_id + sequence + row_hash (data convention)
// ----------------------------------------------------------------
println!("\nstep 2: append twin computation row with source_refs");
let avg = (21.1_f64 + 21.4 + 21.6) / 3.0;
let twin_data = json!({
"computation": "rolling_avg_temp_c",
"result": (avg * 100.0).round() / 100.0,
"source_refs": [
{"chain_id": r0.chain_id, "sequence": r0.sequence, "row_hash": r0.row_hash},
{"chain_id": r1.chain_id, "sequence": r1.sequence, "row_hash": r1.row_hash},
{"chain_id": r2.chain_id, "sequence": r2.sequence, "row_hash": r2.row_hash},
]
});
let tw = t
.append("twin-001", &twin_data, "omniverse://twin-001")
.expect("append twin");
println!(" seq={} row_hash={}", tw.sequence, tw.row_hash);
// ----------------------------------------------------------------
// step 3: verify both chains independently
// ----------------------------------------------------------------
println!("\nstep 3: verify both chains");
let sensor_report = t.verify("sensor-001").expect("verify sensor");
println!(
" sensor-001: ok={} rows={}",
sensor_report.rows_valid, sensor_report.row_count
);
assert!(sensor_report.rows_valid, "sensor chain verification failed");
let twin_report = t.verify("twin-001").expect("verify twin");
println!(
" twin-001: ok={} rows={}",
twin_report.rows_valid, twin_report.row_count
);
assert!(twin_report.rows_valid, "twin chain verification failed");
// ----------------------------------------------------------------
// step 4: external-verifier cross-chain check
// Export both ProofBundles; walk twin source_refs and
// confirm each referenced row_hash is present and matches
// in the sensor bundle.
// ----------------------------------------------------------------
println!("\nstep 4: cross-chain verify from ProofBundles");
let sensor_bundle = t
.export_bundle("sensor-001", None, None)
.expect("sensor bundle");
let twin_bundle = t
.export_bundle("twin-001", None, None)
.expect("twin bundle");
let sensor_verify = sensor_bundle.verify(&sip_key);
let twin_verify = twin_bundle.verify(&sip_key);
println!(
" sensor bundle verify: ok={} rows={}",
sensor_verify.rows_valid, sensor_verify.row_count
);
println!(
" twin bundle verify: ok={} rows={}",
twin_verify.rows_valid, twin_verify.row_count
);
assert!(sensor_verify.rows_valid);
assert!(twin_verify.rows_valid);
// Walk twin rows; for each source_ref, confirm the sensor bundle
// contains a row at that sequence with a matching row_hash.
let mut refs_checked = 0usize;
for twin_row in twin_bundle.rows.iter().filter(|r| r.sequence >= 0) {
let payload: Value = twin_row
.raw_payload
.as_ref()
.and_then(|p| serde_json::from_str(p).ok())
.expect("twin row has no payload");
let data = payload.get("data").expect("payload has no data");
let refs = match data.get("source_refs").and_then(|v| v.as_array()) {
Some(r) => r,
None => continue,
};
for sr in refs {
let ref_chain_id = sr["chain_id"].as_str().expect("chain_id");
let ref_seq = sr["sequence"].as_i64().expect("sequence");
let ref_hash = sr["row_hash"].as_str().expect("row_hash");
let matched = sensor_bundle.rows.iter().find(|r| {
r.chain_id == ref_chain_id
&& r.sequence == ref_seq
&& r.row_hash.as_deref() == Some(ref_hash)
});
match matched {
Some(m) => {
println!(
" source_ref ok: seq={} row_hash={} (sensor chain_id={:.16}...)",
m.sequence, ref_hash, ref_chain_id
);
refs_checked += 1;
}
None => {
panic!(
"source_ref not found in sensor bundle: chain_id={} seq={} row_hash={}",
ref_chain_id, ref_seq, ref_hash
);
}
}
}
}
println!("\n {refs_checked} source_refs verified across ProofBundles");
assert_eq!(refs_checked, 3, "expected 3 source_refs checked");
println!("\ndone. (cleanup: rm {})", path.display());
}