Download cli/tests/cli.rs from Jessedbrown/asolaria-aether: direct link, hf CLI and curl.
- Browser
- Download file 37.7 kB
-
https://huggingface.co/Jessedbrown/asolaria-aether/resolve/main/cli/tests/cli.rs
- Command line
-
hf download hf://Jessedbrown/asolaria-aether/cli/tests/cli.rs
-
curl -L -o cli.rs https://huggingface.co/Jessedbrown/asolaria-aether/resolve/main/cli/tests/cli.rs
37.7 kB
| //! End-to-end tests: every case runs the real `asolaria` binary. | |
| use std::io::Write; | |
| use std::path::{Path, PathBuf}; | |
| use std::process::{Command, Output, Stdio}; | |
| use std::sync::atomic::{AtomicU32, Ordering}; | |
| const BIN: &str = env!("CARGO_BIN_EXE_asolaria"); | |
| const EXAMPLE: &str = include_str!("../examples/envelope.hbp"); | |
| /// y = 2 with every other byte zero has no curve point (it fails Edwards decompression). | |
| const NON_POINT_KEY: &str = "0200000000000000000000000000000000000000000000000000000000000000"; | |
| /// A unique, self-removing directory under `std::env::temp_dir()`. | |
| struct TempDir(PathBuf); | |
| impl TempDir { | |
| fn new(label: &str) -> TempDir { | |
| static COUNTER: AtomicU32 = AtomicU32::new(0); | |
| let nanos = std::time::SystemTime::now() | |
| .duration_since(std::time::UNIX_EPOCH) | |
| .map(|d| d.as_nanos()) | |
| .unwrap_or(0); | |
| let path = std::env::temp_dir().join(format!( | |
| "asolaria-cli-{label}-{}-{nanos}-{}", | |
| std::process::id(), | |
| COUNTER.fetch_add(1, Ordering::Relaxed) | |
| )); | |
| std::fs::create_dir(&path).expect("create temp dir"); | |
| TempDir(path) | |
| } | |
| fn file(&self, name: &str, contents: &[u8]) -> PathBuf { | |
| let path = self.0.join(name); | |
| std::fs::write(&path, contents).expect("write temp file"); | |
| path | |
| } | |
| fn path(&self, name: &str) -> PathBuf { | |
| self.0.join(name) | |
| } | |
| } | |
| impl Drop for TempDir { | |
| fn drop(&mut self) { | |
| let _ = std::fs::remove_dir_all(&self.0); | |
| } | |
| } | |
| fn run(args: &[&str]) -> Output { | |
| Command::new(BIN).args(args).output().expect("run asolaria") | |
| } | |
| fn run_stdin(args: &[&str], input: &[u8]) -> Output { | |
| let mut child = Command::new(BIN) | |
| .args(args) | |
| .stdin(Stdio::piped()) | |
| .stdout(Stdio::piped()) | |
| .stderr(Stdio::piped()) | |
| .spawn() | |
| .expect("spawn asolaria"); | |
| child | |
| .stdin | |
| .take() | |
| .expect("stdin") | |
| .write_all(input) | |
| .expect("write stdin"); | |
| child.wait_with_output().expect("wait asolaria") | |
| } | |
| fn stdout(out: &Output) -> String { | |
| String::from_utf8(out.stdout.clone()).expect("utf8 stdout") | |
| } | |
| fn code(out: &Output) -> i32 { | |
| out.status.code().expect("exit code") | |
| } | |
| fn p(path: &Path) -> &str { | |
| path.to_str().expect("utf8 path") | |
| } | |
| /// Value of `key` in a single HBP row. | |
| fn field(row: &str, key: &str) -> String { | |
| row.trim_end() | |
| .split('|') | |
| .find_map(|part| part.strip_prefix(&format!("{key}="))) | |
| .unwrap_or_else(|| panic!("{key} missing from {row}")) | |
| .to_string() | |
| } | |
| struct Keys { | |
| dir: TempDir, | |
| seed: PathBuf, | |
| public: String, | |
| } | |
| fn keygen(label: &str) -> Keys { | |
| let dir = TempDir::new(label); | |
| let seed = dir.path("seed.hex"); | |
| let out = run(&["keygen", "--out", p(&seed)]); | |
| assert_eq!(code(&out), 0, "{out:?}"); | |
| let public = field(&stdout(&out), "public"); | |
| Keys { dir, seed, public } | |
| } | |
| fn sign_file(keys: &Keys, file: &Path) -> String { | |
| let out = run(&["sign", "--key", p(&keys.seed), p(file)]); | |
| assert_eq!(code(&out), 0, "{out:?}"); | |
| field(&stdout(&out), "sig") | |
| } | |
| fn flip_hex_bit(hex: &str, index: usize) -> String { | |
| let mut chars: Vec<char> = hex.chars().collect(); | |
| let nibble = chars[index].to_digit(16).expect("hex") ^ 1; | |
| chars[index] = char::from_digit(nibble, 16).expect("nibble"); | |
| chars.into_iter().collect() | |
| } | |
| fn gate_text(text: &str, extra: &[&str]) -> Output { | |
| let dir = TempDir::new("gate"); | |
| let env = dir.file("envelope.hbp", text.as_bytes()); | |
| let mut args = vec!["gate", p(&env)]; | |
| args.extend_from_slice(extra); | |
| run(&args) | |
| } | |
| fn keygen_pubkey_sign_verify_roundtrip() { | |
| let keys = keygen("roundtrip"); | |
| assert_eq!(keys.public.len(), 64); | |
| let pubkey = run(&["pubkey", "--key", p(&keys.seed)]); | |
| assert_eq!(code(&pubkey), 0); | |
| assert_eq!(stdout(&pubkey), format!("PUBKEY|public={}|json=0\n", keys.public)); | |
| let message = keys.dir.file("msg.bin", b"exact bytes\r\n\0 with a trailing NUL"); | |
| let signed = run(&["sign", "--key", p(&keys.seed), p(&message)]); | |
| let row = stdout(&signed); | |
| assert!(row.starts_with("SIGN|sig="), "{row}"); | |
| assert_eq!(field(&row, "public"), keys.public); | |
| assert_eq!(field(&row, "bytes"), "34"); | |
| assert_eq!(field(&row, "json"), "0"); | |
| let sig = field(&row, "sig"); | |
| assert_eq!(sig.len(), 128); | |
| let verified = run(&["verify", "--pub", &keys.public, "--sig", &sig, p(&message)]); | |
| assert_eq!(code(&verified), 0); | |
| assert_eq!(stdout(&verified), "VERIFY|ok=1|json=0\n"); | |
| // stdin path signs and verifies the same bytes. | |
| let from_stdin = run_stdin( | |
| &["sign", "--key", p(&keys.seed), "-"], | |
| b"exact bytes\r\n\0 with a trailing NUL", | |
| ); | |
| assert_eq!(field(&stdout(&from_stdin), "sig"), sig, "Ed25519 is deterministic"); | |
| let verified_stdin = run_stdin( | |
| &["verify", "--pub", &keys.public, "--sig", &sig, "-"], | |
| b"exact bytes\r\n\0 with a trailing NUL", | |
| ); | |
| assert_eq!(code(&verified_stdin), 0); | |
| } | |
| fn seed_never_appears_on_stdout_or_stderr() { | |
| let keys = keygen("secret"); | |
| let seed_text = std::fs::read_to_string(&keys.seed).expect("seed"); | |
| assert!(seed_text.ends_with('\n')); | |
| let seed_hex = seed_text.trim_end(); | |
| assert_eq!(seed_hex.len(), 64); | |
| assert!(seed_hex.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())); | |
| assert_ne!(seed_hex, keys.public); | |
| let message = keys.dir.file("msg.txt", b"hello"); | |
| let other = keys.dir.path("seed2.hex"); | |
| for out in [ | |
| run(&["sign", "--key", p(&keys.seed), p(&message)]), | |
| run(&["pubkey", "--key", p(&keys.seed)]), | |
| run(&["keygen", "--out", p(&other)]), | |
| ] { | |
| let all = format!( | |
| "{}{}", | |
| String::from_utf8_lossy(&out.stdout), | |
| String::from_utf8_lossy(&out.stderr) | |
| ); | |
| assert!(!all.contains(seed_hex), "seed leaked: {all}"); | |
| } | |
| let other_seed = std::fs::read_to_string(&other).expect("seed2"); | |
| let keygen_again = run(&["keygen", "--out", p(&keys.dir.path("seed3.hex"))]); | |
| assert!(!stdout(&keygen_again).contains(other_seed.trim_end())); | |
| let third = std::fs::read_to_string(keys.dir.path("seed3.hex")).expect("seed3"); | |
| assert!(!stdout(&keygen_again).contains(third.trim_end())); | |
| assert_ne!(other_seed, third, "fresh randomness per keygen"); | |
| } | |
| fn keygen_refuses_to_overwrite() { | |
| let keys = keygen("overwrite"); | |
| let before = std::fs::read(&keys.seed).expect("seed"); | |
| let again = run(&["keygen", "--out", p(&keys.seed)]); | |
| assert_eq!(code(&again), 2, "{again:?}"); | |
| assert!(again.stdout.is_empty()); | |
| assert_eq!(std::fs::read(&keys.seed).expect("seed"), before); | |
| let unrelated = keys.dir.file("notes.txt", b"keep me"); | |
| assert_eq!(code(&run(&["keygen", "--out", p(&unrelated)])), 2); | |
| assert_eq!(std::fs::read(&unrelated).expect("notes"), b"keep me"); | |
| } | |
| fn seed_file_mode_is_0600() { | |
| use std::os::unix::fs::PermissionsExt; | |
| let keys = keygen("mode"); | |
| let mode = std::fs::metadata(&keys.seed).expect("meta").permissions().mode(); | |
| assert_eq!(mode & 0o777, 0o600, "mode {mode:o}"); | |
| } | |
| fn verify_rejects_tamper_wrong_key_flipped_bit_and_non_point() { | |
| let keys = keygen("verify"); | |
| let message = keys.dir.file("msg.txt", b"pay 10"); | |
| let sig = sign_file(&keys, &message); | |
| let tampered = keys.dir.file("tampered.txt", b"pay 99"); | |
| let out = run(&["verify", "--pub", &keys.public, "--sig", &sig, p(&tampered)]); | |
| assert_eq!(code(&out), 1); | |
| assert_eq!(stdout(&out), "VERIFY|ok=0|reason=signature_invalid|json=0\n"); | |
| let other = keygen("verify-other"); | |
| let out = run(&["verify", "--pub", &other.public, "--sig", &sig, p(&message)]); | |
| assert_eq!(code(&out), 1); | |
| assert_eq!(field(&stdout(&out), "reason"), "signature_invalid"); | |
| for index in [0, 70, 127] { | |
| let flipped = flip_hex_bit(&sig, index); | |
| let out = run(&["verify", "--pub", &keys.public, "--sig", &flipped, p(&message)]); | |
| assert_eq!(code(&out), 1, "nibble {index}"); | |
| assert_eq!(field(&stdout(&out), "ok"), "0"); | |
| } | |
| let out = run(&["verify", "--pub", NON_POINT_KEY, "--sig", &sig, p(&message)]); | |
| assert_eq!(code(&out), 1); | |
| assert_eq!(stdout(&out), "VERIFY|ok=0|reason=key_malformed|json=0\n"); | |
| } | |
| fn malformed_hex_lengths_and_arguments_exit_2() { | |
| let keys = keygen("usage"); | |
| let message = keys.dir.file("msg.txt", b"m"); | |
| let sig = sign_file(&keys, &message); | |
| let cases: Vec<Vec<String>> = vec![ | |
| vec!["verify".into(), "--pub".into(), keys.public[..62].into(), "--sig".into(), sig.clone(), p(&message).into()], | |
| vec!["verify".into(), "--pub".into(), format!("{}0", keys.public), "--sig".into(), sig.clone(), p(&message).into()], | |
| vec!["verify".into(), "--pub".into(), format!("zz{}", &keys.public[2..]), "--sig".into(), sig.clone(), p(&message).into()], | |
| vec!["verify".into(), "--pub".into(), keys.public.clone(), "--sig".into(), sig[..126].into(), p(&message).into()], | |
| vec!["verify".into(), "--pub".into(), keys.public.clone(), "--sig".into(), format!("{}g", &sig[..127]), p(&message).into()], | |
| vec!["verify".into(), "--pub".into(), keys.public.clone(), p(&message).into()], | |
| vec!["verify".into(), "--pub".into(), keys.public.clone(), "--pub".into(), keys.public.clone(), "--sig".into(), sig.clone(), p(&message).into()], | |
| vec!["sign".into(), "--key".into(), p(&keys.seed).into()], | |
| vec!["sign".into(), "--bogus".into(), "x".into(), p(&message).into()], | |
| vec!["keygen".into()], | |
| vec!["frobnicate".into()], | |
| ]; | |
| for args in cases { | |
| let refs: Vec<&str> = args.iter().map(String::as_str).collect(); | |
| let out = run(&refs); | |
| assert_eq!(code(&out), 2, "{args:?}: {out:?}"); | |
| assert!(out.stdout.is_empty(), "{args:?}"); | |
| let err = String::from_utf8_lossy(&out.stderr); | |
| assert!(err.starts_with("ERROR|kind=usage|"), "{err}"); | |
| assert!(err.trim_end().ends_with("|json=0"), "{err}"); | |
| } | |
| let seed_text = std::fs::read_to_string(&keys.seed).expect("seed"); | |
| let hex = seed_text.trim_end(); | |
| let bad_seeds: Vec<Vec<u8>> = vec![ | |
| hex[..62].as_bytes().to_vec(), | |
| format!("{hex}00\n").into_bytes(), | |
| format!("{hex}\n\n").into_bytes(), | |
| format!(" {hex}").into_bytes(), | |
| format!("{}zz\n", &hex[..62]).into_bytes(), | |
| Vec::new(), | |
| ]; | |
| for (i, bad) in bad_seeds.iter().enumerate() { | |
| let path = keys.dir.file(&format!("bad{i}.hex"), bad); | |
| let out = run(&["pubkey", "--key", p(&path)]); | |
| assert_eq!(code(&out), 2, "bad seed {i}"); | |
| let err = String::from_utf8_lossy(&out.stderr).to_string(); | |
| assert!(!err.contains(&hex[..16]), "error echoed seed material"); | |
| } | |
| } | |
| fn missing_files_are_io_errors() { | |
| let dir = TempDir::new("io"); | |
| let out = run(&["pubkey", "--key", p(&dir.path("absent.hex"))]); | |
| assert_eq!(code(&out), 1); | |
| assert!(String::from_utf8_lossy(&out.stderr).starts_with("ERROR|kind=io|")); | |
| let out = run(&["gate", p(&dir.path("absent.hbp"))]); | |
| assert_eq!(code(&out), 1); | |
| } | |
| fn gate_clean_example_proceeds_unchecked() { | |
| let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/envelope.hbp"); | |
| let out = run(&["gate", p(&example)]); | |
| assert_eq!(code(&out), 0, "{out:?}"); | |
| let row = stdout(&out); | |
| assert!(row.starts_with("GATE|verdict=PROCEED|hookwall=PROCEED|spawn_gate=PROCEED|tier_gate=PROCEED|fischer=PROCEED|input_guard=PROCEED|signature=unchecked|reason="), "{row}"); | |
| assert!(row.ends_with("|json=0\n"), "{row}"); | |
| assert_eq!(row.matches('\n').count(), 1); | |
| assert!(!row.contains('{')); | |
| let piped = run_stdin(&["gate", "-"], EXAMPLE.as_bytes()); | |
| assert_eq!(stdout(&piped), row); | |
| } | |
| fn gate_with_valid_signature_is_verified_and_same_verdict() { | |
| let keys = keygen("gate-valid"); | |
| let env = keys.dir.file("envelope.hbp", EXAMPLE.as_bytes()); | |
| let sig = sign_file(&keys, &env); | |
| let unchecked = run(&["gate", p(&env)]); | |
| let out = run(&["gate", p(&env), "--pub", &keys.public, "--sig", &sig]); | |
| assert_eq!(code(&out), 0, "{out:?}"); | |
| let row = stdout(&out); | |
| assert_eq!(field(&row, "signature"), "verified"); | |
| assert_eq!(field(&row, "verdict"), "PROCEED"); | |
| assert_eq!( | |
| row.replace("signature=verified", "signature=unchecked"), | |
| stdout(&unchecked) | |
| ); | |
| } | |
| fn gate_with_forged_signature_blocks_even_a_clean_envelope() { | |
| let keys = keygen("gate-forged"); | |
| let env = keys.dir.file("envelope.hbp", EXAMPLE.as_bytes()); | |
| let sig = sign_file(&keys, &env); | |
| let other = keygen("gate-forger"); | |
| let forged_cases = [ | |
| (keys.public.clone(), flip_hex_bit(&sig, 5)), | |
| (other.public.clone(), sig.clone()), | |
| (NON_POINT_KEY.to_string(), sig.clone()), | |
| ]; | |
| for (public, signature) in forged_cases { | |
| let out = run(&["gate", p(&env), "--pub", &public, "--sig", &signature]); | |
| assert_eq!(code(&out), 4, "{out:?}"); | |
| let row = stdout(&out); | |
| assert_eq!(field(&row, "verdict"), "BLOCK"); | |
| assert_eq!(field(&row, "signature"), "invalid"); | |
| for gate in ["hookwall", "spawn_gate", "tier_gate", "fischer", "input_guard"] { | |
| assert_eq!(field(&row, gate), "PROCEED", "{gate}: {row}"); | |
| } | |
| assert!(field(&row, "reason").starts_with("BLOCK by signature"), "{row}"); | |
| } | |
| // A valid signature over different bytes (one appended comment) does not carry over. | |
| let edited = keys.dir.file("edited.hbp", format!("{EXAMPLE}# appended\n").as_bytes()); | |
| let out = run(&["gate", p(&edited), "--pub", &keys.public, "--sig", &sig]); | |
| assert_eq!(code(&out), 4); | |
| // --pub without --sig is a usage error, not a silent unchecked run. | |
| let out = run(&["gate", p(&env), "--pub", &keys.public]); | |
| assert_eq!(code(&out), 2); | |
| } | |
| fn gate_blocks_what_the_composition_blocks() { | |
| let cases = [ | |
| ("verb=tick", "verb=self_authorize", "fischer"), | |
| ("pid=BH.VERDICT.EXAMPLE000000001\n", "", "fischer"), | |
| ("pid=BH.VERDICT.EXAMPLE000000001", "pid=UNKNOWN", "input_guard"), | |
| ("target_tier=public", "target_tier=secret", "tier_gate"), | |
| ("hook_tier=micro", "hook_tier=firmware", "hookwall"), | |
| ("slot=0", "slot=64", "hookwall"), | |
| ("event_tags=safe-tag", "event_tags=safe-tag,DRIFT", "input_guard"), | |
| ("g4_state=UNKNOWN", "g4_state=converged", "input_guard"), | |
| ]; | |
| for (from, to, deciding) in cases { | |
| assert!(EXAMPLE.contains(from), "{from}"); | |
| let out = gate_text(&EXAMPLE.replacen(from, to, 1), &[]); | |
| assert_eq!(code(&out), 4, "{to}: {out:?}"); | |
| let row = stdout(&out); | |
| assert_eq!(field(&row, "verdict"), "BLOCK", "{to}"); | |
| assert_eq!(field(&row, deciding), "BLOCK", "{to}: {row}"); | |
| } | |
| } | |
| fn gate_hold_path_exits_3() { | |
| let unsealed_write = EXAMPLE | |
| .replacen("hbp_path=ledger/example.hbp\n", "", 1) | |
| .replacen("verb=tick", "verb=write", 1); | |
| for text in [ | |
| EXAMPLE.replacen("hook_tier=micro", "hook_tier=cosign", 1), | |
| EXAMPLE.replacen("forward_score_q=800", "forward_score_q=100", 1), | |
| unsealed_write, | |
| ] { | |
| let out = gate_text(&text, &[]); | |
| assert_eq!(code(&out), 3, "{out:?}"); | |
| assert_eq!(field(&stdout(&out), "verdict"), "HOLD"); | |
| } | |
| } | |
| fn gate_rejects_unknown_duplicate_missing_and_malformed_keys() { | |
| let cases = [ | |
| format!("{EXAMPLE}colour=blue\n"), | |
| format!("{EXAMPLE}slot=0\n"), | |
| EXAMPLE.replacen("hook_tier=micro\n", "", 1), | |
| EXAMPLE.replacen("caller_pid=43981", "caller_pid=-1", 1), | |
| EXAMPLE.replacen("reverse_risk_q=100", "reverse_risk_q=1001", 1), | |
| EXAMPLE.replacen("l0_real=1", "l0_real=yes", 1), | |
| EXAMPLE.replacen("caller_tier=public", "caller_tier=Public", 1), | |
| format!("{EXAMPLE}just some text\n"), | |
| ]; | |
| for text in cases { | |
| let out = gate_text(&text, &[]); | |
| assert_eq!(code(&out), 2, "{out:?}"); | |
| assert!(out.stdout.is_empty()); | |
| assert!(String::from_utf8_lossy(&out.stderr).starts_with("ERROR|kind=usage|reason=envelope")); | |
| } | |
| let out = run_stdin(&["gate", "-"], &[0xff, 0xfe, b'\n']); | |
| assert_eq!(code(&out), 2); | |
| } | |
| fn gate_row_has_fixed_fields_for_every_verdict() { | |
| for (text, verdict) in [ | |
| (EXAMPLE.to_string(), "PROCEED"), | |
| (EXAMPLE.replacen("hook_tier=micro", "hook_tier=cosign", 1), "HOLD"), | |
| (EXAMPLE.replacen("verb=tick", "verb=bad|verb", 1), "BLOCK"), | |
| ] { | |
| let row = stdout(&gate_text(&text, &[])); | |
| assert_eq!(row.matches('\n').count(), 1, "{row}"); | |
| let keys: Vec<&str> = row | |
| .trim_end() | |
| .split('|') | |
| .map(|part| part.split('=').next().unwrap_or("")) | |
| .collect(); | |
| assert_eq!( | |
| keys, | |
| ["GATE", "verdict", "hookwall", "spawn_gate", "tier_gate", "fischer", "input_guard", "signature", "reason", "json"], | |
| "{row}" | |
| ); | |
| assert_eq!(field(&row, "verdict"), verdict); | |
| } | |
| } | |
| fn help_and_version() { | |
| for args in [&[][..], &["help"][..], &["--help"][..]] { | |
| let out = run(args); | |
| assert_eq!(code(&out), 0); | |
| let text = stdout(&out); | |
| for word in ["keygen", "pubkey", "sign", "verify", "gate", "observe", "EXIT CODES", "SCOPE"] { | |
| assert!(text.contains(word), "{word}"); | |
| } | |
| } | |
| let out = run(&["--version"]); | |
| assert_eq!(stdout(&out), format!("VERSION|name=asolaria|version={}|json=0\n", env!("CARGO_PKG_VERSION"))); | |
| } | |
| /// Runs with stdout's read end closed before the binary can write (it first waits on stdin). | |
| fn run_with_broken_stdout(args: &[&str], input: &[u8]) -> Output { | |
| let mut child = Command::new(BIN) | |
| .args(args) | |
| .stdin(Stdio::piped()) | |
| .stdout(Stdio::piped()) | |
| .stderr(Stdio::piped()) | |
| .spawn() | |
| .expect("spawn asolaria"); | |
| drop(child.stdout.take()); | |
| let mut stdin = child.stdin.take().expect("stdin"); | |
| stdin.write_all(input).expect("write stdin"); | |
| drop(stdin); | |
| child.wait_with_output().expect("wait asolaria") | |
| } | |
| fn lost_output_never_exits_0_or_panics_and_block_stays_4() { | |
| let block = EXAMPLE.replacen("verb=tick", "verb=self_authorize", 1); | |
| for (text, expected) in [(block.as_str(), 4), (EXAMPLE, 1)] { | |
| let out = run_with_broken_stdout(&["gate", "-"], text.as_bytes()); | |
| assert_eq!(code(&out), expected, "{out:?}"); | |
| let err = String::from_utf8_lossy(&out.stderr); | |
| assert!(err.starts_with("ERROR|kind=io|reason=writing output"), "{err}"); | |
| assert!(!err.contains("panicked"), "{err}"); | |
| } | |
| } | |
| fn full_disk_stdout_keeps_verdict_code_and_removes_unannounced_seed() { | |
| let full = || std::fs::OpenOptions::new().write(true).open("/dev/full").expect("/dev/full"); | |
| let dir = TempDir::new("devfull"); | |
| let block = dir.file("block.hbp", EXAMPLE.replacen("pid=BH.VERDICT.EXAMPLE000000001\n", "", 1).as_bytes()); | |
| let out = Command::new(BIN).args(["gate", p(&block)]).stdout(full()).output().expect("run"); | |
| assert_eq!(code(&out), 4, "{out:?}"); | |
| let seed = dir.path("lost.seed"); | |
| let out = Command::new(BIN).args(["keygen", "--out", p(&seed)]).stdout(full()).output().expect("run"); | |
| assert_eq!(code(&out), 1, "{out:?}"); | |
| assert!(String::from_utf8_lossy(&out.stderr).contains("seed file removed")); | |
| assert!(!seed.exists(), "a seed whose public key was never shown must not be left behind"); | |
| let out = Command::new(BIN).arg("--version").stdout(full()).output().expect("run"); | |
| assert_eq!(code(&out), 1); | |
| let out = Command::new(BIN).args(["verify", "--pub", "00", "--sig", "00", "x"]).stderr(full()).output().expect("run"); | |
| assert_eq!(code(&out), 2, "a lost ERROR row keeps the usage exit code"); | |
| } | |
| fn control_and_bidi_characters_in_envelopes_exit_2_even_when_signed() { | |
| let keys = keygen("spoof"); | |
| for (from, to) in [ | |
| ("target=fabric", "target=prod\u{1b}[2K\rtarget=fabric"), | |
| ("payload=heartbeat", "payload=rm -rf /\rpayload=heartbeat"), | |
| ("actor=helm", "actor=he\0lm"), | |
| ("actor=helm", "actor=\u{202E}mleh"), | |
| ] { | |
| let env = keys.dir.file("spoof.hbp", EXAMPLE.replacen(from, to, 1).as_bytes()); | |
| let sig = sign_file(&keys, &env); | |
| for extra in [&[][..], &["--pub", &keys.public, "--sig", &sig][..]] { | |
| let mut args = vec!["gate", p(&env)]; | |
| args.extend_from_slice(extra); | |
| let out = run(&args); | |
| assert_eq!(code(&out), 2, "{to:?}: {out:?}"); | |
| assert!(out.stdout.is_empty()); | |
| assert!(String::from_utf8_lossy(&out.stderr).contains("control or bidi character")); | |
| } | |
| } | |
| let out = run_stdin(&["gate", "-"], format!("\u{FEFF}{EXAMPLE}").as_bytes()); | |
| assert_eq!(code(&out), 2); | |
| assert!(String::from_utf8_lossy(&out.stderr).contains("byte order mark")); | |
| } | |
| fn forged_signature_over_unparseable_envelope_is_block_4() { | |
| let keys = keygen("forged-garbage"); | |
| let env = keys.dir.file("envelope.hbp", EXAMPLE.as_bytes()); | |
| let sig = sign_file(&keys, &env); | |
| for garbage in [&b"garbage\n"[..], b"slot=x\n", &[0xff, 0xfe, b'\n']] { | |
| let bad = keys.dir.file("garbage.hbp", garbage); | |
| let out = run(&["gate", p(&bad), "--pub", &keys.public, "--sig", &sig]); | |
| assert_eq!(code(&out), 4, "{out:?}"); | |
| let row = stdout(&out); | |
| assert_eq!(field(&row, "verdict"), "BLOCK"); | |
| assert_eq!(field(&row, "signature"), "invalid"); | |
| for gate in ["hookwall", "spawn_gate", "tier_gate", "fischer", "input_guard"] { | |
| assert_eq!(field(&row, gate), "UNEVALUATED", "{row}"); | |
| } | |
| assert!(field(&row, "reason").contains("envelope unparseable"), "{row}"); | |
| // Without a signature check the same envelope is a plain parse error. | |
| assert_eq!(code(&run(&["gate", p(&bad)])), 2); | |
| } | |
| // A correctly signed but unparseable envelope stays a parse error, not a verdict. | |
| let bad = keys.dir.file("signed-garbage.hbp", b"garbage\n"); | |
| let good_sig = sign_file(&keys, &bad); | |
| assert_eq!(code(&run(&["gate", p(&bad), "--pub", &keys.public, "--sig", &good_sig])), 2); | |
| } | |
| fn oversized_envelope_is_a_usage_error() { | |
| let mut text = EXAMPLE.to_string(); | |
| text.push_str(&"#".repeat(1024 * 1024)); | |
| let out = run_stdin(&["gate", "-"], text.as_bytes()); | |
| assert_eq!(code(&out), 2, "{out:?}"); | |
| assert!(String::from_utf8_lossy(&out.stderr).contains("larger than 1048576 bytes")); | |
| } | |
| fn subcommand_help_and_help_topics() { | |
| for command in ["keygen", "pubkey", "sign", "verify", "gate", "observe"] { | |
| for args in [vec![command, "--help"], vec![command, "-h"], vec!["help", command]] { | |
| let out = run(&args); | |
| assert_eq!(code(&out), 0, "{args:?}: {out:?}"); | |
| let text = stdout(&out); | |
| assert!(text.starts_with("USAGE\n"), "{text}"); | |
| assert!(text.contains(&format!("asolaria {command}")), "{text}"); | |
| } | |
| } | |
| assert_eq!(code(&run(&["help", "frobnicate"])), 2); | |
| // `-h` as an option value is a value, not a help request. | |
| let dir = TempDir::new("help-value"); | |
| let out = run(&["pubkey", "--key", p(&dir.path("-h"))]); | |
| assert_eq!(code(&out), 1); | |
| } | |
| fn out_of_range_u8_says_maximum() { | |
| let out = gate_text(&EXAMPLE.replacen("slot=0", "slot=256", 1), &[]); | |
| assert_eq!(code(&out), 2); | |
| assert!(String::from_utf8_lossy(&out.stderr).contains("slot: 256 is above the maximum 255")); | |
| } | |
| const OBSERVE_EXAMPLE: &str = include_str!("../examples/observe-envelope.hbp"); | |
| const OBSERVE_PID: &str = "BH.VERDICT.EXAMPLE000000001"; | |
| /// The SYNTHETIC events fixture of model/l0-edge. | |
| fn fixture_events() -> PathBuf { | |
| Path::new(env!("CARGO_MANIFEST_DIR")).join("../model/l0-edge/tests/fixtures/events.hbp") | |
| } | |
| fn observe_text(text: &str, extra: &[&str]) -> Output { | |
| let dir = TempDir::new("observe"); | |
| let env = dir.file("observe.hbp", text.as_bytes()); | |
| let events = fixture_events(); | |
| let mut args = vec!["observe", "--events", p(&events), p(&env)]; | |
| args.extend_from_slice(extra); | |
| run(&args) | |
| } | |
| /// The observe example with its request edge set to `actor -> target`. | |
| fn with_edge(actor: &str, target: &str) -> String { | |
| assert!(OBSERVE_EXAMPLE.contains("\nactor=agent:benign\n")); | |
| assert!(OBSERVE_EXAMPLE.contains("\ntarget=svc:ledger\n")); | |
| OBSERVE_EXAMPLE | |
| .replacen("\nactor=agent:benign\n", &format!("\nactor={actor}\n"), 1) | |
| .replacen("\ntarget=svc:ledger\n", &format!("\ntarget={target}\n"), 1) | |
| } | |
| fn rows(out: &Output) -> Vec<String> { | |
| stdout(out).lines().map(str::to_string).collect() | |
| } | |
| fn observe_example_prints_the_row_in_the_readme() { | |
| let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/observe-envelope.hbp"); | |
| let events = fixture_events(); | |
| let out = run(&["observe", "--events", p(&events), p(&example)]); | |
| assert_eq!(code(&out), 3, "{out:?}"); | |
| let printed = rows(&out); | |
| assert_eq!(printed.len(), 1, "{printed:?}"); | |
| let readme = include_str!("../README.md"); | |
| assert!( | |
| readme.contains(printed[0].as_str()), | |
| "README lacks {}", | |
| printed[0] | |
| ); | |
| } | |
| fn observe_scores_each_fixture_request_pair_from_the_events() { | |
| let cases = [ | |
| ( | |
| "agent:benign", | |
| "svc:ledger", | |
| "GATE|verdict=HOLD|score=0.2139|l0_real=true|mark=REVERSE_GAIN_MARK_MISTAKE|reverse_risk=0.7861|", | |
| ("HOLD", 3), | |
| ), | |
| ( | |
| "agent:rogue", | |
| "svc:vault", | |
| "GATE|verdict=PROCEED|score=0.7593|l0_real=true|mark=FORWARD_GNN_MARK_GENIUS|reverse_risk=0.2407|", | |
| ("PROCEED", 0), | |
| ), | |
| ( | |
| "agent:ghost", | |
| "svc:ledger", | |
| "GATE|verdict=HOLD|score=0.4705|l0_real=false|mark=REVERSE_GAIN_MARK_MISTAKE|reverse_risk=0.5295|", | |
| ("HOLD", 3), | |
| ), | |
| ]; | |
| for (actor, target, start, (spawn, exit)) in cases { | |
| let out = observe_text(&with_edge(actor, target), &[]); | |
| assert_eq!(code(&out), exit, "{out:?}"); | |
| let printed = rows(&out); | |
| assert_eq!(printed.len(), 1, "{printed:?}"); | |
| let gate = &printed[0]; | |
| assert!(gate.starts_with(start), "{gate}"); | |
| assert_eq!(field(gate, "spawn_gate"), spawn, "{gate}"); | |
| for other in ["hookwall", "tier_gate", "fischer", "input_guard"] { | |
| assert_eq!(field(gate, other), "PROCEED", "{gate}"); | |
| } | |
| assert_eq!(field(gate, "signature"), "unchecked"); | |
| let has_gnn_flag = field(gate, "reason").contains("gnn_observable"); | |
| assert_eq!(has_gnn_flag, field(gate, "l0_real") == "true", "{gate}"); | |
| } | |
| } | |
| fn observe_rows_are_what_the_library_computes() { | |
| use asolaria_hookwall::hookwall::HookTier; | |
| use asolaria_hookwall::syscall::AccessTier; | |
| use asolaria_observed::{observe_and_compose, PolicyInputs}; | |
| use asolaria_server_fischer_eval::FischerEnvelope; | |
| let text = std::fs::read_to_string(fixture_events()).expect("events fixture"); | |
| let events = l0_edge::event::parse_events(&text).expect("events parse"); | |
| for (actor, target) in [ | |
| ("agent:benign", "svc:ledger"), | |
| ("agent:rogue", "svc:vault"), | |
| ("agent:ghost", "svc:ledger"), | |
| ("agent:benign", "agent:ghost"), | |
| ] { | |
| let policy = PolicyInputs { | |
| slot: 0, | |
| syscall_no: 1, | |
| caller_pid: 43981, | |
| hook_tier: HookTier::Micro, | |
| caller_tier: AccessTier::Public, | |
| target_tier: AccessTier::Public, | |
| event_tags: vec!["safe-tag".to_string()], | |
| fischer_envelope: FischerEnvelope { | |
| pid: Some(OBSERVE_PID.to_string()), | |
| actor: actor.to_string(), | |
| verb: "tick".to_string(), | |
| target: target.to_string(), | |
| payload: "heartbeat".to_string(), | |
| payload_json_true: false, | |
| hbp_path: Some("ledger/example.hbp".to_string()), | |
| sidecar_plan: None, | |
| ledger_path: None, | |
| tuple: None, | |
| cube_47d: None, | |
| cosign: None, | |
| halt_path: Some("/halt".to_string()), | |
| authority_jump: false, | |
| recursive_consent: false, | |
| operator_witness_required: false, | |
| operator_witness: false, | |
| }, | |
| fischer_strict: false, | |
| }; | |
| let (observed, composed) = observe_and_compose(&events, policy); | |
| let out = observe_text(&with_edge(actor, target), &[]); | |
| let printed = rows(&out); | |
| assert_eq!(printed.len(), 1, "{printed:?}"); | |
| let gate = &printed[0]; | |
| let js_number = |e4: u32| { | |
| let text = format!("{}.{:04}", e4 / 10_000, e4 % 10_000); | |
| text.trim_end_matches('0').trim_end_matches('.').to_string() | |
| }; | |
| assert_eq!(field(gate, "score"), js_number(observed.composite_e4)); | |
| assert_eq!( | |
| field(gate, "score"), | |
| observed.envelope.fischer_score.composite | |
| ); | |
| assert_eq!(field(gate, "l0_real"), observed.l0.is_some().to_string()); | |
| let reverse = &observed.reverse_gain; | |
| let mark = if reverse.promoted { | |
| "FORWARD_GNN_MARK_GENIUS" | |
| } else { | |
| "REVERSE_GAIN_MARK_MISTAKE" | |
| }; | |
| assert_eq!(field(gate, "mark"), mark); | |
| assert_eq!( | |
| field(gate, "reverse_risk"), | |
| js_number(reverse.reverse_risk_e4) | |
| ); | |
| let signals = &observed.signals; | |
| assert_eq!( | |
| (signals.l4, signals.g1, signals.g2, signals.g3), | |
| (None, None, None, None) | |
| ); | |
| assert_eq!(field(gate, "verdict"), composed.verdict.as_str()); | |
| for (name, severity) in composed.per_gate { | |
| assert_eq!(field(gate, name), severity.as_str(), "{name}"); | |
| } | |
| assert_eq!(field(gate, "reason"), composed.reason); | |
| let exit = match composed.verdict.as_str() { | |
| "PROCEED" => 0, | |
| "HOLD" => 3, | |
| _ => 4, | |
| }; | |
| assert_eq!(code(&out), exit); | |
| } | |
| } | |
| fn observe_rejects_every_score_key_with_exit_2() { | |
| let keys = keygen("observe-score-key"); | |
| for line in [ | |
| "forward_score_q=800", | |
| "reverse_risk_q=100", | |
| "composite=0.85", | |
| "l0_real=1", | |
| "shannon_q=550", | |
| "shannon_above=0", | |
| "g4_state=UNKNOWN", | |
| ] { | |
| let key = line.split('=').next().unwrap_or_default(); | |
| let env = keys.dir.file( | |
| "scored.hbp", | |
| format!("{OBSERVE_EXAMPLE}{line}\n").as_bytes(), | |
| ); | |
| let sig = sign_file(&keys, &env); | |
| let events = fixture_events(); | |
| for extra in [&[][..], &["--pub", &keys.public, "--sig", &sig][..]] { | |
| let mut args = vec!["observe", "--events", p(&events), p(&env)]; | |
| args.extend_from_slice(extra); | |
| let out = run(&args); | |
| assert_eq!(code(&out), 2, "{line}: {out:?}"); | |
| assert!(out.stdout.is_empty(), "{line}"); | |
| let err = String::from_utf8_lossy(&out.stderr); | |
| assert!( | |
| err.starts_with("ERROR|kind=usage|reason=envelope: line "), | |
| "{err}" | |
| ); | |
| assert!( | |
| err.contains(&format!("score key {key} is not accepted")), | |
| "{err}" | |
| ); | |
| } | |
| // A failed signature over a refused envelope is BLOCK, as `gate` does for any unparseable one. | |
| let forged = flip_hex_bit(&sig, 9); | |
| let mut args = vec!["observe", "--events", p(&events), p(&env)]; | |
| args.extend_from_slice(&["--pub", &keys.public, "--sig", &forged]); | |
| let out = run(&args); | |
| assert_eq!(code(&out), 4, "{line}: {out:?}"); | |
| let row = stdout(&out); | |
| assert_eq!(field(&row, "verdict"), "BLOCK"); | |
| assert_eq!(field(&row, "spawn_gate"), "UNEVALUATED"); | |
| assert!( | |
| field(&row, "reason").contains("envelope unparseable"), | |
| "{row}" | |
| ); | |
| } | |
| // The gate example carries every score key, so observe refuses it. | |
| let out = observe_text(EXAMPLE, &[]); | |
| assert_eq!(code(&out), 2, "{out:?}"); | |
| } | |
| fn observe_signature_covers_the_envelope_and_only_tightens() { | |
| let keys = keygen("observe-sig"); | |
| let env = keys.dir.file( | |
| "observe.hbp", | |
| with_edge("agent:rogue", "svc:vault").as_bytes(), | |
| ); | |
| let sig = sign_file(&keys, &env); | |
| let events = fixture_events(); | |
| let base = ["observe", "--events", p(&events), p(&env)]; | |
| let unchecked = run(&base); | |
| assert_eq!(code(&unchecked), 0, "{unchecked:?}"); | |
| let mut args = base.to_vec(); | |
| args.extend_from_slice(&["--pub", &keys.public, "--sig", &sig]); | |
| let verified = run(&args); | |
| assert_eq!(code(&verified), 0, "{verified:?}"); | |
| assert_eq!( | |
| stdout(&verified).replace("signature=verified", "signature=unchecked"), | |
| stdout(&unchecked) | |
| ); | |
| let forged = flip_hex_bit(&sig, 3); | |
| let mut args = base.to_vec(); | |
| args.extend_from_slice(&["--pub", &keys.public, "--sig", &forged]); | |
| let blocked = run(&args); | |
| assert_eq!(code(&blocked), 4, "{blocked:?}"); | |
| let (forged, clean) = (stdout(&blocked), stdout(&unchecked)); | |
| for key in ["score", "l0_real", "mark", "reverse_risk"] { | |
| assert_eq!(field(&forged, key), field(&clean, key), "{key}"); | |
| } | |
| assert_eq!(field(&forged, "verdict"), "BLOCK"); | |
| assert_eq!(field(&forged, "signature"), "invalid"); | |
| assert_eq!(field(&forged, "spawn_gate"), "PROCEED"); | |
| // The events file is not signed: a different events file keeps signature=verified. | |
| let other_events = keys.dir.file( | |
| "events.hbp", | |
| b"EVENT|actor=agent:rogue|target=svc:vault|risk=0\n", | |
| ); | |
| let out = run(&[ | |
| "observe", | |
| "--events", | |
| p(&other_events), | |
| p(&env), | |
| "--pub", | |
| &keys.public, | |
| "--sig", | |
| &sig, | |
| ]); | |
| assert_eq!(field(&stdout(&out), "signature"), "verified"); | |
| assert_ne!(field(&stdout(&out), "score"), field(&clean, "score")); | |
| } | |
| fn observe_reads_events_or_envelope_from_stdin() { | |
| let events = fixture_events(); | |
| let events_text = std::fs::read(&events).expect("events fixture"); | |
| let file_run = observe_text(OBSERVE_EXAMPLE, &[]); | |
| let dir = TempDir::new("observe-stdin"); | |
| let env = dir.file("observe.hbp", OBSERVE_EXAMPLE.as_bytes()); | |
| let events_stdin = run_stdin(&["observe", "--events", "-", p(&env)], &events_text); | |
| assert_eq!(stdout(&events_stdin), stdout(&file_run)); | |
| let envelope_stdin = run_stdin( | |
| &["observe", "--events", p(&events), "-"], | |
| OBSERVE_EXAMPLE.as_bytes(), | |
| ); | |
| assert_eq!(stdout(&envelope_stdin), stdout(&file_run)); | |
| let both = run(&["observe", "--events", "-", "-"]); | |
| assert_eq!(code(&both), 2, "{both:?}"); | |
| assert!(String::from_utf8_lossy(&both.stderr).contains("cannot both be read from stdin")); | |
| } | |
| fn observe_usage_and_io_errors() { | |
| let dir = TempDir::new("observe-errors"); | |
| let env = dir.file("observe.hbp", OBSERVE_EXAMPLE.as_bytes()); | |
| let events = fixture_events(); | |
| let bad_events = dir.file("bad-events.hbp", b"EVENT|actor=a|risk=x\n"); | |
| let no_payload = dir.file( | |
| "no-payload.hbp", | |
| OBSERVE_EXAMPLE | |
| .replace("payload=heartbeat\n", "") | |
| .as_bytes(), | |
| ); | |
| let not_utf8 = dir.file( | |
| "latin1.hbp", | |
| &[b'E', b'V', b'E', b'N', b'T', b'|', 0xff, b'\n'], | |
| ); | |
| let usage_cases: Vec<Vec<&str>> = vec![ | |
| vec!["observe", p(&env)], | |
| vec!["observe", "--events", p(&events)], | |
| vec!["observe", "--events", p(&events), p(&env), p(&env)], | |
| vec![ | |
| "observe", | |
| "--events", | |
| p(&events), | |
| "--forward_score_q", | |
| "800", | |
| p(&env), | |
| ], | |
| vec![ | |
| "observe", | |
| "--events", | |
| p(&events), | |
| "--pub", | |
| NON_POINT_KEY, | |
| p(&env), | |
| ], | |
| vec!["observe", "--events", p(&bad_events), p(&env)], | |
| vec!["observe", "--events", p(¬_utf8), p(&env)], | |
| vec!["observe", "--events", p(&events), p(&no_payload)], | |
| ]; | |
| for args in usage_cases { | |
| let out = run(&args); | |
| assert_eq!(code(&out), 2, "{args:?}: {out:?}"); | |
| assert!(out.stdout.is_empty(), "{args:?}"); | |
| assert!(String::from_utf8_lossy(&out.stderr).starts_with("ERROR|kind=usage|")); | |
| } | |
| let out = run(&["observe", "--events", p(&bad_events), p(&env)]); | |
| assert!(String::from_utf8_lossy(&out.stderr) | |
| .contains("reason=events: line 1: risk must be decimal digits")); | |
| let out = run(&["observe", "--events", p(&events), p(&no_payload)]); | |
| assert_eq!( | |
| String::from_utf8_lossy(&out.stderr), | |
| "ERROR|kind=usage|reason=envelope: missing required key payload|json=0\n" | |
| ); | |
| for args in [ | |
| vec![ | |
| "observe", | |
| "--events", | |
| p(&dir.path("absent-events.hbp")), | |
| p(&env), | |
| ], | |
| vec![ | |
| "observe", | |
| "--events", | |
| p(&events), | |
| p(&dir.path("absent.hbp")), | |
| ], | |
| ] { | |
| let out = run(&args); | |
| assert_eq!(code(&out), 1, "{args:?}: {out:?}"); | |
| assert!(String::from_utf8_lossy(&out.stderr).starts_with("ERROR|kind=io|")); | |
| } | |
| } | |