use std::sync::Mutex;
use std::time::{Duration, Instant};
use kanade_shared::signing::{KeyPolicy, KeyRing, SigHeaders, VerifyError, verify};
use kanade_shared::wire::ObsEvent;
use serde::Deserialize;
use tracing::{error, info, warn};
const REG_SUBKEY: &str = r"SOFTWARE\kanade\agent";
const REG_VALUE: &str = "CommandKeys";
const REG_ENFORCE: &str = "RequireSignedCommands";
const KEYRING_FILE: &str = "/etc/kanade/command-keys.json";
const ENFORCE_FILE: &str = "/etc/kanade/require-signed-commands";
#[cfg(windows)]
macro_rules! keyring_location {
() => {
r"HKLM\SOFTWARE\kanade\agent\CommandKeys"
};
}
#[cfg(not(windows))]
macro_rules! keyring_location {
() => {
"/etc/kanade/command-keys.json"
};
}
const SOURCE: &str = "command_signature";
const RELOAD_MIN_INTERVAL: Duration = Duration::from_secs(30);
const RELOAD_FORCED_MIN_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Deserialize)]
struct KeyEntry {
kid: String,
public_key: String,
#[serde(default)]
label: Option<String>,
#[serde(default)]
max_age_secs: Option<u64>,
#[serde(default)]
audit_every_use: bool,
}
fn enforce_requested() -> bool {
enforce_requested_from(
kanade_shared::secrets::read_hklm_value(REG_SUBKEY, REG_ENFORCE),
platform_file(ENFORCE_FILE)
.as_deref()
.map(|p| (p, ROOT_UID)),
)
}
fn platform_file(path: &str) -> Option<std::path::PathBuf> {
(!cfg!(windows)).then(|| std::path::PathBuf::from(path))
}
fn enforce_requested_from(registry: Option<String>, file: Option<(&std::path::Path, u32)>) -> bool {
let value = registry.or_else(|| {
let (path, owner) = file?;
if let Err(e) = check_file_trusted(path, owner) {
error!(error = %e, "enforcement file is not root-only — treating enforcement as requested");
return Some("1".to_string());
}
std::fs::read_to_string(path).ok()
});
matches!(
value
.as_deref()
.map(str::trim)
.map(str::to_ascii_lowercase)
.as_deref(),
Some("1" | "true" | "yes")
)
}
fn lock_kids(ring: &KeyRing) -> Vec<&str> {
ring.kids().collect()
}
fn read_keyring_raw() -> Result<Option<String>, String> {
read_keyring_raw_from(
kanade_shared::secrets::try_read_hklm_value(REG_SUBKEY, REG_VALUE),
platform_file(KEYRING_FILE)
.as_deref()
.map(|p| (p, ROOT_UID)),
)
}
const ROOT_UID: u32 = 0;
fn read_keyring_raw_from(
registry: Result<Option<String>, String>,
file: Option<(&std::path::Path, u32)>,
) -> Result<Option<String>, String> {
match registry? {
Some(v) => Ok(Some(v)),
None => match file {
Some((p, owner)) => read_keyring_file(p, owner),
None => Ok(None),
},
}
}
fn check_file_trusted(path: &std::path::Path, owner: u32) -> Result<(), String> {
#[cfg(unix)]
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
if let Ok(m) = std::fs::metadata(path) {
if m.uid() != owner {
return Err(format!(
"{}: owned by uid {}, not uid {owner} — refusing to trust it; chown root",
path.display(),
m.uid()
));
}
if m.permissions().mode() & 0o022 != 0 {
return Err(format!(
"{}: writable by group/other (mode {:o}) — refusing to trust it; chmod 0600",
path.display(),
m.permissions().mode() & 0o777
));
}
}
}
#[cfg(not(unix))]
let _ = (path, owner);
Ok(())
}
fn read_keyring_file(path: &std::path::Path, owner: u32) -> Result<Option<String>, String> {
check_file_trusted(path, owner)?;
interpret_file_read(path, std::fs::read_to_string(path))
}
fn interpret_file_read(
path: &std::path::Path,
read: std::io::Result<String>,
) -> Result<Option<String>, String> {
match read {
Ok(s) if s.trim().is_empty() => Err(format!(
"{}: file is empty — write `[]` to revoke every key deliberately, or remove the file",
path.display()
)),
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("{}: {e}", path.display())),
}
}
fn parse_raw(raw: Option<&str>) -> Result<KeyRing, String> {
match raw {
None => Ok(KeyRing::new()),
Some(s) => parse_keyring(s),
}
}
fn parse_keyring(raw: &str) -> Result<KeyRing, String> {
use base64::Engine;
let entries: Vec<KeyEntry> = serde_json::from_str(raw).map_err(|e| e.to_string())?;
let mut ring = KeyRing::new();
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for e in entries {
if !seen.insert(e.kid.clone()) {
return Err(format!(
"key {} appears twice — two different keys must never share an id, and a ring \
keyed by id cannot hold both",
e.kid
));
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(&e.public_key)
.map_err(|err| format!("key {}: {err}", e.kid))?;
let arr: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| format!("key {}: expected 32 bytes, got {}", e.kid, bytes.len()))?;
let vk = ed25519_dalek::VerifyingKey::from_bytes(&arr)
.map_err(|err| format!("key {}: {err}", e.kid))?;
let label = e.label.unwrap_or_else(|| e.kid.clone());
let policy = match e.max_age_secs {
Some(secs) => KeyPolicy::break_glass(label, std::time::Duration::from_secs(secs)),
None => {
let mut p = KeyPolicy::backend(label);
p.audit_every_use = e.audit_every_use;
p
}
};
ring.insert(e.kid, vk, policy);
}
Ok(ring)
}
pub fn check_keyring_file(path: &std::path::Path) -> Result<Vec<String>, String> {
let raw = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
let value: serde_json::Value =
serde_json::from_str(raw.trim()).map_err(|e| format!("not valid JSON: {e}"))?;
let entries = value
.as_array()
.ok_or("must be a JSON ARRAY of entries, even for a single key")?;
if entries.is_empty() {
return Err("is an empty array — that provisions no keys at all".into());
}
let mut kids: Vec<String> = Vec::new();
for e in entries {
let field = |name: &str| {
e.get(name)
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
};
let (Some(kid), Some(_)) = (field("kid"), field("public_key")) else {
return Err(format!(
"every entry needs a non-empty STRING 'kid' and 'public_key'; got: {e}"
));
};
if e.get("label").is_some_and(|v| !v.is_string()) {
return Err(format!("key {kid}: 'label' must be a string"));
}
if let Some(v) = e.get("max_age_secs") {
match v.as_u64() {
Some(n) if n > 0 => {}
_ => {
return Err(format!(
"key {kid}: 'max_age_secs' must be a positive integer (do not quote it); \
a zero window rejects every signature made with the key"
));
}
}
}
if kids.iter().any(|k| k == kid) {
return Err(format!(
"key id {kid} appears more than once — two different keys must never share an id"
));
}
kids.push(kid.to_string());
}
parse_keyring(raw.trim())?;
Ok(kids)
}
pub fn headers_of(msg: &async_nats::Message) -> SigHeaders {
let get = |name: &str| {
msg.headers
.as_ref()
.and_then(|h| h.get(name))
.map(|v| v.to_string())
};
SigHeaders {
sig_b64: get(kanade_shared::signing::SIG),
kid: get(kanade_shared::signing::SIG_KID),
alg: get(kanade_shared::signing::SIG_ALG),
at_ms: get(kanade_shared::signing::SIG_AT),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Verified,
Unsigned,
Unprovisioned,
UnknownKid,
Invalid,
Stale,
}
impl Outcome {
pub fn is_refusal(self) -> bool {
!matches!(self, Outcome::Verified)
}
fn refusal_reason(self) -> &'static str {
match self {
Outcome::Verified => "verified",
Outcome::Unsigned => {
"command carried no signature, and this host requires one. If this came from \
`kanade run`, set the break-glass key; if from the backend, that backend is not \
signing yet."
}
Outcome::Unprovisioned => {
concat!(
"this host holds no command-signing keys at all, so nothing can be verified. \
Provision ",
keyring_location!(),
"."
)
}
Outcome::UnknownKid => {
"signed by a key this host does not have — it likely missed a rotation. Re-run \
the keyring provisioning for this machine."
}
Outcome::Invalid => {
"signature does not match these bytes. Either the command was tampered with in \
flight or it was signed by a key that is not the one it claims."
}
Outcome::Stale => {
"signature is outside its freshness window. Most often the clocks disagree — \
compare this host's time with the signing host's before assuming a replay."
}
}
}
#[cfg(test)]
const ALL: [Outcome; 6] = [
Outcome::Verified,
Outcome::Unsigned,
Outcome::Unprovisioned,
Outcome::UnknownKid,
Outcome::Invalid,
Outcome::Stale,
];
fn kind(self) -> &'static str {
match self {
Outcome::Verified => "command_signature_ok",
Outcome::Unsigned => "command_signature_absent",
Outcome::Unprovisioned => "command_signature_unprovisioned",
Outcome::UnknownKid => "command_signature_unknown_key",
Outcome::Invalid => "command_signature_invalid",
Outcome::Stale => "command_signature_stale",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reload {
Done,
Unchanged,
RateLimited,
Failed,
}
impl Reload {
fn as_str(self) -> &'static str {
match self {
Reload::Done => "reloaded",
Reload::Unchanged => "unchanged",
Reload::RateLimited => "rate-limited",
Reload::Failed => "reload-failed",
}
}
}
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
type Loader = Box<dyn Fn() -> Result<Option<String>, String> + Send + Sync>;
pub struct Verifier {
ring: Mutex<KeyRing>,
last_raw: Mutex<Option<String>>,
loader: Loader,
enforce_requested: bool,
empty_ring_warned: std::sync::atomic::AtomicBool,
last_reload: Mutex<Instant>,
pc_id: String,
obs_dir: std::path::PathBuf,
last: Mutex<Option<Outcome>>,
}
impl Verifier {
pub fn new(pc_id: String, obs_dir: std::path::PathBuf) -> Self {
Self::with_loader_and_policy(
pc_id,
obs_dir,
Box::new(read_keyring_raw),
enforce_requested(),
)
}
#[cfg(test)]
fn with_loader(pc_id: String, obs_dir: std::path::PathBuf, loader: Loader) -> Self {
Self::with_loader_and_policy(pc_id, obs_dir, loader, false)
}
fn with_loader_and_policy(
pc_id: String,
obs_dir: std::path::PathBuf,
loader: Loader,
enforce_requested: bool,
) -> Self {
if let Err(e) = crate::obs_outbox::ensure_outbox_dir(&obs_dir) {
warn!(error = %e, "command_verify: outbox dir — reports may be dropped until it exists");
}
let raw = loader().unwrap_or_else(|e| {
warn!(error = %e, "command keyring could not be READ — starting with no keys");
None
});
let ring = parse_raw(raw.as_deref()).unwrap_or_else(|e| {
warn!(error = %e, "command keyring is present but UNPARSEABLE — starting with no keys");
KeyRing::new()
});
if ring.is_empty() {
info!(
"command keyring is empty — signed commands will be reported unprovisioned until \
one is distributed (no restart needed; the ring reloads on demand)"
);
} else {
info!(kids = ?ring.kids().collect::<Vec<_>>(), "command keyring loaded");
}
let enforcing = enforce_requested && !ring.is_empty();
if enforce_requested && !enforcing {
error!(
"RequireSignedCommands is set but this host holds NO command-signing keys — \
refusing to enforce, because that would reject every command including the one \
that would provision the keys. Provision the keyring; the ring reloads on \
demand, so no restart is needed."
);
} else if enforcing {
warn!(
kids = ?lock_kids(&ring),
"enforcing command signatures — unverified commands will be REFUSED"
);
}
Self {
ring: Mutex::new(ring),
last_raw: Mutex::new(raw),
loader,
enforce_requested,
empty_ring_warned: std::sync::atomic::AtomicBool::new(false),
last_reload: Mutex::new(Instant::now()),
pc_id,
obs_dir,
last: Mutex::new(None),
}
}
fn enforcing_now(&self) -> bool {
if !self.enforce_requested {
return false;
}
if lock(&self.ring).is_empty() {
if !self
.empty_ring_warned
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
error!(concat!(
"this host is configured to require signed commands but its keyring is \
now EMPTY — declining to enforce rather than refusing everything, \
including the command that would restore the keys. Re-provision ",
keyring_location!(),
"."
));
}
return false;
}
self.empty_ring_warned
.store(false, std::sync::atomic::Ordering::Relaxed);
true
}
fn enforcing_with(&self, ring: &KeyRing) -> bool {
self.enforce_requested && !ring.is_empty()
}
pub fn refusal(&self, outcome: Outcome) -> Option<&'static str> {
(outcome.is_refusal() && self.enforcing_now()).then(|| outcome.refusal_reason())
}
#[cfg(test)]
pub fn trusted_keys(&self) -> Vec<String> {
lock(&self.ring).kid_fingerprints().collect()
}
pub fn observe(&self, body: &[u8], headers: &SigHeaders, request_id: &str) -> Outcome {
self.observe_at(
body,
headers,
request_id,
chrono::Utc::now().timestamp_millis(),
)
}
fn observe_at(
&self,
body: &[u8],
headers: &SigHeaders,
request_id: &str,
now_ms: i64,
) -> Outcome {
let outcome = self.classify(body, headers, request_id, now_ms, Instant::now());
self.report_transition(outcome);
outcome
}
fn classify(
&self,
body: &[u8],
headers: &SigHeaders,
request_id: &str,
now_ms: i64,
now: Instant,
) -> Outcome {
match self.check(body, headers, request_id, now_ms) {
Ok(outcome) => outcome,
Err(kid) => {
let reload = self.reload_if_due(now, self.enforce_requested);
if reload != Reload::Done {
return self.report_missing(&kid, request_id, reload.as_str());
}
match self.check(body, headers, request_id, now_ms) {
Ok(outcome) => {
info!(
kid,
request_id, "keyring reload resolved a previously unknown key"
);
outcome
}
Err(kid) => self.report_missing(&kid, request_id, "reloaded"),
}
}
}
}
fn check(
&self,
body: &[u8],
headers: &SigHeaders,
request_id: &str,
now_ms: i64,
) -> Result<Outcome, String> {
let ring = lock(&self.ring);
match verify(&ring, body, headers, now_ms) {
Ok(v) => {
if v.policy.audit_every_use {
warn!(kid = v.kid, request_id, "command signed by an audited key");
}
Ok(Outcome::Verified)
}
Err(VerifyError::Unsigned) => Ok(Outcome::Unsigned),
Err(VerifyError::UnknownKid { kid }) => Err(kid),
Err(e @ VerifyError::Stale { .. }) => {
warn!(error = %e, request_id, "command signature is past its freshness bound");
Ok(Outcome::Stale)
}
Err(e) => {
warn!(error = %e, request_id, "command signature did not verify");
Ok(Outcome::Invalid)
}
}
}
fn report_missing(&self, kid: &str, request_id: &str, reload: &str) -> Outcome {
let ring = lock(&self.ring);
if ring.is_empty() {
warn!(
kid,
request_id,
reload,
concat!(
"command is signed but this agent holds no keys — provision ",
keyring_location!()
)
);
Outcome::Unprovisioned
} else {
warn!(
kid,
request_id,
reload,
known = ?ring.kids().collect::<Vec<_>>(),
"command signed by a key this agent does not have"
);
Outcome::UnknownKid
}
}
fn reload_if_due(&self, now: Instant, force: bool) -> Reload {
let mut last = lock(&self.last_reload);
let floor = if force {
RELOAD_FORCED_MIN_INTERVAL
} else {
RELOAD_MIN_INTERVAL
};
if now.checked_duration_since(*last).unwrap_or_default() < floor {
return Reload::RateLimited;
}
*last = now;
drop(last);
self.pull()
}
fn pull(&self) -> Reload {
let mut last_raw = lock(&self.last_raw);
let raw = match (self.loader)() {
Ok(raw) => raw,
Err(e) => {
warn!(error = %e, "keyring refresh failed — keeping the keys already loaded");
return Reload::Failed;
}
};
if *last_raw == raw {
return Reload::Unchanged;
}
match parse_raw(raw.as_deref()) {
Ok(fresh) => {
info!(
kids = ?fresh.kids().collect::<Vec<_>>(),
"command keyring changed — adopted"
);
*lock(&self.ring) = fresh;
*last_raw = raw;
Reload::Done
}
Err(e) => {
warn!(error = %e, "keyring changed but is unreadable — keeping the keys already loaded");
Reload::Failed
}
}
}
pub fn refresh_and_report(&self) -> (Vec<String>, bool) {
self.pull();
let ring = lock(&self.ring);
(
ring.kid_fingerprints().collect(),
self.enforcing_with(&ring),
)
}
fn report_transition(&self, outcome: Outcome) {
let mut last = lock(&self.last);
let Some(transition) = step(*last, outcome) else {
return;
};
let event = build_event(&self.pc_id, &transition, chrono::Utc::now());
match crate::obs_outbox::enqueue(&self.obs_dir, &event) {
Ok(_path) => *last = Some(outcome),
Err(e) => warn!(
error = %e,
kind = outcome.kind(),
"command_verify: enqueue failed — will retry on the next command"
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Transition {
from: Option<Outcome>,
to: Outcome,
}
fn step(last: Option<Outcome>, outcome: Outcome) -> Option<Transition> {
if last == Some(outcome) {
return None;
}
Some(Transition {
from: last,
to: outcome,
})
}
fn build_event(pc_id: &str, t: &Transition, at: chrono::DateTime<chrono::Utc>) -> ObsEvent {
ObsEvent {
pc_id: pc_id.to_string(),
at,
kind: t.to.kind().to_string(),
source: SOURCE.to_string(),
event_record_id: Some(format!("{}:{}", t.to.kind(), at.timestamp_millis())),
payload: serde_json::json!({
"from": t.from.map(|p| p.kind()),
"to": t.to.kind(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use ed25519_dalek::SigningKey;
use kanade_shared::signing::sign;
fn b64(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn test_dir() -> std::path::PathBuf {
std::env::temp_dir().join("kanade-command-verify-test")
}
fn ring_to_json(ring: &KeyRing) -> Option<String> {
let kids: Vec<&str> = ring.kids().collect();
if kids.is_empty() {
return None;
}
let entries: Vec<serde_json::Value> = kids
.iter()
.map(|kid| {
let (_, vk, policy) = ring.get(kid).expect("kid came from this ring");
match policy.max_age {
Some(d) => serde_json::json!({
"kid": kid,
"public_key": kanade_shared::signing::encode_public(vk),
"max_age_secs": d.as_secs(),
}),
None => serde_json::json!({
"kid": kid,
"public_key": kanade_shared::signing::encode_public(vk),
}),
}
})
.collect();
Some(serde_json::to_string(&entries).expect("serialising a ring is infallible"))
}
fn verifier_with(ring: KeyRing) -> Verifier {
let raw = ring_to_json(&ring);
Verifier::with_loader("PC1".into(), test_dir(), Box::new(move || Ok(raw.clone())))
}
fn enforcing_with(ring: KeyRing) -> Verifier {
let raw = ring_to_json(&ring);
Verifier::with_loader_and_policy(
"PC1".into(),
test_dir(),
Box::new(move || Ok(raw.clone())),
true,
)
}
fn backend_ring(kid: &str, sk: &SigningKey) -> KeyRing {
let mut r = KeyRing::new();
r.insert(kid, sk.verifying_key(), KeyPolicy::backend("backend"));
r
}
fn reported(kid: &str, sk: &SigningKey) -> String {
format!(
"{kid}:{}",
kanade_shared::signing::fingerprint(&sk.verifying_key())
)
}
type StoreState = (Result<Option<String>, String>, usize);
#[derive(Clone)]
struct Store {
inner: std::sync::Arc<Mutex<StoreState>>,
}
impl Default for Store {
fn default() -> Self {
Self {
inner: std::sync::Arc::new(Mutex::new((Ok(None), 0))),
}
}
}
impl Store {
fn provision(&self, ring: KeyRing) {
lock(&self.inner).0 = Ok(ring_to_json(&ring));
}
fn corrupt(&self) {
lock(&self.inner).0 = Ok(Some("[{\"kid\":\"half-writ".to_string()));
}
fn unreadable(&self) {
lock(&self.inner).0 = Err("registry unavailable".into());
}
fn reads(&self) -> usize {
lock(&self.inner).1
}
fn loader(&self) -> Loader {
let inner = self.inner.clone();
Box::new(move || {
let mut g = lock(&inner);
g.1 += 1;
g.0.clone()
})
}
}
#[test]
fn keyring_parses_the_provisioned_shape() {
let sk = SigningKey::from_bytes(&[7u8; 32]);
let raw = format!(
r#"[{{"kid":"backend-1","public_key":"{}","label":"backend"}}]"#,
b64(sk.verifying_key().as_bytes())
);
let ring = parse_keyring(&raw).expect("parses");
let (kid, _, policy) = ring.get("backend-1").expect("present");
assert_eq!(kid, "backend-1");
assert_eq!(policy.label, "backend");
assert_eq!(policy.max_age, None);
assert!(!policy.audit_every_use);
}
#[test]
fn a_max_age_entry_becomes_a_break_glass_policy() {
let sk = SigningKey::from_bytes(&[8u8; 32]);
let raw = format!(
r#"[{{"kid":"bg","public_key":"{}","max_age_secs":300}}]"#,
b64(sk.verifying_key().as_bytes())
);
let ring = parse_keyring(&raw).unwrap();
let (_, _, policy) = ring.get("bg").unwrap();
assert_eq!(policy.max_age, Some(std::time::Duration::from_secs(300)));
assert!(policy.audit_every_use);
assert_eq!(policy.label, "bg");
}
#[test]
fn a_malformed_keyring_is_rejected_rather_than_half_loaded() {
let good = b64(SigningKey::from_bytes(&[1u8; 32])
.verifying_key()
.as_bytes());
let raw = format!(
r#"[{{"kid":"ok","public_key":"{good}"}},{{"kid":"bad","public_key":"not base64"}}]"#
);
assert!(parse_keyring(&raw).is_err());
let short = b64(&[0u8; 31]);
assert!(parse_keyring(&format!(r#"[{{"kid":"s","public_key":"{short}"}}]"#)).is_err());
assert!(parse_keyring("{{{").is_err());
}
#[test]
fn a_duplicate_kid_is_refused_rather_than_silently_collapsed() {
let a = b64(SigningKey::from_bytes(&[1u8; 32])
.verifying_key()
.as_bytes());
let b = b64(SigningKey::from_bytes(&[2u8; 32])
.verifying_key()
.as_bytes());
let raw =
format!(r#"[{{"kid":"bg","public_key":"{a}"}},{{"kid":"bg","public_key":"{b}"}}]"#);
let err = parse_keyring(&raw).unwrap_err();
assert!(err.contains("bg"), "the error must name the id: {err}");
assert!(err.contains("twice"), "{err}");
let raw = format!(
r#"[{{"kid":"backend-1","public_key":"{a}"}},{{"kid":"bg","public_key":"{b}","max_age_secs":900}}]"#
);
let ring = parse_keyring(&raw).expect("two distinct ids are fine");
assert!(ring.get("backend-1").is_some());
assert!(ring.get("bg").is_some());
}
#[test]
fn an_empty_ring_reports_unsigned_and_unknown_but_never_verifies() {
let ring = parse_keyring("[]").unwrap();
assert!(ring.is_empty());
let sk = SigningKey::from_bytes(&[3u8; 32]);
assert_eq!(
verify(&ring, b"body", &SigHeaders::default(), 0),
Err(VerifyError::Unsigned)
);
let headers = sign(&sk, "backend-1", b"body", 0);
assert!(matches!(
verify(&ring, b"body", &headers, 0),
Err(VerifyError::UnknownKid { .. })
));
}
#[test]
fn outcome_kinds_are_distinct_and_stable() {
let kinds: Vec<_> = Outcome::ALL.iter().map(|o| o.kind()).collect();
let unique: std::collections::BTreeSet<_> = kinds.iter().collect();
assert_eq!(unique.len(), kinds.len(), "kinds must not collide");
assert!(kinds.iter().all(|k| k.starts_with("command_signature")));
}
#[test]
fn step_reports_the_baseline_then_only_on_change() {
assert_eq!(
step(None, Outcome::Unsigned),
Some(Transition {
from: None,
to: Outcome::Unsigned
})
);
assert_eq!(step(Some(Outcome::Unsigned), Outcome::Unsigned), None);
assert_eq!(
step(Some(Outcome::Unsigned), Outcome::Verified),
Some(Transition {
from: Some(Outcome::Unsigned),
to: Outcome::Verified
})
);
assert_eq!(
step(Some(Outcome::Verified), Outcome::UnknownKid),
Some(Transition {
from: Some(Outcome::Verified),
to: Outcome::UnknownKid
})
);
}
#[test]
fn a_flapping_state_reports_each_way() {
let mut last = None;
let mut reported = Vec::new();
for o in [
Outcome::Verified,
Outcome::Verified,
Outcome::UnknownKid,
Outcome::Verified,
] {
if let Some(t) = step(last, o) {
reported.push(t.to);
last = Some(o);
}
}
assert_eq!(
reported,
vec![Outcome::Verified, Outcome::UnknownKid, Outcome::Verified]
);
}
#[test]
fn a_failed_enqueue_leaves_the_transition_unreported_so_it_retries() {
let mut last: Option<Outcome> = None;
let enqueue_ok = false;
if let Some(_t) = step(last, Outcome::UnknownKid)
&& enqueue_ok
{
last = Some(Outcome::UnknownKid);
}
assert_eq!(last, None, "a failed enqueue must not mark it reported");
assert!(
step(last, Outcome::UnknownKid).is_some(),
"the retry has to be possible"
);
}
#[test]
fn the_event_carries_both_ends_of_the_transition() {
let at = chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap();
let e = build_event(
"PC1",
&Transition {
from: Some(Outcome::Verified),
to: Outcome::UnknownKid,
},
at,
);
assert_eq!(e.pc_id, "PC1");
assert_eq!(e.source, SOURCE);
assert_eq!(e.kind, "command_signature_unknown_key");
assert_eq!(e.payload["from"], "command_signature_ok");
assert_eq!(e.payload["to"], "command_signature_unknown_key");
assert_eq!(
e.event_record_id.as_deref(),
Some("command_signature_unknown_key:1700000000000")
);
}
#[test]
fn the_first_event_reports_no_previous_state() {
let at = chrono::DateTime::from_timestamp(0, 0).unwrap();
let e = build_event(
"PC1",
&Transition {
from: None,
to: Outcome::Unsigned,
},
at,
);
assert!(e.payload["from"].is_null());
}
#[test]
fn a_replayed_break_glass_command_is_reported_stale_not_invalid() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let mut ring = KeyRing::new();
ring.insert(
"break-glass",
sk.verifying_key(),
KeyPolicy::break_glass("break-glass", std::time::Duration::from_secs(300)),
);
let v = verifier_with(ring);
let now = 1_700_000_000_000i64;
let body = b"emergency";
let week = 7 * 24 * 60 * 60 * 1000;
assert_eq!(
v.observe_at(body, &sign(&sk, "break-glass", body, now - week), "r1", now),
Outcome::Stale
);
assert_eq!(
v.observe_at(
body,
&sign(&sk, "break-glass", body, now - 1_000),
"r2",
now
),
Outcome::Verified
);
}
#[test]
fn a_key_provisioned_after_boot_takes_effect_without_a_restart() {
let sk = SigningKey::from_bytes(&[11u8; 32]);
let store = Store::default();
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let now = 1_700_000_000_000i64;
let body = b"job";
let headers = sign(&sk, "backend-1", body, now);
let boot = Instant::now();
assert_eq!(
v.classify(body, &headers, "r1", now, boot),
Outcome::Unprovisioned
);
store.provision(backend_ring("backend-1", &sk));
assert_eq!(
v.classify(body, &headers, "r2", now, boot),
Outcome::Unprovisioned
);
let later = boot + RELOAD_MIN_INTERVAL;
assert_eq!(
v.classify(body, &headers, "r3", now, later),
Outcome::Verified
);
}
#[test]
fn enforcement_refuses_everything_that_is_not_verified() {
let sk = SigningKey::from_bytes(&[31u8; 32]);
let v = enforcing_with(backend_ring("backend-1", &sk));
assert!(v.refusal(Outcome::Verified).is_none());
for o in [
Outcome::Unsigned,
Outcome::Unprovisioned,
Outcome::UnknownKid,
Outcome::Invalid,
Outcome::Stale,
] {
let reason = v.refusal(o).unwrap_or_else(|| panic!("{o:?} must refuse"));
assert!(!reason.is_empty());
}
}
#[test]
fn an_enforcing_host_reloads_before_refusing_even_inside_the_rate_limit() {
let sk = SigningKey::from_bytes(&[41u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader_and_policy(
"PC1".into(),
test_dir(),
store.loader(),
true, );
let now = 1_700_000_000_000i64;
let boot = Instant::now();
let headers = sign(&sk, "backend-2", b"job", now);
assert_eq!(
v.classify(b"job", &headers, "r1", now, boot),
Outcome::UnknownKid
);
let mut rotated = backend_ring("backend-1", &sk);
rotated.insert(
"backend-2",
sk.verifying_key(),
KeyPolicy::backend("backend (new)"),
);
store.provision(rotated);
assert_eq!(
v.classify(
b"job",
&headers,
"r2",
now,
boot + RELOAD_FORCED_MIN_INTERVAL
),
Outcome::Verified,
"an enforcing host must not refuse a key it already has on disk"
);
}
#[test]
fn the_forced_path_shortens_the_floor_rather_than_removing_it() {
let sk = SigningKey::from_bytes(&[43u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader_and_policy("PC1".into(), test_dir(), store.loader(), true);
let now = 1_700_000_000_000i64;
let headers = sign(&sk, "backend-2", b"x", now);
let base = Instant::now() + RELOAD_FORCED_MIN_INTERVAL;
let reads = store.reads();
for i in 0..10 {
assert_eq!(
v.classify(
b"x",
&headers,
"r",
now,
base + RELOAD_FORCED_MIN_INTERVAL / 20 * i
),
Outcome::UnknownKid
);
}
assert_eq!(
store.reads(),
reads + 1,
"a burst must not be one read each"
);
assert_eq!(
v.classify(
b"x",
&headers,
"r",
now,
base + RELOAD_FORCED_MIN_INTERVAL * 2
),
Outcome::UnknownKid
);
assert_eq!(store.reads(), reads + 2);
}
#[test]
fn a_non_enforcing_host_still_respects_the_rate_limit() {
let sk = SigningKey::from_bytes(&[42u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let now = 1_700_000_000_000i64;
let boot = Instant::now();
let headers = sign(&sk, "backend-2", b"job", now);
let reads = store.reads();
for i in 0..5 {
assert_eq!(
v.classify(
b"job",
&headers,
"r",
now,
boot + RELOAD_MIN_INTERVAL / 20 * i
),
Outcome::UnknownKid
);
}
assert_eq!(store.reads(), reads, "still bounded when not enforcing");
}
#[test]
fn a_host_that_is_not_enforcing_refuses_nothing() {
let sk = SigningKey::from_bytes(&[32u8; 32]);
let v = verifier_with(backend_ring("backend-1", &sk));
for o in Outcome::ALL {
assert!(v.refusal(o).is_none(), "{o:?} must not refuse");
}
}
#[test]
fn a_ring_that_goes_empty_at_runtime_stops_enforcing_too() {
let sk = SigningKey::from_bytes(&[51u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader_and_policy("PC1".into(), test_dir(), store.loader(), true);
assert!(v.refusal(Outcome::Unsigned).is_some());
store.provision(KeyRing::new());
let t = Instant::now() + RELOAD_MIN_INTERVAL * 2;
assert_eq!(
v.classify(b"job", &sign(&sk, "backend-2", b"job", 0), "r1", 0, t),
Outcome::Unprovisioned
);
assert!(
v.refusal(Outcome::Unsigned).is_none(),
"a host holding no keys must stop enforcing, not brick"
);
store.provision(backend_ring("backend-1", &sk));
assert_eq!(
v.classify(
b"job",
&sign(&sk, "backend-2", b"job", 0),
"r2",
0,
t + RELOAD_MIN_INTERVAL * 2
),
Outcome::UnknownKid
);
assert!(
v.refusal(Outcome::Unsigned).is_some(),
"enforcement must come back when the keys do"
);
}
#[test]
fn an_empty_ring_declines_to_enforce_rather_than_bricking_the_host() {
let v =
Verifier::with_loader_and_policy("PC1".into(), test_dir(), Box::new(|| Ok(None)), true);
assert!(
v.refusal(Outcome::Unsigned).is_none(),
"an empty ring must not enforce"
);
}
#[test]
fn every_refusal_reason_says_what_to_do_next() {
for o in Outcome::ALL {
if !o.is_refusal() {
continue;
}
let r = o.refusal_reason();
assert!(r.len() > 40, "{o:?} reason is too thin: {r}");
assert!(
r.contains("host") || r.contains("key") || r.contains("clock"),
"{o:?} reason should point somewhere: {r}"
);
}
}
#[test]
fn revoking_a_key_takes_effect_without_a_restart() {
let backend = SigningKey::from_bytes(&[61u8; 32]);
let compromised = SigningKey::from_bytes(&[62u8; 32]);
let mut both = backend_ring("backend-1", &backend);
both.insert(
"leaked",
compromised.verifying_key(),
KeyPolicy::backend("leaked"),
);
let store = Store::default();
store.provision(both);
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let now = 1_700_000_000_000i64;
let t = Instant::now() + RELOAD_MIN_INTERVAL * 2;
let signed = sign(&compromised, "leaked", b"payload", now);
assert_eq!(
v.classify(b"payload", &signed, "r1", now, t),
Outcome::Verified
);
store.provision(backend_ring("backend-1", &backend));
assert_eq!(
v.classify(b"payload", &signed, "r2", now, t),
Outcome::Verified,
"this is the gap: the command path has no reason to re-read"
);
let (kids, _) = v.refresh_and_report();
assert_eq!(kids, vec![reported("backend-1", &backend)]);
assert_eq!(
v.classify(b"payload", &signed, "r3", now, t),
Outcome::UnknownKid,
"a revoked key must stop verifying"
);
}
#[test]
fn two_rings_sharing_a_kid_but_not_a_key_report_differently() {
let right = SigningKey::from_bytes(&[70u8; 32]);
let wrong = SigningKey::from_bytes(&[71u8; 32]);
let good = Store::default();
good.provision(backend_ring("backend-1", &right));
let bad = Store::default();
bad.provision(backend_ring("backend-1", &wrong));
let a = Verifier::with_loader("PC1".into(), test_dir(), good.loader());
let b = Verifier::with_loader("PC2".into(), test_dir(), bad.loader());
let (a, b) = (a.trusted_keys(), b.trusted_keys());
assert_ne!(a, b, "a fleet view must be able to tell these apart");
assert!(
a[0].starts_with("backend-1:") && b[0].starts_with("backend-1:"),
"and the kid must still be greppable on its own: {a:?} {b:?}"
);
}
#[test]
fn a_fingerprint_survives_a_reload_that_changes_nothing_else() {
let sk = SigningKey::from_bytes(&[72u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let (first, _) = v.refresh_and_report();
assert_eq!(v.pull(), Reload::Unchanged);
assert_eq!(v.refresh_and_report().0, first);
assert_eq!(first, vec![reported("backend-1", &sk)]);
}
#[test]
fn a_host_that_is_not_configured_to_enforce_reports_false() {
let sk = SigningKey::from_bytes(&[73u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let (keys, enforcing) = v.refresh_and_report();
assert_eq!(keys, vec![reported("backend-1", &sk)]);
assert!(!enforcing, "with_loader does not request enforcement");
}
#[test]
fn an_enforcing_host_that_loses_its_ring_reports_false() {
let sk = SigningKey::from_bytes(&[74u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader_and_policy("PC1".into(), test_dir(), store.loader(), true);
let (_, enforcing) = v.refresh_and_report();
assert!(enforcing, "a requested, populated ring enforces");
store.provision(KeyRing::new());
let (keys, enforcing) = v.refresh_and_report();
assert!(keys.is_empty());
assert!(
!enforcing,
"an empty ring cannot enforce, whatever the registry says"
);
}
#[test]
fn reporting_does_not_fire_the_empty_ring_warning() {
let sk = SigningKey::from_bytes(&[75u8; 32]);
let store = Store::default();
store.provision(KeyRing::new());
let v = Verifier::with_loader_and_policy("PC1".into(), test_dir(), store.loader(), true);
for _ in 0..3 {
assert!(!v.refresh_and_report().1);
}
assert!(
!v.empty_ring_warned
.load(std::sync::atomic::Ordering::Relaxed),
"reporting must not consume the one-shot warning the command path owns"
);
let _ = v.refusal(Outcome::Unsigned);
assert!(
v.empty_ring_warned
.load(std::sync::atomic::Ordering::Relaxed),
"the decision path is what warns"
);
let _ = sk;
}
#[test]
fn an_unchanged_store_is_not_reparsed() {
let sk = SigningKey::from_bytes(&[63u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let reads = store.reads();
assert_eq!(v.pull(), Reload::Unchanged);
assert_eq!(v.pull(), Reload::Unchanged);
assert_eq!(store.reads(), reads + 2);
store.provision(backend_ring("backend-2", &sk));
assert_eq!(v.pull(), Reload::Done);
assert_eq!(v.pull(), Reload::Unchanged);
}
#[test]
fn a_half_written_value_is_retried_rather_than_remembered() {
let sk = SigningKey::from_bytes(&[64u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
store.corrupt();
assert_eq!(v.pull(), Reload::Failed);
assert_eq!(v.pull(), Reload::Failed, "it must keep retrying");
assert_eq!(v.trusted_keys(), vec![reported("backend-1", &sk)]);
store.provision(backend_ring("backend-2", &sk));
assert_eq!(v.pull(), Reload::Done);
assert_eq!(v.trusted_keys(), vec![reported("backend-2", &sk)]);
}
#[test]
fn an_unreadable_store_keeps_the_ring_and_does_not_poison_the_cache() {
let sk = SigningKey::from_bytes(&[65u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
store.unreadable();
assert_eq!(v.pull(), Reload::Failed);
assert_eq!(v.trusted_keys(), vec![reported("backend-1", &sk)]);
store.provision(backend_ring("backend-2", &sk));
assert_eq!(v.pull(), Reload::Done);
}
#[test]
fn a_reload_that_cannot_be_read_keeps_the_working_ring() {
let sk = SigningKey::from_bytes(&[21u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let now = 1_700_000_000_000i64;
let good = sign(&sk, "backend-1", b"job", now);
let t = Instant::now() + RELOAD_MIN_INTERVAL * 2;
assert_eq!(v.classify(b"job", &good, "r1", now, t), Outcome::Verified);
store.corrupt();
let unknown = sign(&sk, "backend-2", b"job", now);
assert_eq!(
v.classify(b"job", &unknown, "r2", now, t + RELOAD_MIN_INTERVAL),
Outcome::UnknownKid,
"a failed reload must not turn this into Unprovisioned"
);
assert_eq!(
v.classify(b"job", &good, "r3", now, t + RELOAD_MIN_INTERVAL * 2),
Outcome::Verified,
"the working ring must survive a failed reload"
);
}
#[test]
fn only_an_unknown_key_reaches_the_store() {
let sk = SigningKey::from_bytes(&[12u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let after_boot = Instant::now() + RELOAD_MIN_INTERVAL * 10;
let now = 1_700_000_000_000i64;
let reads = store.reads();
assert_eq!(
v.classify(b"x", &SigHeaders::default(), "r1", now, after_boot),
Outcome::Unsigned
);
let forged = sign(&SigningKey::from_bytes(&[99u8; 32]), "backend-1", b"x", now);
assert_eq!(
v.classify(b"x", &forged, "r2", now, after_boot),
Outcome::Invalid
);
let partial = SigHeaders {
sig_b64: Some("AAAA".into()),
kid: None,
alg: None,
at_ms: None,
};
assert_eq!(
v.classify(b"x", &partial, "r3", now, after_boot),
Outcome::Invalid
);
assert_eq!(store.reads(), reads, "none of these may touch the store");
let unknown = sign(&sk, "backend-2", b"x", now);
assert_eq!(
v.classify(b"x", &unknown, "r4", now, after_boot),
Outcome::UnknownKid
);
assert_eq!(store.reads(), reads + 1);
}
#[test]
fn repeated_unknown_keys_reload_at_most_once_per_interval() {
let sk = SigningKey::from_bytes(&[13u8; 32]);
let store = Store::default();
store.provision(backend_ring("backend-1", &sk));
let v = Verifier::with_loader("PC1".into(), test_dir(), store.loader());
let now = 1_700_000_000_000i64;
let headers = sign(&sk, "backend-2", b"x", now);
let base = Instant::now() + RELOAD_MIN_INTERVAL;
let reads = store.reads();
for i in 0..20 {
let t = base + RELOAD_MIN_INTERVAL / 40 * i;
assert_eq!(v.classify(b"x", &headers, "r", now, t), Outcome::UnknownKid);
}
assert_eq!(
store.reads(),
reads + 1,
"a flood of invented key ids must not become a flood of store reads"
);
assert_eq!(
v.classify(b"x", &headers, "r", now, base + RELOAD_MIN_INTERVAL * 2),
Outcome::UnknownKid
);
assert_eq!(store.reads(), reads + 2);
}
#[test]
fn an_empty_ring_and_a_missing_key_are_different_states() {
let sk = SigningKey::from_bytes(&[14u8; 32]);
let now = 1_700_000_000_000i64;
let headers = sign(&sk, "backend-2", b"x", now);
let t = Instant::now() + RELOAD_MIN_INTERVAL * 2;
let empty = Verifier::with_loader("PC1".into(), test_dir(), Box::new(|| Ok(None)));
assert_eq!(
empty.classify(b"x", &headers, "r1", now, t),
Outcome::Unprovisioned
);
let other = verifier_with(backend_ring("backend-1", &sk));
assert_eq!(
other.classify(b"x", &headers, "r2", now, t),
Outcome::UnknownKid
);
}
#[test]
fn a_non_monotonic_clock_does_not_panic() {
let sk = SigningKey::from_bytes(&[15u8; 32]);
let v = verifier_with(backend_ring("backend-1", &sk));
let now = 1_700_000_000_000i64;
let headers = sign(&sk, "backend-2", b"x", now);
let Some(past) = Instant::now().checked_sub(RELOAD_MIN_INTERVAL * 3) else {
return;
};
assert_eq!(
v.classify(b"x", &headers, "r1", now, past),
Outcome::UnknownKid
);
}
#[test]
fn the_ordinary_signer_is_never_stale() {
let sk = SigningKey::from_bytes(&[4u8; 32]);
let mut ring = KeyRing::new();
ring.insert(
"backend-1",
sk.verifying_key(),
KeyPolicy::backend("backend"),
);
let v = verifier_with(ring);
let now = 1_700_000_000_000i64;
let week = 7 * 24 * 60 * 60 * 1000;
assert_eq!(
v.observe_at(
b"job",
&sign(&sk, "backend-1", b"job", now - week),
"r1",
now
),
Outcome::Verified
);
}
fn ring_json(kid: &str, sk: &SigningKey) -> String {
ring_to_json(&backend_ring(kid, sk)).expect("non-empty ring")
}
fn me() -> u32 {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let f = tempfile::NamedTempFile::new().unwrap();
f.as_file().metadata().unwrap().uid()
}
#[cfg(not(unix))]
{
0
}
}
fn file_loader(path: std::path::PathBuf) -> Loader {
Box::new(move || read_keyring_raw_from(Ok(None), Some((&path, me()))))
}
#[test]
fn an_absent_keyring_file_is_nothing_provisioned() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
assert_eq!(
read_keyring_raw_from(Ok(None), Some((&path, me()))),
Ok(None)
);
assert_eq!(read_keyring_raw_from(Ok(None), None), Ok(None));
}
#[test]
fn a_valid_keyring_file_is_read_and_parses() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let sk = SigningKey::from_bytes(&[71u8; 32]);
std::fs::write(&path, ring_json("backend-1", &sk)).unwrap();
let raw = read_keyring_raw_from(Ok(None), Some((&path, me()))).unwrap();
let ring = parse_raw(raw.as_deref()).unwrap();
assert_eq!(ring.kids().collect::<Vec<_>>(), vec!["backend-1"]);
}
#[test]
fn a_malformed_keyring_file_is_present_but_unparseable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let sk = SigningKey::from_bytes(&[72u8; 32]);
std::fs::write(
&path,
format!(
r#"[{{"kid":"ok","public_key":"{}"}},{{"kid":"bad","public_key":"not-base64!"}}]"#,
b64(sk.verifying_key().as_bytes())
),
)
.unwrap();
let raw = read_keyring_raw_from(Ok(None), Some((&path, me()))).unwrap();
assert!(raw.is_some(), "present, so not reported as absent");
assert!(parse_raw(raw.as_deref()).is_err());
}
#[test]
fn an_empty_keyring_file_is_an_error_but_an_empty_array_is_a_ring() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
std::fs::write(&path, " \n").unwrap();
assert!(read_keyring_raw_from(Ok(None), Some((&path, me()))).is_err());
std::fs::write(&path, "[]").unwrap();
let raw = read_keyring_raw_from(Ok(None), Some((&path, me()))).unwrap();
assert!(parse_raw(raw.as_deref()).unwrap().is_empty());
}
#[test]
fn an_unreadable_keyring_file_is_an_error_not_absence() {
let path = std::path::Path::new("/etc/kanade/command-keys.json");
let denied = Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
assert!(interpret_file_read(path, denied).is_err());
let gone = Err(std::io::Error::from(std::io::ErrorKind::NotFound));
assert_eq!(interpret_file_read(path, gone), Ok(None));
}
#[cfg(unix)]
#[test]
fn a_keyring_file_others_can_write_is_refused() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let sk = SigningKey::from_bytes(&[73u8; 32]);
std::fs::write(&path, ring_json("backend-1", &sk)).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o666)).unwrap();
assert!(read_keyring_raw_from(Ok(None), Some((&path, me()))).is_err());
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(read_keyring_raw_from(Ok(None), Some((&path, me()))).is_ok());
}
#[cfg(unix)]
#[test]
fn a_keyring_file_owned_by_someone_else_is_refused() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let sk = SigningKey::from_bytes(&[82u8; 32]);
std::fs::write(&path, ring_json("backend-1", &sk)).unwrap();
assert!(read_keyring_raw_from(Ok(None), Some((&path, me() + 1))).is_err());
assert!(read_keyring_raw_from(Ok(None), Some((&path, me()))).is_ok());
}
#[test]
fn the_registry_wins_over_the_file_and_a_registry_error_does_not_fall_through() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let file_key = SigningKey::from_bytes(&[74u8; 32]);
std::fs::write(&path, ring_json("from-file", &file_key)).unwrap();
let reg = Ok(Some("[]".to_string()));
assert_eq!(
read_keyring_raw_from(reg, Some((&path, me()))),
Ok(Some("[]".to_string()))
);
assert_eq!(
read_keyring_raw_from(Ok(None), Some((&path, me()))).unwrap(),
Some(ring_json("from-file", &file_key))
);
assert!(
read_keyring_raw_from(Err("registry unavailable".into()), Some((&path, me()))).is_err()
);
}
#[test]
fn a_file_keyring_verifies_and_is_listed_like_the_registry_one() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let sk = SigningKey::from_bytes(&[75u8; 32]);
std::fs::write(&path, ring_json("backend-1", &sk)).unwrap();
let v = Verifier::with_loader("PC1".into(), test_dir(), file_loader(path));
let now = 1_700_000_000_000i64;
assert_eq!(
v.observe_at(b"p", &sign(&sk, "backend-1", b"p", now), "r1", now),
Outcome::Verified
);
assert_eq!(v.trusted_keys(), vec![reported("backend-1", &sk)]);
}
#[test]
fn a_key_added_to_the_file_after_boot_takes_effect_on_an_unknown_kid() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let v = Verifier::with_loader("PC1".into(), test_dir(), file_loader(path.clone()));
let sk = SigningKey::from_bytes(&[76u8; 32]);
let now = 1_700_000_000_000i64;
let t = Instant::now() + RELOAD_MIN_INTERVAL * 2;
let signed = sign(&sk, "backend-1", b"p", now);
assert_eq!(
v.classify(b"p", &signed, "r1", now, t),
Outcome::Unprovisioned
);
std::fs::write(&path, ring_json("backend-1", &sk)).unwrap();
assert_eq!(
v.classify(b"p", &signed, "r2", now, t + RELOAD_MIN_INTERVAL * 2),
Outcome::Verified
);
}
#[test]
fn revoking_a_key_in_the_file_takes_effect_on_the_heartbeat() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let backend = SigningKey::from_bytes(&[77u8; 32]);
let leaked = SigningKey::from_bytes(&[78u8; 32]);
let mut both = backend_ring("backend-1", &backend);
both.insert(
"leaked",
leaked.verifying_key(),
KeyPolicy::backend("leaked"),
);
std::fs::write(&path, ring_to_json(&both).unwrap()).unwrap();
let v = Verifier::with_loader("PC1".into(), test_dir(), file_loader(path.clone()));
assert_eq!(v.trusted_keys().len(), 2);
std::fs::write(&path, ring_json("backend-1", &backend)).unwrap();
let (kids, _) = v.refresh_and_report();
assert_eq!(kids, vec![reported("backend-1", &backend)]);
std::fs::remove_file(&path).unwrap();
let (kids, _) = v.refresh_and_report();
assert!(kids.is_empty());
}
#[test]
fn a_broken_keyring_file_keeps_the_working_ring() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("command-keys.json");
let sk = SigningKey::from_bytes(&[79u8; 32]);
std::fs::write(&path, ring_json("backend-1", &sk)).unwrap();
let v = Verifier::with_loader("PC1".into(), test_dir(), file_loader(path.clone()));
std::fs::write(&path, "[{\"kid\":\"half-writ").unwrap();
let (kids, _) = v.refresh_and_report();
assert_eq!(kids, vec![reported("backend-1", &sk)]);
}
#[test]
fn the_installer_check_applies_the_deploy_script_rules() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("keys.json");
let sk = SigningKey::from_bytes(&[81u8; 32]);
let pk = b64(sk.verifying_key().as_bytes());
let check = |body: &str| {
std::fs::write(&path, body).unwrap();
check_keyring_file(&path)
};
assert_eq!(
check(&format!(r#"[{{"kid":"a","public_key":"{pk}"}}]"#)),
Ok(vec!["a".to_string()])
);
assert!(check("{}").is_err(), "not an array");
assert!(check("[]").is_err(), "empty");
assert!(check("nope").is_err(), "not JSON");
assert!(check(&format!(r#"[{{"kid":20260728,"public_key":"{pk}"}}]"#)).is_err());
assert!(check(&format!(r#"[{{"kid":" ","public_key":"{pk}"}}]"#)).is_err());
assert!(check(r#"[{"kid":"a","public_key":""}]"#).is_err());
assert!(
check(&format!(
r#"[{{"kid":"a","public_key":"{pk}"}},{{"kid":"a","public_key":"{pk}"}}]"#
))
.is_err(),
"duplicate kid"
);
assert!(check(r#"[{"kid":"a","public_key":"not-a-key"}]"#).is_err());
for bad in ["0", "-5", "\"900\"", "1.5"] {
assert!(
check(&format!(
r#"[{{"kid":"a","public_key":"{pk}","max_age_secs":{bad}}}]"#
))
.is_err(),
"max_age_secs {bad}"
);
}
assert!(check(&format!(r#"[{{"kid":"a","public_key":"{pk}","label":5}}]"#)).is_err());
assert!(
check(&format!(
r#"[{{"kid":"a","public_key":"{pk}","max_age_secs":900}}]"#
))
.is_ok()
);
}
#[test]
fn the_enforce_file_turns_enforcement_on_only_for_truthy_values() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("require-signed-commands");
for (content, want) in [
("1\n", true),
("true", true),
(" YES ", true),
("0", false),
("", false),
("no", false),
] {
std::fs::write(&path, content).unwrap();
assert_eq!(
enforce_requested_from(None, Some((&path, me()))),
want,
"content {content:?}"
);
}
}
#[test]
fn an_absent_or_unreadable_enforce_file_leaves_enforcement_off() {
let dir = tempfile::tempdir().unwrap();
assert!(!enforce_requested_from(
None,
Some((&dir.path().join("nope"), me()))
));
assert!(!enforce_requested_from(None, Some((dir.path(), me()))));
assert!(!enforce_requested_from(None, None));
}
#[cfg(unix)]
#[test]
fn an_enforce_file_others_can_rewrite_is_treated_as_enforcing() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("require-signed-commands");
std::fs::write(&path, "0").unwrap();
assert!(enforce_requested_from(None, Some((&path, me() + 1))));
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o666)).unwrap();
assert!(enforce_requested_from(None, Some((&path, me()))));
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(!enforce_requested_from(None, Some((&path, me()))));
}
#[test]
fn the_registry_flag_takes_precedence_over_the_enforce_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("require-signed-commands");
std::fs::write(&path, "0").unwrap();
assert!(enforce_requested_from(
Some("1".into()),
Some((&path, me()))
));
std::fs::write(&path, "1").unwrap();
assert!(!enforce_requested_from(
Some("0".into()),
Some((&path, me()))
));
}
#[test]
fn a_requested_flag_with_an_empty_ring_still_declines_to_enforce() {
let v =
Verifier::with_loader_and_policy("PC1".into(), test_dir(), Box::new(|| Ok(None)), true);
assert!(!v.enforcing_now());
let sk = SigningKey::from_bytes(&[80u8; 32]);
let v = enforcing_with(backend_ring("backend-1", &sk));
assert!(v.enforcing_now());
}
}