use aethershell::safety;
use std::path::PathBuf;
const TEST_KEY_HEX: &str = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
fn key_bytes() -> Vec<u8> {
(0..TEST_KEY_HEX.len())
.step_by(2)
.map(|i| u8::from_str_radix(&TEST_KEY_HEX[i..i + 2], 16).expect("hex"))
.collect()
}
fn tmp_log(tag: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!(
"ae_chain_{tag}_{}_{}.log",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let _ = std::fs::remove_file(&p);
p
}
fn write_entries(log: &PathBuf, n: usize) {
std::env::set_var("AETHER_AUDIT_LOG", log);
for i in 0..n {
safety::audit(
"file_write",
safety::Effect::WriteLocal,
"allow",
&format!("/w/file{i}"),
serde_json::json!({ "i": i }),
)
.expect("audit append");
}
}
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock() -> std::sync::MutexGuard<'static, ()> {
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn install_key() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
std::env::set_var("AETHER_AUDIT_KEY", TEST_KEY_HEX);
assert!(
safety::audit_key().is_some(),
"the test key did not resolve; every keyed assertion below would pass vacuously"
);
});
}
#[test]
fn an_unkeyed_rewrite_is_refused_by_a_keyed_verifier() {
let _g = lock();
let log = tmp_log("forged");
let mut prev = "0".repeat(64);
let mut lines = Vec::new();
for seq in 1..=3u64 {
let core = serde_json::json!({
"seq": seq,
"ts": "2026-08-31T00:00:00+00:00",
"principal": "attacker",
"builtin": "file_write",
"effect": "WriteLocal",
"decision": "allow",
"resource": "/w/innocuous",
"detail": {},
"prev_hash": prev,
});
let hash = {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(core.to_string().as_bytes());
h.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
};
let mut full = core;
full["entry_hash"] = serde_json::json!(hash);
lines.push(full.to_string());
prev = hash;
}
std::fs::write(&log, lines.join("\n") + "\n").expect("write forged log");
assert_eq!(
safety::verify_audit_with(&log, None),
Ok(3),
"an unkeyed chain accepts the forgery; if this ever fails the premise of \
AS-2026-02 has changed and the rest of this file needs revisiting"
);
let err = safety::verify_audit_with(&log, Some(&key_bytes()))
.expect_err("a keyed verifier must refuse an unkeyed chain");
assert!(
err.contains("unkeyed"),
"the refusal should name the downgrade, got: {err}"
);
let _ = std::fs::remove_file(&log);
}
#[test]
fn a_relabelled_forgery_still_fails_the_tag() {
let _g = lock();
let log = tmp_log("relabel");
let core = serde_json::json!({
"seq": 1,
"ts": "2026-08-31T00:00:00+00:00",
"principal": "attacker",
"builtin": "file_write",
"effect": "WriteLocal",
"decision": "allow",
"resource": "/w/x",
"detail": {},
"prev_hash": "0".repeat(64),
"mac": "hmac-sha256",
"key_id": "deadbeefdeadbeef",
});
let bogus = {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(core.to_string().as_bytes());
h.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
};
let mut full = core;
full["entry_hash"] = serde_json::json!(bogus);
std::fs::write(&log, full.to_string() + "\n").expect("write");
let err = safety::verify_audit_with(&log, Some(&key_bytes()))
.expect_err("a SHA-256 hash wearing an hmac label must not verify");
assert!(
err.contains("mismatch") || err.contains("tampered"),
"unexpected error: {err}"
);
let _ = std::fs::remove_file(&log);
}
#[test]
fn a_keyed_log_verifies_with_the_key_and_not_without_it() {
let _g = lock();
install_key();
let log = tmp_log("genuine");
write_entries(&log, 3);
let n = safety::verify_audit_with(&log, Some(&key_bytes()))
.expect("a genuine keyed log must verify with its key");
assert_eq!(n, 3);
let err = safety::verify_audit_with(&log, None)
.expect_err("an unkeyed verifier must not report a keyed log as verified");
assert!(
err.contains("no chain key is configured"),
"unexpected error: {err}"
);
let _ = std::fs::remove_file(&log);
}
#[test]
fn the_wrong_key_does_not_verify() {
let _g = lock();
install_key();
let log = tmp_log("wrongkey");
write_entries(&log, 2);
let mut wrong = key_bytes();
wrong[0] ^= 0xff;
let err = safety::verify_audit_with(&log, Some(&wrong))
.expect_err("a different key must not verify this chain");
assert!(
err.contains("mismatch") || err.contains("tampered"),
"unexpected error: {err}"
);
let _ = std::fs::remove_file(&log);
}
#[test]
fn keyed_entries_carry_the_algorithm_and_a_key_id() {
let _g = lock();
install_key();
let log = tmp_log("labels");
write_entries(&log, 1);
let content = std::fs::read_to_string(&log).expect("read");
let obj: serde_json::Value =
serde_json::from_str(content.lines().next().expect("one line")).expect("json");
assert_eq!(obj["mac"], "hmac-sha256");
assert!(
obj["key_id"].as_str().is_some_and(|s| s.len() == 16),
"entries need a key id so rotation reads as rotation rather than as tampering: {obj}"
);
let _ = std::fs::remove_file(&log);
}
#[test]
fn the_key_is_removed_from_the_environment_after_it_is_read() {
let _g = lock();
install_key();
assert!(
safety::audit_key().is_some(),
"precondition: a key is configured"
);
assert!(
std::env::var("AETHER_AUDIT_KEY").is_err(),
"AETHER_AUDIT_KEY is still in this process's environment, so every spawned \
child inherits the key that is supposed to distinguish us from them"
);
}
#[test]
fn truncation_between_appends_is_recorded_in_the_log() {
let _g = lock();
install_key();
let log = tmp_log("truncate");
write_entries(&log, 2);
std::fs::write(&log, "").expect("truncate");
write_entries(&log, 1);
let content = std::fs::read_to_string(&log).expect("read");
assert!(
content.contains("tamper-detected"),
"a truncation went unremarked; the log after the next append was:\n{content}"
);
assert!(
content.contains("audit_chain"),
"the marker should be attributable to the audit layer itself: {content}"
);
let _ = std::fs::remove_file(&log);
}
#[test]
fn every_entry_is_mirrored_to_the_sink() {
let _g = lock();
install_key();
let log = tmp_log("sinklog");
let sink = tmp_log("sink");
std::env::set_var("AETHER_AUDIT_SINK", &sink);
write_entries(&log, 3);
std::env::remove_var("AETHER_AUDIT_SINK");
let mirrored = std::fs::read_to_string(&sink).expect("sink was never written");
let lines: Vec<&str> = mirrored.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(lines.len(), 3, "sink holds {} of 3 entries", lines.len());
let primary = std::fs::read_to_string(&log).expect("read log");
assert_eq!(primary.lines().filter(|l| !l.trim().is_empty()).count(), 3);
for (a, b) in primary.lines().zip(lines.iter()) {
assert_eq!(a, *b, "sink line differs from the log line");
}
let _ = std::fs::remove_file(&log);
let _ = std::fs::remove_file(&sink);
}
#[test]
fn the_sink_still_verifies_after_the_log_is_truncated() {
let _g = lock();
install_key();
let log = tmp_log("sinktrunc");
let sink = tmp_log("sinktrunc_out");
std::env::set_var("AETHER_AUDIT_SINK", &sink);
write_entries(&log, 3);
std::env::remove_var("AETHER_AUDIT_SINK");
std::fs::write(&log, "").expect("truncate");
let n = safety::verify_audit_with(&sink, Some(&key_bytes()))
.expect("the sink should still verify as a chain");
assert_eq!(n, 3, "sink lost entries the log had");
let _ = std::fs::remove_file(&log);
let _ = std::fs::remove_file(&sink);
}
#[test]
fn no_sink_configured_is_not_an_error() {
let _g = lock();
std::env::remove_var("AETHER_AUDIT_SINK");
let log = tmp_log("nosink");
write_entries(&log, 2);
assert_eq!(
std::fs::read_to_string(&log)
.expect("read")
.lines()
.filter(|l| !l.trim().is_empty())
.count(),
2
);
let _ = std::fs::remove_file(&log);
}