Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ venv/
Thumbs.db
.vscode/
.idea/

# local secrets
.env
.env.*
!.env.example
8 changes: 5 additions & 3 deletions crates/lictor-cli/src/cmd/ack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -60,8 +62,8 @@ pub fn resolve_handoff(arg: &str) -> anyhow::Result<(String, Option<HandoffRecor
}
let p = Path::new(arg);
let text = std::fs::read_to_string(p).map_err(|e| anyhow::anyhow!("{}: {e}", p.display()))?;
let rec: HandoffRecord =
serde_json::from_str(&text).map_err(|e| anyhow::anyhow!("{}: not a handoff record: {e}", p.display()))?;
let rec: HandoffRecord = serde_json::from_str(&text)
.map_err(|e| anyhow::anyhow!("{}: not a handoff record: {e}", p.display()))?;
let recomputed = rec.compute_digest()?;
if recomputed != rec.digest {
anyhow::bail!(
Expand Down
3 changes: 2 additions & 1 deletion crates/lictor-cli/src/cmd/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ pub fn run(a: Args, json: bool) -> anyhow::Result<i32> {
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 {
Expand Down
29 changes: 17 additions & 12 deletions crates/lictor-cli/src/cmd/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -63,22 +65,20 @@ pub fn verify_file(p: &Path) -> anyhow::Result<VerifyOut> {
let entries: Vec<LedgerEntry> = 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"));
Expand Down Expand Up @@ -145,7 +145,8 @@ pub fn run(a: Args, json: bool) -> anyhow::Result<i32> {
);
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(),
Expand All @@ -158,7 +159,11 @@ pub fn run(a: Args, json: bool) -> anyhow::Result<i32> {
} 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)
Expand Down
24 changes: 19 additions & 5 deletions crates/lictor-cli/src/cmd/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,13 @@ fn one_pass(inp: &ReplayInput<'_>) -> anyhow::Result<Pass> {
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 { .. } => {}
Expand Down Expand Up @@ -185,9 +190,15 @@ pub fn replay(inp: &ReplayInput<'_>, repeat: u32) -> anyhow::Result<ReplayOutcom
.iter()
.zip(p.chains.iter())
.find(|(a, b)| a != b)
.map(|(a, b)| Divergence { repeat: k, tick: a.0, seq: a.1, first: a.2.clone(), other: b.2.clone() })
.map(|(a, b)| Divergence {
repeat: k,
tick: a.0,
seq: a.1,
first: a.2.clone(),
other: b.2.clone(),
})
.or_else(|| {
first.chains.iter().zip(p.chains.iter()).last().map(|(a, _)| Divergence {
first.chains.iter().zip(p.chains.iter()).next_back().map(|(a, _)| Divergence {
repeat: k,
tick: a.0,
seq: a.1,
Expand All @@ -213,7 +224,10 @@ pub fn replay(inp: &ReplayInput<'_>, repeat: u32) -> anyhow::Result<ReplayOutcom
pub fn render(out: &ReplayOutcome) -> Vec<String> {
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");
Expand Down
54 changes: 41 additions & 13 deletions crates/lictor-cli/src/cmd/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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));
}
}
Expand Down Expand Up @@ -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))
Expand All @@ -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 {
Expand All @@ -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)))
}
}
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -569,7 +596,8 @@ pub fn run_checks(o: &Opts) -> Outcome {

pub fn run(a: Args, json: bool) -> anyhow::Result<i32> {
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)?;
Expand Down
5 changes: 4 additions & 1 deletion crates/lictor-cli/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
}
3 changes: 3 additions & 0 deletions crates/lictor-receipt/tests/fixtures/receipt/key.hex
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions crates/lictor-receipt/tests/fixtures/receipt/operator.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# lictor TEST operator key (seed). Committed on purpose; never use outside tests.
202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f
Loading