diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cc4226..4febc32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,15 @@ env: RUSTFLAGS: -D warnings CARGO_TARGET_DIR: ${{ github.workspace }}/target +permissions: + contents: read + jobs: rust: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: components: rustfmt, clippy - name: fmt @@ -33,4 +36,4 @@ jobs: - name: python (no GPU, no torch) run: bash scripts/ci_python.sh - name: stdlib receipt verifier - run: python3 adapters/verify_receipt.py crates/lictor-receipt/tests/fixtures/receipt/receipt_000007.json | grep -q '^OK' + run: python3 adapters/verify_receipt.py crates/lictor-receipt/tests/fixtures/receipt/receipt_000007.json --pubkey 03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8 | grep -q '^OK' # the committed test key, TEST_PUBKEYS[0] diff --git a/.gitignore b/.gitignore index 9363f15..ccaed20 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ venv/ Thumbs.db .vscode/ .idea/ + +# local secrets +.env +.env.* +!.env.example diff --git a/crates/lictor-cli/src/cmd/ack.rs b/crates/lictor-cli/src/cmd/ack.rs index 755dd84..3341390 100644 --- a/crates/lictor-cli/src/cmd/ack.rs +++ b/crates/lictor-cli/src/cmd/ack.rs @@ -11,7 +11,9 @@ use std::path::{Path, PathBuf}; use lictor_core::AckDecision; -use lictor_receipt::{default_key_dir, load_nonces, load_seed, save_nonces, sign_ack, AckToken, HandoffRecord}; +use lictor_receipt::{ + default_key_dir, load_nonces, load_seed, save_nonces, sign_ack, AckToken, HandoffRecord, +}; use crate::render::{glossary, json_out, kv, pretty_sorted, print_lines, sanitize, short}; @@ -60,8 +62,8 @@ pub fn resolve_handoff(arg: &str) -> anyhow::Result<(String, Option anyhow::Result { eprintln!("lictor key init: refused: {e}"); return Ok(2); } - let out = KeyOut { pubkey: pubkey_hex(&seed), path: display(&path), role, mode: MODE_TEXT.to_string() }; + let out = + KeyOut { pubkey: pubkey_hex(&seed), path: display(&path), role, mode: MODE_TEXT.to_string() }; if json { json_out(&out)?; } else { diff --git a/crates/lictor-cli/src/cmd/ledger.rs b/crates/lictor-cli/src/cmd/ledger.rs index 7d56e8f..344a488 100644 --- a/crates/lictor-cli/src/cmd/ledger.rs +++ b/crates/lictor-cli/src/cmd/ledger.rs @@ -8,7 +8,9 @@ use std::path::{Path, PathBuf}; use clap::Subcommand; -use lictor_receipt::{append_ledger, read_ledger, verify, verify_ledger, LedgerEntry, SignedReceipt, LEDGER_SCHEMA}; +use lictor_receipt::{ + append_ledger, read_ledger, verify, verify_ledger, LedgerEntry, SignedReceipt, LEDGER_SCHEMA, +}; use crate::render::{json_out, print_lines, sanitize, short, status}; @@ -63,22 +65,20 @@ pub fn verify_file(p: &Path) -> anyhow::Result { let entries: Vec = read_ledger(p).map_err(|e| anyhow::anyhow!("{e}"))?; let rep = verify_ledger(&entries); let header = ledger_header(p); - let run_id = header.as_ref().and_then(|h| h.get("run_id")).and_then(|v| v.as_str()).unwrap_or("").to_string(); - let arm_id = header.as_ref().and_then(|h| h.get("arm_id")).and_then(|v| v.as_str()).unwrap_or("").to_string(); + let run_id = + header.as_ref().and_then(|h| h.get("run_id")).and_then(|v| v.as_str()).unwrap_or("").to_string(); + let arm_id = + header.as_ref().and_then(|h| h.get("arm_id")).and_then(|v| v.as_str()).unwrap_or("").to_string(); let fuse_ok = entries.iter().filter(|e| e.fuse_ok).count() as u32; let mut lines = Vec::new(); if rep.chain_ok { - let pct = if rep.episodes == 0 { 0.0 } else { 100.0 * f64::from(rep.successes) / f64::from(rep.episodes) }; + let pct = + if rep.episodes == 0 { 0.0 } else { 100.0 * f64::from(rep.successes) / f64::from(rep.episodes) }; lines.push(format!( " episodes {} chain ok success {}/{} ({:.1}%) stops {} escalated {} fuse_ok {}/{}", rep.episodes, rep.successes, rep.episodes, pct, rep.stops, rep.escalations, fuse_ok, rep.episodes )); - lines.push(format!( - " head {} run {} arm {}", - rep.head, - sanitize(&run_id), - sanitize(&arm_id) - )); + lines.push(format!(" head {} run {} arm {}", rep.head, sanitize(&run_id), sanitize(&arm_id))); let foreign = entries.iter().filter(|e| e.run_id != run_id || e.arm_id != arm_id).count(); if !run_id.is_empty() && foreign > 0 { lines.push(format!(" warning: {foreign} entries name a different run/arm than the header")); @@ -145,7 +145,8 @@ pub fn run(a: Args, json: bool) -> anyhow::Result { ); return Ok(1); } - let entry = append_ledger(&ledger, &sr).map_err(|e| anyhow::anyhow!("{}: {e}", ledger.display()))?; + let entry = + append_ledger(&ledger, &sr).map_err(|e| anyhow::anyhow!("{}: {e}", ledger.display()))?; let out = AppendOut { seq: entry.seq, hash: entry.hash.clone(), @@ -158,7 +159,11 @@ pub fn run(a: Args, json: bool) -> anyhow::Result { } else { print_lines(&[ status("appended", "ok", &format!("seq={} hash={}", out.seq, out.hash)), - status("receipt", "ok", &format!("digest {} (key {})", short(&out.receipt_digest), short(&sr.pubkey))), + status( + "receipt", + "ok", + &format!("digest {} (key {})", short(&out.receipt_digest), short(&sr.pubkey)), + ), ]); } Ok(0) diff --git a/crates/lictor-cli/src/cmd/replay.rs b/crates/lictor-cli/src/cmd/replay.rs index 3735d15..56a2614 100644 --- a/crates/lictor-cli/src/cmd/replay.rs +++ b/crates/lictor-cli/src/cmd/replay.rs @@ -139,8 +139,13 @@ fn one_pass(inp: &ReplayInput<'_>) -> anyhow::Result { Response::Error(e) => { pass.errors += 1; if pass.first_error.is_none() { - pass.first_error = - Some(format!("id={} code={} fatal={}: {}", e.id, e.code, e.fatal, sanitize(&e.message))); + pass.first_error = Some(format!( + "id={} code={} fatal={}: {}", + e.id, + e.code, + e.fatal, + sanitize(&e.message) + )); } } Response::HelloOk(_) | Response::EpisodeOk(_) | Response::ByeOk { .. } => {} @@ -185,9 +190,15 @@ pub fn replay(inp: &ReplayInput<'_>, repeat: u32) -> anyhow::Result, repeat: u32) -> anyhow::Result Vec { let mut lines = vec![ format!(" ticks={} repeats={}", out.ticks, out.repeats), - format!(" replays {}/{} byte-identical verdict_chain={}", out.identical, out.repeats, out.verdict_chain), + format!( + " replays {}/{} byte-identical verdict_chain={}", + out.identical, out.repeats, out.verdict_chain + ), " timing chain head varies (by design -- wall-clock is not replayed)".to_string(), ]; let mut states = String::from(" states"); diff --git a/crates/lictor-cli/src/cmd/verify.rs b/crates/lictor-cli/src/cmd/verify.rs index 0ab8819..aa12c03 100644 --- a/crates/lictor-cli/src/cmd/verify.rs +++ b/crates/lictor-cli/src/cmd/verify.rs @@ -15,7 +15,8 @@ use std::path::{Path, PathBuf}; use lictor_canon::{canon, sha256_hex, sha256_jcs}; use lictor_receipt::{ read_ledger, verify, verify_curve, verify_ledger, verify_ticks_file, verify_timing_chain, SignedCurve, - SignedReceipt, TickEvent, TimingEvent, VerdictCounts, LEDGER_SCHEMA, TEST_PUBKEYS, TICKS_SCHEMA, ZERO_HASH, + SignedReceipt, TickEvent, TimingEvent, VerdictCounts, LEDGER_SCHEMA, TEST_PUBKEYS, TICKS_SCHEMA, + ZERO_HASH, }; use crate::render::{glossary, json_out, kv, notes, print_lines, sanitize, short, status}; @@ -96,12 +97,18 @@ impl Outcome { } /// The check named `name`, if it ran. + #[allow(dead_code)] // kept for the receipt-check API; unused by the CLI path (CI builds with -D warnings) pub fn check(&self, name: &str) -> Option<&Check> { self.checks.iter().find(|c| c.name == name) } fn push(&mut self, name: &str, ok: bool, st: &str, detail: &str) { - self.checks.push(Check { name: name.to_string(), ok, status: st.to_string(), detail: detail.to_string() }); + self.checks.push(Check { + name: name.to_string(), + ok, + status: st.to_string(), + detail: detail.to_string(), + }); self.lines.push(status(name, st, detail)); } } @@ -278,7 +285,11 @@ fn check_ledger(sr: &SignedReceipt, p: &Path) -> Result<(String, String), (Strin if entry.prev != prev { return Err(( "FAIL".to_string(), - format!("ledger entry prev={} but the receipt binds ledger_prev={}", short(&entry.prev), short(&prev)), + format!( + "ledger entry prev={} but the receipt binds ledger_prev={}", + short(&entry.prev), + short(&prev) + ), )); } Ok((format!("seq={} head={} ({} entries)", entry.seq, short(&rep.head), rep.episodes), rep.head)) @@ -296,7 +307,10 @@ fn check_curve(sr: &SignedReceipt, p: &Path, ledger_head: Option<&str>) -> Check if sc.body.run_id != run.run_id || sc.body.arm_id != run.arm_id { return Err(( "FAIL".to_string(), - format!("curve is for {}/{}, receipt is {}/{}", sc.body.run_id, sc.body.arm_id, run.run_id, run.arm_id), + format!( + "curve is for {}/{}, receipt is {}/{}", + sc.body.run_id, sc.body.arm_id, run.run_id, run.arm_id + ), )); } match ledger_head { @@ -308,12 +322,15 @@ fn check_curve(sr: &SignedReceipt, p: &Path, ledger_head: Option<&str>) -> Check )), Some(h) => Err(( "FAIL".to_string(), - format!("curve ledger_head={} but the ledger's head is {}", short(&sc.body.ledger_head), short(h)), - )), - None => Ok(format!( - "signature ok; ledger_head={} unchecked (pass --ledger)", - short(&sc.body.ledger_head) + format!( + "curve ledger_head={} but the ledger's head is {}", + short(&sc.body.ledger_head), + short(h) + ), )), + None => { + Ok(format!("signature ok; ledger_head={} unchecked (pass --ledger)", short(&sc.body.ledger_head))) + } } } @@ -369,7 +386,9 @@ fn check_calibration(sr: &SignedReceipt, p: &Path) -> CheckResult { } match sr.body.calibration_digest.as_deref() { Some(d) if d == recomputed => {} - Some(d) => bad.push(format!("receipt calibration_digest {} != file {}", short(d), short(&recomputed))), + Some(d) => { + bad.push(format!("receipt calibration_digest {} != file {}", short(d), short(&recomputed))) + } None => bad.push("receipt carries no calibration_digest".to_string()), } if !bad.is_empty() { @@ -438,7 +457,12 @@ pub fn run_checks(o: &Opts) -> Outcome { out.push("pubkey", false, "MISMATCH", &format!(" {detail}")); } } - out.push("body digest", rep.digest_ok, if rep.digest_ok { "ok" } else { "FAIL" }, &short(&sr.body_digest)); + out.push( + "body digest", + rep.digest_ok, + if rep.digest_ok { "ok" } else { "FAIL" }, + &short(&sr.body_digest), + ); let emb = out.embodiment_digest.as_deref().map(short).unwrap_or_else(|| "n/a".to_string()); out.push( "envelope", @@ -525,7 +549,10 @@ pub fn run_checks(o: &Opts) -> Outcome { (Some(r), Some(rev)) => format!("{r}@{}", short(rev)), (Some(r), None) => r.clone(), (None, _) => { - format!("weights={}", short(b.run.policy.get("weights_sha256").map(String::as_str).unwrap_or(""))) + format!( + "weights={}", + short(b.run.policy.get("weights_sha256").map(String::as_str).unwrap_or("")) + ) } }; out.lines.push(kv( @@ -569,7 +596,8 @@ pub fn run_checks(o: &Opts) -> Outcome { pub fn run(a: Args, json: bool) -> anyhow::Result { let Args { receipt, ticks, timing, ledger, curve, calibration, pubkey } = a; - let o = Opts { receipt, ticks, timing, ledger, curve, calibration, pubkey: pubkey.map(|p| p.to_lowercase()) }; + let o = + Opts { receipt, ticks, timing, ledger, curve, calibration, pubkey: pubkey.map(|p| p.to_lowercase()) }; let out = run_checks(&o); if json { json_out(&out)?; diff --git a/crates/lictor-cli/src/render.rs b/crates/lictor-cli/src/render.rs index 2aba368..f0b16ef 100644 --- a/crates/lictor-cli/src/render.rs +++ b/crates/lictor-cli/src/render.rs @@ -193,6 +193,9 @@ mod tests { zeta: u32, alpha: u32, } - assert_eq!(pretty_sorted(&S { zeta: 1, alpha: 2 }).unwrap(), "{\n \"alpha\": 2,\n \"zeta\": 1\n}\n"); + assert_eq!( + pretty_sorted(&S { zeta: 1, alpha: 2 }).unwrap(), + "{\n \"alpha\": 2,\n \"zeta\": 1\n}\n" + ); } } diff --git a/crates/lictor-receipt/tests/fixtures/receipt/key.hex b/crates/lictor-receipt/tests/fixtures/receipt/key.hex new file mode 100644 index 0000000..203ea60 --- /dev/null +++ b/crates/lictor-receipt/tests/fixtures/receipt/key.hex @@ -0,0 +1,3 @@ +# lictor TEST signing key (seed). Committed on purpose; never use outside tests. +# Its public key is TEST_PUBKEYS[0] in lictor-receipt/src/sign.rs and `lictor verify` warns on it. +000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f diff --git a/crates/lictor-receipt/tests/fixtures/receipt/operator.hex b/crates/lictor-receipt/tests/fixtures/receipt/operator.hex new file mode 100644 index 0000000..33c7fec --- /dev/null +++ b/crates/lictor-receipt/tests/fixtures/receipt/operator.hex @@ -0,0 +1,2 @@ +# lictor TEST operator key (seed). Committed on purpose; never use outside tests. +202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f