extern crate alloc;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use zerodds_security_pki::{DelegationChain, SignatureAlgorithm};
use crate::topic_match::topic_match;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TrustPolicy {
GatewayOnly,
DirectOrDelegated,
Federation,
StrictDelegated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustAnchor {
pub subject_guid: [u8; 16],
pub verify_public_key: Vec<u8>,
pub algorithm: SignatureAlgorithm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DelegationProfile {
pub name: String,
pub trust_policy: TrustPolicy,
pub trust_anchors: Vec<TrustAnchor>,
pub max_chain_depth: usize,
pub allowed_algorithms: BTreeSet<u8>, pub require_ocsp: bool,
}
impl DelegationProfile {
#[must_use]
pub fn default_with_anchor(name: String, anchor: TrustAnchor) -> Self {
let mut algos = BTreeSet::new();
for a in [
SignatureAlgorithm::EcdsaP256,
SignatureAlgorithm::EcdsaP384,
SignatureAlgorithm::RsaPss2048,
SignatureAlgorithm::Ed25519,
] {
algos.insert(a.wire_id());
}
Self {
name,
trust_policy: TrustPolicy::DirectOrDelegated,
trust_anchors: alloc::vec![anchor],
max_chain_depth: 3,
allowed_algorithms: algos,
require_ocsp: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DelegationCheckError {
EmptyChain,
ChainBroken {
index: usize,
},
OriginMismatch,
UntrustedDelegator,
SignatureInvalid {
index: usize,
reason: String,
},
LinkExpired {
index: usize,
now: i64,
not_before: i64,
not_after: i64,
},
ChainTooDeep {
depth: usize,
max: usize,
},
AlgorithmRejected {
index: usize,
algorithm: u8,
},
NoTrustAnchor,
AnchorAlgorithmMismatch,
}
impl core::fmt::Display for DelegationCheckError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::EmptyChain => write!(f, "delegation chain is empty"),
Self::ChainBroken { index } => write!(f, "chain broken at link {index}"),
Self::OriginMismatch => write!(f, "origin_guid != links[0].delegator_guid"),
Self::UntrustedDelegator => write!(f, "origin delegator not in trust anchors"),
Self::SignatureInvalid { index, reason } => {
write!(f, "link {index} signature invalid: {reason}")
}
Self::LinkExpired {
index,
now,
not_before,
not_after,
} => write!(
f,
"link {index} expired (now={now}, window=[{not_before}, {not_after}])"
),
Self::ChainTooDeep { depth, max } => write!(f, "chain depth {depth} > max {max}"),
Self::AlgorithmRejected { index, algorithm } => {
write!(f, "link {index} algorithm {algorithm} rejected by profile")
}
Self::NoTrustAnchor => write!(f, "profile has no trust anchors"),
Self::AnchorAlgorithmMismatch => {
write!(f, "trust anchor algorithm differs from initial link")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DelegationCheckError {}
pub type DelegationCheckResult<T> = Result<T, DelegationCheckError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidatedChain {
pub origin_guid: [u8; 16],
pub edge_guid: [u8; 16],
pub chain_depth: usize,
pub effective_topic_patterns: Vec<String>,
pub effective_partition_patterns: Vec<String>,
}
impl ValidatedChain {
#[must_use]
pub fn allows_topic(&self, topic_name: &str) -> bool {
if self.effective_topic_patterns.is_empty() {
return false;
}
self.effective_topic_patterns
.iter()
.any(|p| topic_match(p, topic_name))
}
#[must_use]
pub fn allows_partition(&self, partition_name: &str) -> bool {
if self.effective_partition_patterns.is_empty() {
return partition_name.is_empty();
}
self.effective_partition_patterns
.iter()
.any(|p| topic_match(p, partition_name))
}
}
pub fn validate_chain<F>(
chain: &DelegationChain,
profile: &DelegationProfile,
now: i64,
pubkey_resolver: F,
) -> DelegationCheckResult<ValidatedChain>
where
F: Fn(&[u8; 16]) -> Option<(Vec<u8>, SignatureAlgorithm)>,
{
if chain.links.is_empty() {
return Err(DelegationCheckError::EmptyChain);
}
if profile.trust_anchors.is_empty() {
return Err(DelegationCheckError::NoTrustAnchor);
}
if chain.depth() > profile.max_chain_depth {
return Err(DelegationCheckError::ChainTooDeep {
depth: chain.depth(),
max: profile.max_chain_depth,
});
}
if chain.origin_guid != chain.links[0].delegator_guid {
return Err(DelegationCheckError::OriginMismatch);
}
for i in 0..chain.links.len() - 1 {
if chain.links[i].delegatee_guid != chain.links[i + 1].delegator_guid {
return Err(DelegationCheckError::ChainBroken { index: i });
}
}
let initial = &chain.links[0];
let anchor = match profile.trust_policy {
TrustPolicy::GatewayOnly => {
if profile.trust_anchors.len() != 1 {
return Err(DelegationCheckError::AnchorAlgorithmMismatch);
}
let a = &profile.trust_anchors[0];
if a.subject_guid != initial.delegator_guid {
return Err(DelegationCheckError::UntrustedDelegator);
}
a
}
TrustPolicy::Federation | TrustPolicy::DirectOrDelegated | TrustPolicy::StrictDelegated => {
profile
.trust_anchors
.iter()
.find(|a| a.subject_guid == initial.delegator_guid)
.ok_or(DelegationCheckError::UntrustedDelegator)?
}
};
for (idx, link) in chain.links.iter().enumerate() {
if !profile
.allowed_algorithms
.contains(&link.algorithm.wire_id())
{
return Err(DelegationCheckError::AlgorithmRejected {
index: idx,
algorithm: link.algorithm.wire_id(),
});
}
if now < link.not_before || now > link.not_after {
return Err(DelegationCheckError::LinkExpired {
index: idx,
now,
not_before: link.not_before,
not_after: link.not_after,
});
}
let (verify_pk, expected_algo) = if idx == 0 {
(anchor.verify_public_key.clone(), anchor.algorithm)
} else {
pubkey_resolver(&link.delegator_guid).ok_or_else(|| {
DelegationCheckError::SignatureInvalid {
index: idx,
reason: alloc::format!("no public key for delegator {:?}", link.delegator_guid),
}
})?
};
if idx == 0 && expected_algo != link.algorithm {
return Err(DelegationCheckError::AnchorAlgorithmMismatch);
}
link.verify(&verify_pk)
.map_err(|e| DelegationCheckError::SignatureInvalid {
index: idx,
reason: alloc::format!("{e}"),
})?;
}
let mut effective_topics = chain.links[0].allowed_topic_patterns.clone();
let mut effective_parts = chain.links[0].allowed_partition_patterns.clone();
for link in chain.links.iter().skip(1) {
effective_topics = scope_intersect(&effective_topics, &link.allowed_topic_patterns);
effective_parts = scope_intersect(&effective_parts, &link.allowed_partition_patterns);
}
let edge_guid = chain
.edge_guid()
.unwrap_or(chain.links[chain.links.len() - 1].delegatee_guid);
Ok(ValidatedChain {
origin_guid: chain.origin_guid,
edge_guid,
chain_depth: chain.depth(),
effective_topic_patterns: effective_topics,
effective_partition_patterns: effective_parts,
})
}
#[must_use]
pub fn scope_intersect(a: &[String], b: &[String]) -> Vec<String> {
if a.is_empty() {
return b.to_vec();
}
if b.is_empty() {
return a.to_vec();
}
if a.iter().any(|p| p == "*") {
return b.to_vec();
}
if b.iter().any(|p| p == "*") {
return a.to_vec();
}
let mut out: Vec<String> = Vec::new();
for pa in a {
let pa_in_b = b.iter().any(|pb| topic_match(pb, pa));
if pa_in_b && !out.contains(pa) {
out.push(pa.clone());
}
}
for pb in b {
let pb_in_a = a.iter().any(|pa| topic_match(pa, pb));
if pb_in_a && !out.contains(pb) {
out.push(pb.clone());
}
}
out
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use alloc::string::ToString;
use ring::rand::SystemRandom;
use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, KeyPair};
use zerodds_security_pki::DelegationLink;
fn ecdsa_keys() -> (Vec<u8>, Vec<u8>) {
let rng = SystemRandom::new();
let pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).expect("gen");
let pkcs8_vec = pkcs8.as_ref().to_vec();
let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &pkcs8_vec, &rng)
.expect("parse");
(pkcs8_vec, key.public_key().as_ref().to_vec())
}
fn make_link(
delegator: [u8; 16],
delegatee: [u8; 16],
topics: &[&str],
signing_pkcs8: &[u8],
) -> DelegationLink {
let mut l = DelegationLink::new(
delegator,
delegatee,
topics.iter().map(|s| s.to_string()).collect(),
alloc::vec![],
1_000,
2_000,
SignatureAlgorithm::EcdsaP256,
)
.expect("new link");
l.sign(signing_pkcs8).expect("sign");
l
}
fn profile_with(
anchor: TrustAnchor,
policy: TrustPolicy,
max_depth: usize,
) -> DelegationProfile {
let mut algos = BTreeSet::new();
algos.insert(SignatureAlgorithm::EcdsaP256.wire_id());
algos.insert(SignatureAlgorithm::EcdsaP384.wire_id());
algos.insert(SignatureAlgorithm::Ed25519.wire_id());
DelegationProfile {
name: "test".to_string(),
trust_policy: policy,
trust_anchors: alloc::vec![anchor],
max_chain_depth: max_depth,
allowed_algorithms: algos,
require_ocsp: false,
}
}
#[test]
fn one_hop_chain_validates() {
let (sk, pk) = ecdsa_keys();
let gateway = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gateway, edge, &["sensor/*"], &sk);
let chain = DelegationChain::new(gateway, alloc::vec![link]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gateway,
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
let validated = validate_chain(&chain, &profile, 1_500, |_| None).expect("validate");
assert_eq!(validated.origin_guid, gateway);
assert_eq!(validated.edge_guid, edge);
assert_eq!(validated.chain_depth, 1);
assert_eq!(
validated.effective_topic_patterns,
alloc::vec!["sensor/*".to_string()]
);
}
#[test]
fn empty_chain_rejects() {
let (_, pk) = ecdsa_keys();
let anchor = TrustAnchor {
subject_guid: [0; 16],
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
let chain = DelegationChain {
origin_guid: [0; 16],
links: alloc::vec![],
};
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(err, DelegationCheckError::EmptyChain));
}
#[test]
fn chain_too_deep_rejects() {
let (sk, pk) = ecdsa_keys();
let gw = [0xAA; 16];
let mid = [0xCC; 16];
let edge = [0xBB; 16];
let l1 = make_link(gw, mid, &["sensor/*"], &sk);
let l2 = make_link(mid, edge, &["sensor/lidar"], &sk); let chain = DelegationChain::new(gw, alloc::vec![l1, l2]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let mut profile = profile_with(anchor, TrustPolicy::GatewayOnly, 1);
profile.max_chain_depth = 1;
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(
err,
DelegationCheckError::ChainTooDeep { depth: 2, max: 1 }
));
}
#[test]
fn origin_mismatch_rejects() {
let (sk, pk) = ecdsa_keys();
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gw, edge, &["sensor/*"], &sk);
let chain = DelegationChain {
origin_guid: [0xFF; 16],
links: alloc::vec![link],
};
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(err, DelegationCheckError::OriginMismatch));
}
#[test]
fn untrusted_delegator_rejects() {
let (sk, _pk_sk) = ecdsa_keys();
let (_sk2, pk_anchor) = ecdsa_keys(); let gw = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gw, edge, &["sensor/*"], &sk);
let chain = DelegationChain::new(gw, alloc::vec![link]).expect("chain");
let anchor = TrustAnchor {
subject_guid: [0x99; 16], verify_public_key: pk_anchor,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(err, DelegationCheckError::UntrustedDelegator));
}
#[test]
fn link_expired_rejects() {
let (sk, pk) = ecdsa_keys();
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gw, edge, &["sensor/*"], &sk);
let chain = DelegationChain::new(gw, alloc::vec![link]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
let err = validate_chain(&chain, &profile, 5_000, |_| None).expect_err("must fail");
assert!(matches!(err, DelegationCheckError::LinkExpired { .. }));
}
#[test]
fn algorithm_rejected_by_profile() {
let (sk, pk) = ecdsa_keys();
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gw, edge, &["sensor/*"], &sk);
let chain = DelegationChain::new(gw, alloc::vec![link]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let mut profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
profile.allowed_algorithms.clear();
profile
.allowed_algorithms
.insert(SignatureAlgorithm::Ed25519.wire_id());
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(
err,
DelegationCheckError::AlgorithmRejected { .. }
));
}
#[test]
fn signature_invalid_rejects() {
let (sk, _pk_sk) = ecdsa_keys();
let (_sk2, pk_anchor_other) = ecdsa_keys();
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gw, edge, &["sensor/*"], &sk);
let chain = DelegationChain::new(gw, alloc::vec![link]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk_anchor_other,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::GatewayOnly, 3);
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(
err,
DelegationCheckError::SignatureInvalid { index: 0, .. }
));
}
#[test]
fn two_hop_chain_via_resolver() {
let rng = SystemRandom::new();
let pkcs8_gw =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).expect("gw");
let sk_gw = pkcs8_gw.as_ref().to_vec();
let pk_gw = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &sk_gw, &rng)
.expect("parse")
.public_key()
.as_ref()
.to_vec();
let pkcs8_mid =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).expect("mid");
let sk_mid = pkcs8_mid.as_ref().to_vec();
let pk_mid = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &sk_mid, &rng)
.expect("parse")
.public_key()
.as_ref()
.to_vec();
let gw = [0xAA; 16];
let mid = [0xCC; 16];
let edge = [0xBB; 16];
let l1 = make_link(gw, mid, &["sensor/*"], &sk_gw);
let l2 = make_link(mid, edge, &["sensor/lidar"], &sk_mid);
let chain = DelegationChain::new(gw, alloc::vec![l1, l2]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk_gw,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::DirectOrDelegated, 3);
let resolver = |g: &[u8; 16]| -> Option<(Vec<u8>, SignatureAlgorithm)> {
if g == &mid {
Some((pk_mid.clone(), SignatureAlgorithm::EcdsaP256))
} else {
None
}
};
let validated = validate_chain(&chain, &profile, 1_500, resolver).expect("validate");
assert_eq!(validated.chain_depth, 2);
assert_eq!(validated.edge_guid, edge);
assert!(
validated
.effective_topic_patterns
.contains(&"sensor/lidar".to_string())
);
}
#[test]
fn chain_broken_rejects() {
let (sk, pk) = ecdsa_keys();
let gw = [0xAA; 16];
let mid = [0xCC; 16];
let edge = [0xBB; 16];
let l1 = make_link(gw, mid, &["sensor/*"], &sk);
let l2 = make_link([0xDD; 16], edge, &["sensor/lidar"], &sk);
let chain = DelegationChain::new(gw, alloc::vec![l1, l2]).expect("chain");
let anchor = TrustAnchor {
subject_guid: gw,
verify_public_key: pk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let profile = profile_with(anchor, TrustPolicy::DirectOrDelegated, 3);
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(
err,
DelegationCheckError::ChainBroken { index: 0 }
));
}
#[test]
fn federation_finds_anchor_in_list() {
let (sk1, pk1) = ecdsa_keys();
let (_sk2, pk2) = ecdsa_keys();
let gw1 = [0x11; 16];
let gw2 = [0x22; 16];
let edge = [0xBB; 16];
let link = make_link(gw2, edge, &["sensor/*"], &sk1); let chain = DelegationChain::new(gw2, alloc::vec![link]).expect("chain");
let mut profile = profile_with(
TrustAnchor {
subject_guid: gw1,
verify_public_key: pk1.clone(),
algorithm: SignatureAlgorithm::EcdsaP256,
},
TrustPolicy::Federation,
3,
);
profile.trust_anchors.push(TrustAnchor {
subject_guid: gw2,
verify_public_key: pk2,
algorithm: SignatureAlgorithm::EcdsaP256,
});
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(err, DelegationCheckError::SignatureInvalid { .. }));
profile.trust_anchors[1].verify_public_key = pk1;
let validated = validate_chain(&chain, &profile, 1_500, |_| None).expect("validate");
assert_eq!(validated.origin_guid, gw2);
}
#[test]
fn no_trust_anchor_rejects() {
let (sk, _) = ecdsa_keys();
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let link = make_link(gw, edge, &["sensor/*"], &sk);
let chain = DelegationChain::new(gw, alloc::vec![link]).expect("chain");
let mut algos = BTreeSet::new();
algos.insert(SignatureAlgorithm::EcdsaP256.wire_id());
let profile = DelegationProfile {
name: "no-anchor".to_string(),
trust_policy: TrustPolicy::DirectOrDelegated,
trust_anchors: alloc::vec![],
max_chain_depth: 3,
allowed_algorithms: algos,
require_ocsp: false,
};
let err = validate_chain(&chain, &profile, 1_500, |_| None).expect_err("must fail");
assert!(matches!(err, DelegationCheckError::NoTrustAnchor));
}
#[test]
fn validated_chain_topic_match() {
let v = ValidatedChain {
origin_guid: [0; 16],
edge_guid: [0; 16],
chain_depth: 1,
effective_topic_patterns: alloc::vec!["sensor/*".to_string()],
effective_partition_patterns: alloc::vec![],
};
assert!(v.allows_topic("sensor/lidar"));
assert!(!v.allows_topic("actuator/x"));
assert!(v.allows_partition(""));
assert!(!v.allows_partition("public"));
}
#[test]
fn validated_chain_partition_match_with_patterns() {
let v = ValidatedChain {
origin_guid: [0; 16],
edge_guid: [0; 16],
chain_depth: 1,
effective_topic_patterns: alloc::vec!["*".to_string()],
effective_partition_patterns: alloc::vec!["pub_*".to_string()],
};
assert!(v.allows_partition("pub_alpha"));
assert!(!v.allows_partition("priv_x"));
}
#[test]
fn scope_intersect_empty_treats_as_allow_all() {
let a: Vec<String> = alloc::vec![];
let b: Vec<String> = alloc::vec!["sensor/*".to_string()];
assert_eq!(scope_intersect(&a, &b), alloc::vec!["sensor/*".to_string()]);
}
#[test]
fn scope_intersect_star_is_neutral() {
let a = alloc::vec!["*".to_string()];
let b = alloc::vec!["sensor/lidar".to_string(), "sensor/cam".to_string()];
assert_eq!(scope_intersect(&a, &b), b);
}
#[test]
fn scope_intersect_narrows() {
let a = alloc::vec!["sensor/*".to_string()];
let b = alloc::vec!["sensor/lidar".to_string()];
let isec = scope_intersect(&a, &b);
assert!(isec.contains(&"sensor/lidar".to_string()));
}
#[test]
fn scope_intersect_disjoint() {
let a = alloc::vec!["sensor/*".to_string()];
let b = alloc::vec!["actuator/*".to_string()];
let isec = scope_intersect(&a, &b);
assert!(isec.is_empty());
}
#[test]
fn ed25519_default_anchor_constructor() {
let pk = alloc::vec![0u8; 32];
let anchor = TrustAnchor {
subject_guid: [1; 16],
verify_public_key: pk,
algorithm: SignatureAlgorithm::Ed25519,
};
let profile = DelegationProfile::default_with_anchor("default".to_string(), anchor);
assert_eq!(profile.max_chain_depth, 3);
assert!(
profile
.allowed_algorithms
.contains(&SignatureAlgorithm::Ed25519.wire_id())
);
assert!(matches!(
profile.trust_policy,
TrustPolicy::DirectOrDelegated
));
}
}