use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64;
use ed25519_dalek::VerifyingKey;
use super::credential::{
CREDENTIAL_PREFIX, CredentialError, FederationCredential, SignedCredential,
};
use super::trust_bundle::TrustBundle;
pub const CHAIN_HEADER: &str = "x-memory-cred-chain";
pub const DEFAULT_MAX_CHAIN_DEPTH: usize = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainError {
EmptyChain,
ChainTooDeep {
depth: usize,
max: usize,
},
NameMismatch,
DomainMismatch,
Link(CredentialError),
DelegationOutOfNamespace {
subject: String,
delegated_namespace: String,
},
}
impl ChainError {
#[must_use]
pub fn tag(&self) -> &'static str {
match self {
Self::EmptyChain => "chain_empty",
Self::ChainTooDeep { .. } => "chain_too_deep",
Self::NameMismatch => "chain_name_mismatch",
Self::DomainMismatch => "chain_domain_mismatch",
Self::DelegationOutOfNamespace { .. } => "chain_delegation_out_of_namespace",
Self::Link(e) => e.tag(),
}
}
}
impl std::fmt::Display for ChainError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ChainTooDeep { depth, max } => {
write!(f, "{} (depth {depth} > max {max})", self.tag())
}
Self::Link(e) => write!(f, "{e}"),
_ => f.write_str(self.tag()),
}
}
}
impl std::error::Error for ChainError {}
impl From<CredentialError> for ChainError {
fn from(e: CredentialError) -> Self {
Self::Link(e)
}
}
#[derive(Debug, Clone)]
pub struct CertChain {
pub intermediates: Vec<SignedCredential>,
pub leaf: SignedCredential,
}
impl CertChain {
#[must_use]
pub fn new(leaf: SignedCredential, intermediates: Vec<SignedCredential>) -> Self {
Self {
intermediates,
leaf,
}
}
#[must_use]
pub fn direct(leaf: SignedCredential) -> Self {
Self {
intermediates: Vec::new(),
leaf,
}
}
#[must_use]
pub fn depth(&self) -> usize {
self.intermediates.len() + 1
}
pub fn intermediates_header_value(&self) -> Result<Option<String>, CredentialError> {
intermediates_to_header_value(&self.intermediates)
}
pub fn intermediates_from_header_value(
value: &str,
) -> Result<Vec<SignedCredential>, CredentialError> {
let b64 = value
.strip_prefix(CREDENTIAL_PREFIX)
.ok_or(CredentialError::Malformed)?;
let wire = B64.decode(b64).map_err(|_| CredentialError::Malformed)?;
let parsed: ciborium::Value =
ciborium::de::from_reader(&wire[..]).map_err(|_| CredentialError::Malformed)?;
let items = match parsed {
ciborium::Value::Array(a) => a,
_ => return Err(CredentialError::Malformed),
};
let mut out = Vec::with_capacity(items.len());
for item in items {
let bytes = match item {
ciborium::Value::Bytes(b) => b,
_ => return Err(CredentialError::Malformed),
};
out.push(SignedCredential::from_wire_bytes(&bytes)?);
}
Ok(out)
}
pub fn verify(
&self,
bundle: &TrustBundle,
now_unix: i64,
max_depth: usize,
) -> Result<FederationCredential, ChainError> {
let depth = self.depth();
if depth > max_depth {
return Err(ChainError::ChainTooDeep {
depth,
max: max_depth,
});
}
let Some((anchor, rest)) = self.intermediates.split_first() else {
return Ok(bundle.verify(&self.leaf, now_unix)?);
};
let mut parent = bundle.verify(anchor, now_unix)?;
for cert in rest.iter().chain(std::iter::once(&self.leaf)) {
verify_link(cert, &parent, now_unix)?;
parent = cert.credential().clone();
}
Ok(self.leaf.credential().clone())
}
}
fn verify_link(
child: &SignedCredential,
parent: &FederationCredential,
now_unix: i64,
) -> Result<(), ChainError> {
let c = child.credential();
if c.issuer_id != parent.subject_agent_id {
return Err(ChainError::NameMismatch);
}
if c.trust_domain != parent.trust_domain {
return Err(ChainError::DomainMismatch);
}
let parent_key: VerifyingKey = parent.subject_verifying_key()?;
child.verify_against(&parent_key, now_unix)?;
let delegated = delegated_namespace_of(&parent.subject_agent_id);
if !subject_in_delegated_namespace(&c.subject_agent_id, delegated) {
return Err(ChainError::DelegationOutOfNamespace {
subject: c.subject_agent_id.clone(),
delegated_namespace: delegated.to_string(),
});
}
Ok(())
}
pub const CA_MARKER_SUFFIX: &str = "/ca";
fn delegated_namespace_of(parent_subject: &str) -> &str {
parent_subject
.strip_suffix(CA_MARKER_SUFFIX)
.unwrap_or(parent_subject)
}
#[must_use]
pub fn subject_in_delegated_namespace(subject_agent_id: &str, ca_namespace: &str) -> bool {
if ca_namespace.is_empty() {
return true;
}
if subject_agent_id == ca_namespace {
return true;
}
let boundary = if ca_namespace.ends_with('/') {
ca_namespace.to_string()
} else {
format!("{ca_namespace}/")
};
subject_agent_id.starts_with(&boundary)
}
pub const FED_CRED_CHAIN_PATH_ENV: &str = "AI_MEMORY_FED_CRED_CHAIN_PATH";
pub fn intermediates_to_header_value(
intermediates: &[SignedCredential],
) -> Result<Option<String>, CredentialError> {
if intermediates.is_empty() {
return Ok(None);
}
let mut items = Vec::with_capacity(intermediates.len());
for ic in intermediates {
items.push(ciborium::Value::Bytes(ic.to_wire_bytes()?));
}
let value = ciborium::Value::Array(items);
let mut out = Vec::new();
ciborium::ser::into_writer(&value, &mut out).map_err(|_| CredentialError::Malformed)?;
Ok(Some(format!("{CREDENTIAL_PREFIX}{}", B64.encode(out))))
}
pub fn load_intermediates_from_path(
path: &std::path::Path,
) -> std::io::Result<Vec<SignedCredential>> {
let raw = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
};
CertChain::intermediates_from_header_value(raw.trim())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn load_intermediates_from_env() -> std::io::Result<Vec<SignedCredential>> {
match std::env::var(FED_CRED_CHAIN_PATH_ENV) {
Ok(path) => load_intermediates_from_path(std::path::Path::new(&path)),
Err(_) => Ok(Vec::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
const NOW: i64 = 1_900_000_000;
const DOMAIN: &str = "fleet.example";
fn signing_key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
fn mint(
signer: &SigningKey,
issuer_id: &str,
subject_id: &str,
subject_key: &VerifyingKey,
domain: &str,
not_after: i64,
) -> SignedCredential {
FederationCredential {
subject_agent_id: subject_id.to_string(),
subject_pubkey: subject_key.to_bytes(),
issuer_id: issuer_id.to_string(),
trust_domain: domain.to_string(),
not_before: NOW - 10,
not_after,
cred_version: super::super::credential::CRED_VERSION,
}
.sign(signer)
.expect("sign")
}
fn two_level_setup() -> (TrustBundle, SignedCredential, SignedCredential) {
let root = signing_key(1);
let intermediate = signing_key(2);
let node = signing_key(3);
let inter_cert = mint(
&root,
"root",
"region/nyc/ca",
&intermediate.verifying_key(),
DOMAIN,
NOW + 7200,
);
let leaf = mint(
&intermediate,
"region/nyc/ca",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
(bundle, inter_cert, leaf)
}
#[test]
fn two_level_chain_verifies() {
let (bundle, inter, leaf) = two_level_setup();
let chain = CertChain::new(leaf, vec![inter]);
let verified = chain
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.expect("chain verifies");
assert_eq!(verified.subject_agent_id, "region/nyc/node-1");
}
#[test]
fn intermediates_header_round_trips_and_reverifies() {
let (bundle, inter, leaf) = two_level_setup();
let chain = CertChain::new(leaf, vec![inter]);
let header = chain
.intermediates_header_value()
.expect("encode chain header")
.expect("two-level chain emits a header");
let parsed_inters =
CertChain::intermediates_from_header_value(&header).expect("parse chain header");
let leaf_header = chain.leaf.to_header_value().expect("encode leaf");
let parsed_leaf = SignedCredential::from_header_value(&leaf_header).expect("parse leaf");
let rebuilt = CertChain::new(parsed_leaf, parsed_inters);
let verified = rebuilt
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.expect("rebuilt chain verifies");
assert_eq!(verified.subject_agent_id, "region/nyc/node-1");
}
#[test]
fn direct_chain_emits_no_intermediates_header() {
let root = signing_key(60);
let node = signing_key(61);
let leaf = mint(
&root,
"root",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let chain = CertChain::direct(leaf);
assert!(
chain
.intermediates_header_value()
.expect("encode")
.is_none(),
"a one-level chain must emit no chain header"
);
}
#[test]
fn malformed_chain_header_is_rejected() {
assert_eq!(
CertChain::intermediates_from_header_value("not-a-prefix").unwrap_err(),
CredentialError::Malformed
);
assert_eq!(
CertChain::intermediates_from_header_value("v1=@@notbase64@@").unwrap_err(),
CredentialError::Malformed
);
}
fn scratch_dir() -> std::path::PathBuf {
let mut dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.push(".local-runs");
dir.push("test-tmp");
std::fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
fn unique_chain_path(label: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
scratch_dir().join(format!("chain-{label}-{nanos}.chain"))
}
#[test]
fn free_encoder_matches_the_chain_method() {
let (_bundle, inter, leaf) = two_level_setup();
let chain = CertChain::new(leaf, vec![inter.clone()]);
assert_eq!(
intermediates_to_header_value(&[inter]).expect("free encode"),
chain.intermediates_header_value().expect("method encode"),
);
assert!(
intermediates_to_header_value(&[])
.expect("empty encode")
.is_none(),
"an empty intermediates list emits no header"
);
}
#[test]
fn load_intermediates_from_path_round_trips() {
let (bundle, inter, leaf) = two_level_setup();
let header = intermediates_to_header_value(std::slice::from_ref(&inter))
.expect("encode")
.expect("two-level emits a header");
let path = unique_chain_path("roundtrip");
std::fs::write(&path, format!("{header}\n")).expect("write chain file");
let loaded = load_intermediates_from_path(&path).expect("io ok");
let rebuilt = CertChain::new(leaf, loaded);
let verified = rebuilt
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.expect("rebuilt chain verifies");
assert_eq!(verified.subject_agent_id, "region/nyc/node-1");
let _ = std::fs::remove_file(&path);
}
#[test]
fn load_intermediates_from_path_missing_file_is_empty() {
let path = unique_chain_path("missing");
assert!(
load_intermediates_from_path(&path)
.expect("missing file is not an error")
.is_empty()
);
}
#[test]
fn load_intermediates_from_path_malformed_is_invalid_data() {
let path = unique_chain_path("garbage");
std::fs::write(&path, "not-a-chain-header").expect("write");
let err = load_intermediates_from_path(&path).expect_err("malformed must error");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
let _ = std::fs::remove_file(&path);
}
#[test]
fn one_level_direct_chain_still_verifies() {
let root = signing_key(10);
let node = signing_key(11);
let leaf = mint(
&root,
"root",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
let verified = CertChain::direct(leaf)
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.expect("direct chain verifies");
assert_eq!(verified.subject_agent_id, "region/nyc/node-1");
}
#[test]
fn chain_deeper_than_max_is_rejected() {
let (bundle, inter, leaf) = two_level_setup();
let chain = CertChain::new(leaf, vec![inter.clone(), inter]);
let err = chain
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::ChainTooDeep { depth: 3, max: 2 });
assert_eq!(err.tag(), "chain_too_deep");
}
#[test]
fn name_mismatch_between_levels_is_rejected() {
let root = signing_key(20);
let intermediate = signing_key(21);
let node = signing_key(22);
let inter_cert = mint(
&root,
"root",
"region/nyc/ca",
&intermediate.verifying_key(),
DOMAIN,
NOW + 7200,
);
let leaf = mint(
&intermediate,
"region/sfo/ca",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
let err = CertChain::new(leaf, vec![inter_cert])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::NameMismatch);
}
#[test]
fn intermediate_minting_out_of_namespace_leaf_is_rejected() {
let ca_id = "region/nyc/ca";
let foreign_subject = "region/sfo/node-1";
let expected_ns = ca_id.strip_suffix(CA_MARKER_SUFFIX).unwrap();
let root = signing_key(50);
let intermediate = signing_key(51);
let node = signing_key(52);
let inter_cert = mint(
&root,
"root",
ca_id,
&intermediate.verifying_key(),
DOMAIN,
NOW + 7200,
);
let leaf = mint(
&intermediate,
ca_id,
foreign_subject,
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
let err = CertChain::new(leaf, vec![inter_cert])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(
err,
ChainError::DelegationOutOfNamespace {
subject: foreign_subject.to_string(),
delegated_namespace: expected_ns.to_string(),
}
);
}
#[test]
fn intermediate_minting_in_namespace_leaf_is_accepted() {
let (bundle, inter_cert, leaf) = two_level_setup(); let verified = CertChain::new(leaf, vec![inter_cert])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.expect("in-namespace leaf must verify");
assert_eq!(verified.subject_agent_id, "region/nyc/node-1");
}
#[test]
fn delegated_namespace_of_strips_ca_marker_else_self() {
let ca_id = format!("region/nyc{CA_MARKER_SUFFIX}");
assert_eq!(delegated_namespace_of(&ca_id), "region/nyc");
assert_eq!(delegated_namespace_of("region/nyc"), "region/nyc");
let ns = delegated_namespace_of(&ca_id);
assert!(subject_in_delegated_namespace("region/nyc/node-1", ns));
assert!(!subject_in_delegated_namespace("region/nyceast/node-2", ns));
assert!(!subject_in_delegated_namespace("region/sfo/node-1", ns));
}
#[test]
fn delegation_violation_has_stable_tag() {
let err = ChainError::DelegationOutOfNamespace {
subject: "region/sfo/node-1".to_string(),
delegated_namespace: "region/nyc".to_string(),
};
assert_eq!(err.tag(), "chain_delegation_out_of_namespace");
}
#[test]
fn domain_mismatch_between_levels_is_rejected() {
let root = signing_key(30);
let intermediate = signing_key(31);
let node = signing_key(32);
let inter_cert = mint(
&root,
"root",
"region/nyc/ca",
&intermediate.verifying_key(),
DOMAIN,
NOW + 7200,
);
let leaf = mint(
&intermediate,
"region/nyc/ca",
"region/nyc/node-1",
&node.verifying_key(),
"other.tenant",
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
let err = CertChain::new(leaf, vec![inter_cert])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::DomainMismatch);
}
#[test]
fn rogue_intermediate_not_signed_by_root_is_rejected() {
let root = signing_key(40);
let attacker = signing_key(41);
let node = signing_key(42);
let rogue_inter = mint(
&attacker,
"root",
"region/nyc/ca",
&attacker.verifying_key(),
DOMAIN,
NOW + 7200,
);
let leaf = mint(
&attacker,
"region/nyc/ca",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
let err = CertChain::new(leaf, vec![rogue_inter])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::Link(CredentialError::BadSignature));
}
#[test]
fn leaf_signed_by_wrong_key_is_bad_signature() {
let (bundle, inter, _leaf) = two_level_setup();
let imposter = signing_key(99);
let node = signing_key(98);
let forged_leaf = mint(
&imposter,
"region/nyc/ca",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let err = CertChain::new(forged_leaf, vec![inter])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::Link(CredentialError::BadSignature));
}
#[test]
fn expired_intermediate_propagates_window_error() {
let root = signing_key(50);
let intermediate = signing_key(51);
let node = signing_key(52);
let inter_cert = mint(
&root,
"root",
"region/nyc/ca",
&intermediate.verifying_key(),
DOMAIN,
NOW - 1,
);
let leaf = mint(
&intermediate,
"region/nyc/ca",
"region/nyc/node-1",
&node.verifying_key(),
DOMAIN,
NOW + 3600,
);
let bundle = TrustBundle::new().with_issuer("root", root.verifying_key());
let err = CertChain::new(leaf, vec![inter_cert])
.verify(&bundle, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::Link(CredentialError::Expired));
}
#[test]
fn anchor_issuer_not_in_bundle_is_unknown_issuer() {
let (_bundle, inter, leaf) = two_level_setup();
let other_root = signing_key(60);
let empty_for_root =
TrustBundle::new().with_issuer("other-root", other_root.verifying_key());
let err = CertChain::new(leaf, vec![inter])
.verify(&empty_for_root, NOW, DEFAULT_MAX_CHAIN_DEPTH)
.unwrap_err();
assert_eq!(err, ChainError::Link(CredentialError::UnknownIssuer));
}
#[test]
fn delegated_namespace_accepts_child_and_self_rejects_sibling() {
assert!(subject_in_delegated_namespace(
"region/nyc/node-1",
"region/nyc"
));
assert!(subject_in_delegated_namespace("region/nyc", "region/nyc"));
assert!(!subject_in_delegated_namespace(
"region/sfo/node-9",
"region/nyc"
));
assert!(!subject_in_delegated_namespace(
"region/nyceast/node-2",
"region/nyc"
));
assert!(subject_in_delegated_namespace("anything/at/all", ""));
}
}