extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use zerodds_security_permissions::EdgeIdentityConfig;
use zerodds_security_pki::{DelegationChain, DelegationError, DelegationLink, SignatureAlgorithm};
#[derive(Debug, Clone)]
pub struct GatewayBridgeConfig {
pub gateway_guid: [u8; 16],
pub signing_key: Vec<u8>,
pub algorithm: SignatureAlgorithm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum GatewayBridgeError {
UnknownEdge {
edge_guid: [u8; 16],
},
DelegationFailed(DelegationError),
}
impl core::fmt::Display for GatewayBridgeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownEdge { edge_guid } => {
write!(f, "no active delegation for edge {edge_guid:?}")
}
Self::DelegationFailed(e) => write!(f, "delegation failed: {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for GatewayBridgeError {}
impl From<DelegationError> for GatewayBridgeError {
fn from(e: DelegationError) -> Self {
Self::DelegationFailed(e)
}
}
pub type GatewayBridgeResult<T> = Result<T, GatewayBridgeError>;
#[derive(Debug, Clone)]
pub struct GatewayBridge {
config: GatewayBridgeConfig,
upstream: Option<DelegationChain>,
active: BTreeMap<[u8; 16], DelegationLink>,
revocations: Vec<[u8; 16]>,
}
impl GatewayBridge {
#[must_use]
pub fn new(config: GatewayBridgeConfig) -> Self {
Self {
config,
upstream: None,
active: BTreeMap::new(),
revocations: Vec::new(),
}
}
pub fn with_upstream(&mut self, upstream_chain: DelegationChain) {
self.upstream = Some(upstream_chain);
}
#[must_use]
pub fn gateway_guid(&self) -> [u8; 16] {
self.config.gateway_guid
}
pub fn delegate_for(
&mut self,
edge_guid: [u8; 16],
topic_patterns: Vec<String>,
partition_patterns: Vec<String>,
not_before: i64,
not_after: i64,
) -> GatewayBridgeResult<&DelegationLink> {
let mut link = DelegationLink::new(
self.config.gateway_guid,
edge_guid,
topic_patterns,
partition_patterns,
not_before,
not_after,
self.config.algorithm,
)?;
link.sign(&self.config.signing_key)?;
self.active.insert(edge_guid, link);
self.revocations.retain(|g| g != &edge_guid);
self.active
.get(&edge_guid)
.ok_or(GatewayBridgeError::UnknownEdge { edge_guid })
}
pub fn revoke_delegation(&mut self, edge_guid: [u8; 16]) -> GatewayBridgeResult<()> {
if self.active.remove(&edge_guid).is_some() {
if !self.revocations.contains(&edge_guid) {
self.revocations.push(edge_guid);
}
Ok(())
} else {
Err(GatewayBridgeError::UnknownEdge { edge_guid })
}
}
#[must_use]
pub fn chain_for(&self, edge_guid: &[u8; 16]) -> Option<DelegationChain> {
let edge_link = self.active.get(edge_guid)?.clone();
match &self.upstream {
None => DelegationChain::new(self.config.gateway_guid, alloc::vec![edge_link]).ok(),
Some(up) => {
let mut links = up.links.clone();
links.push(edge_link);
DelegationChain::new(up.origin_guid, links).ok()
}
}
}
#[must_use]
pub fn active_count(&self) -> usize {
self.active.len()
}
#[must_use]
pub fn has_edge(&self, edge_guid: &[u8; 16]) -> bool {
self.active.contains_key(edge_guid)
}
pub fn iter_active(&self) -> impl Iterator<Item = (&[u8; 16], &DelegationLink)> {
self.active.iter()
}
pub fn take_revocations(&mut self) -> Vec<[u8; 16]> {
core::mem::take(&mut self.revocations)
}
#[must_use]
pub fn upstream(&self) -> Option<&DelegationChain> {
self.upstream.as_ref()
}
pub fn rotate_ephemerals<F>(
&mut self,
identities: &[EdgeIdentityConfig],
now: i64,
topic_patterns: Vec<String>,
partition_patterns: Vec<String>,
mut prefix_generator: F,
) -> (Vec<String>, Vec<(String, GatewayBridgeError)>)
where
F: FnMut(&str) -> [u8; 12],
{
let mut rotated = Vec::new();
let mut failed = Vec::new();
for cfg in identities.iter().filter(|c| c.is_ephemeral()) {
let new_prefix = prefix_generator(&cfg.name);
let mut edge_guid = [0u8; 16];
edge_guid[..12].copy_from_slice(&new_prefix);
edge_guid[12..].copy_from_slice(&[0x00, 0x00, 0x01, 0xC1]);
let lifetime = i64::from(cfg.effective_lifetime());
let new_not_after = now.saturating_add(lifetime);
match self.delegate_for(
edge_guid,
topic_patterns.clone(),
partition_patterns.clone(),
now,
new_not_after,
) {
Ok(_) => rotated.push(cfg.name.clone()),
Err(e) => failed.push((cfg.name.clone(), e)),
}
}
(rotated, failed)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
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_permissions::EdgeIdentityMode;
fn ecdsa_p256_keypair() -> (Vec<u8>, Vec<u8>) {
let rng = SystemRandom::new();
let pkcs8 =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).expect("gen");
let sk = pkcs8.as_ref().to_vec();
let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &sk, &rng).expect("p");
(sk, kp.public_key().as_ref().to_vec())
}
fn make_bridge(gw_guid: [u8; 16]) -> (GatewayBridge, Vec<u8>) {
let (sk, pk) = ecdsa_p256_keypair();
let cfg = GatewayBridgeConfig {
gateway_guid: gw_guid,
signing_key: sk,
algorithm: SignatureAlgorithm::EcdsaP256,
};
(GatewayBridge::new(cfg), pk)
}
#[test]
fn delegate_for_creates_signed_link() {
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let (mut bridge, pk) = make_bridge(gw);
let link = bridge
.delegate_for(
edge,
alloc::vec!["sensor/*".to_string()],
alloc::vec![],
1_000,
9_000,
)
.expect("delegate")
.clone();
assert_eq!(link.delegator_guid, gw);
assert_eq!(link.delegatee_guid, edge);
assert_eq!(link.signature.len(), 64); link.verify(&pk).expect("verify");
assert_eq!(bridge.active_count(), 1);
assert!(bridge.has_edge(&edge));
}
#[test]
fn one_hop_chain_for_edge() {
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let (mut bridge, _pk) = make_bridge(gw);
bridge
.delegate_for(
edge,
alloc::vec!["sensor/*".to_string()],
alloc::vec![],
0,
9_000,
)
.expect("delegate");
let chain = bridge.chain_for(&edge).expect("chain");
assert_eq!(chain.depth(), 1);
assert_eq!(chain.origin_guid, gw);
assert_eq!(chain.edge_guid(), Some(edge));
}
#[test]
fn chain_for_missing_edge_is_none() {
let gw = [0xAA; 16];
let (bridge, _) = make_bridge(gw);
assert!(bridge.chain_for(&[0xCC; 16]).is_none());
}
#[test]
fn revoke_delegation_removes_active_and_records_revocation() {
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let (mut bridge, _pk) = make_bridge(gw);
bridge
.delegate_for(edge, alloc::vec![], alloc::vec![], 0, 9_000)
.expect("delegate");
bridge.revoke_delegation(edge).expect("revoke");
assert_eq!(bridge.active_count(), 0);
assert!(!bridge.has_edge(&edge));
let revocations = bridge.take_revocations();
assert_eq!(revocations, alloc::vec![edge]);
assert!(bridge.take_revocations().is_empty());
}
#[test]
fn revoke_unknown_edge_is_error() {
let gw = [0xAA; 16];
let (mut bridge, _) = make_bridge(gw);
let err = bridge.revoke_delegation([0xFF; 16]).expect_err("must fail");
assert!(matches!(err, GatewayBridgeError::UnknownEdge { .. }));
}
#[test]
fn re_delegate_clears_pending_revocation() {
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let (mut bridge, _) = make_bridge(gw);
bridge
.delegate_for(edge, alloc::vec![], alloc::vec![], 0, 9_000)
.expect("delegate");
bridge.revoke_delegation(edge).expect("revoke");
bridge
.delegate_for(edge, alloc::vec![], alloc::vec![], 100, 10_000)
.expect("redelegate");
assert!(bridge.take_revocations().is_empty());
assert!(bridge.has_edge(&edge));
}
#[test]
fn sub_gateway_chaining_two_hops() {
let gw1 = [0x11; 16];
let gw2 = [0x22; 16];
let edge = [0x33; 16];
let (sk1, _pk1) = ecdsa_p256_keypair();
let mut upstream_link = DelegationLink::new(
gw1,
gw2,
alloc::vec!["*".to_string()],
alloc::vec![],
0,
9_000,
SignatureAlgorithm::EcdsaP256,
)
.expect("upstream link");
upstream_link.sign(&sk1).expect("sign upstream");
let upstream_chain =
DelegationChain::new(gw1, alloc::vec![upstream_link]).expect("upstream chain");
let (mut turm_bridge, _pk2) = make_bridge(gw2);
turm_bridge.with_upstream(upstream_chain);
turm_bridge
.delegate_for(
edge,
alloc::vec!["sensor/imu".to_string()],
alloc::vec![],
100,
8_000,
)
.expect("turm delegate");
let chain = turm_bridge.chain_for(&edge).expect("chain");
assert_eq!(chain.depth(), 2);
assert_eq!(chain.origin_guid, gw1);
assert_eq!(chain.edge_guid(), Some(edge));
assert_eq!(chain.links.last().unwrap().delegator_guid, gw2);
assert_eq!(chain.links.last().unwrap().delegatee_guid, edge);
}
#[test]
fn iter_active_lists_all_delegations() {
let gw = [0xAA; 16];
let (mut bridge, _) = make_bridge(gw);
for i in 0..5u8 {
let mut edge = [0u8; 16];
edge[0] = i;
bridge
.delegate_for(edge, alloc::vec![], alloc::vec![], 0, 9_000)
.expect("delegate");
}
assert_eq!(bridge.active_count(), 5);
let collected: Vec<[u8; 16]> = bridge.iter_active().map(|(g, _)| *g).collect();
assert_eq!(collected.len(), 5);
}
#[test]
fn upstream_accessor_reflects_state() {
let gw = [0xAA; 16];
let (mut bridge, _) = make_bridge(gw);
assert!(bridge.upstream().is_none());
let (sk, _pk) = ecdsa_p256_keypair();
let mut up_link = DelegationLink::new(
[0x11; 16],
gw,
alloc::vec!["*".to_string()],
alloc::vec![],
0,
9_000,
SignatureAlgorithm::EcdsaP256,
)
.unwrap();
up_link.sign(&sk).unwrap();
let chain = DelegationChain::new([0x11; 16], alloc::vec![up_link]).unwrap();
bridge.with_upstream(chain.clone());
assert_eq!(bridge.upstream(), Some(&chain));
}
#[test]
fn bridge_two_hop_chain_validates_via_chain_check() {
use alloc::collections::BTreeSet;
use zerodds_security_permissions::{
DelegationProfile, TrustAnchor, TrustPolicy, validate_chain,
};
let gw1 = [0x11; 16];
let gw2 = [0x22; 16];
let edge = [0x33; 16];
let (sk1, pk1) = ecdsa_p256_keypair();
let (sk2, pk2) = ecdsa_p256_keypair();
let mut upstream_link = DelegationLink::new(
gw1,
gw2,
alloc::vec!["*".to_string()],
alloc::vec![],
0,
9_000,
SignatureAlgorithm::EcdsaP256,
)
.unwrap();
upstream_link.sign(&sk1).unwrap();
let upstream = DelegationChain::new(gw1, alloc::vec![upstream_link]).unwrap();
let cfg = GatewayBridgeConfig {
gateway_guid: gw2,
signing_key: sk2,
algorithm: SignatureAlgorithm::EcdsaP256,
};
let mut turm_bridge = GatewayBridge::new(cfg);
turm_bridge.with_upstream(upstream);
turm_bridge
.delegate_for(
edge,
alloc::vec!["sensor/imu".to_string()],
alloc::vec![],
100,
8_000,
)
.unwrap();
let chain = turm_bridge.chain_for(&edge).expect("chain");
let mut algos = BTreeSet::new();
algos.insert(SignatureAlgorithm::EcdsaP256.wire_id());
let profile = DelegationProfile {
name: "vehicle".to_string(),
trust_policy: TrustPolicy::DirectOrDelegated,
trust_anchors: alloc::vec![TrustAnchor {
subject_guid: gw1,
verify_public_key: pk1,
algorithm: SignatureAlgorithm::EcdsaP256,
}],
max_chain_depth: 3,
allowed_algorithms: algos,
require_ocsp: false,
};
let resolver = move |g: &[u8; 16]| -> Option<(Vec<u8>, SignatureAlgorithm)> {
if g == &gw2 {
Some((pk2.clone(), SignatureAlgorithm::EcdsaP256))
} else {
None
}
};
let validated = validate_chain(&chain, &profile, 5_000, resolver).expect("validate");
assert_eq!(validated.chain_depth, 2);
assert_eq!(validated.edge_guid, edge);
assert!(
validated
.effective_topic_patterns
.contains(&"sensor/imu".to_string())
);
}
#[test]
fn rotate_ephemerals_creates_delegations_for_ephemeral_only() {
let gw = [0xAA; 16];
let (mut bridge, _pk) = make_bridge(gw);
let identities = alloc::vec![
EdgeIdentityConfig {
name: "static-edge".into(),
mode: EdgeIdentityMode::Static,
guid_prefix: Some([0x01; 12]),
lifetime_seconds: None,
},
EdgeIdentityConfig {
name: "ephemeral-edge".into(),
mode: EdgeIdentityMode::Ephemeral,
guid_prefix: None,
lifetime_seconds: Some(60),
},
];
let mut counter = 0u8;
let prefix_gen = |_name: &str| -> [u8; 12] {
counter += 1;
[counter; 12]
};
let (rotated, failed) = bridge.rotate_ephemerals(
&identities,
1_000,
alloc::vec!["sensor/*".to_string()],
alloc::vec![],
prefix_gen,
);
assert_eq!(rotated, alloc::vec!["ephemeral-edge".to_string()]);
assert!(failed.is_empty());
assert_eq!(bridge.active_count(), 1);
}
#[test]
fn rotate_ephemerals_uses_provided_prefix_generator() {
let gw = [0xAA; 16];
let (mut bridge, _) = make_bridge(gw);
let identities = alloc::vec![EdgeIdentityConfig {
name: "rot-edge".into(),
mode: EdgeIdentityMode::Ephemeral,
guid_prefix: None,
lifetime_seconds: Some(120),
}];
let captured_name: alloc::sync::Arc<core::sync::atomic::AtomicBool> =
alloc::sync::Arc::new(core::sync::atomic::AtomicBool::new(false));
let captured_clone = captured_name.clone();
let prefix_gen = move |name: &str| -> [u8; 12] {
if name == "rot-edge" {
captured_clone.store(true, core::sync::atomic::Ordering::SeqCst);
}
[0xDE; 12]
};
let (rotated, _) =
bridge.rotate_ephemerals(&identities, 5_000, alloc::vec![], alloc::vec![], prefix_gen);
assert_eq!(rotated.len(), 1);
assert!(captured_name.load(core::sync::atomic::Ordering::SeqCst));
let mut expected_guid = [0u8; 16];
expected_guid[..12].copy_from_slice(&[0xDE; 12]);
expected_guid[12..].copy_from_slice(&[0x00, 0x00, 0x01, 0xC1]);
assert!(bridge.has_edge(&expected_guid));
}
#[test]
fn rotate_ephemerals_uses_lifetime_for_not_after() {
let gw = [0xAA; 16];
let (mut bridge, _) = make_bridge(gw);
let identities = alloc::vec![EdgeIdentityConfig {
name: "imu".into(),
mode: EdgeIdentityMode::Ephemeral,
guid_prefix: None,
lifetime_seconds: Some(300),
}];
let (_rotated, _) =
bridge.rotate_ephemerals(&identities, 1_000, alloc::vec![], alloc::vec![], |_| {
[0xAB; 12]
});
let mut edge_guid = [0u8; 16];
edge_guid[..12].copy_from_slice(&[0xAB; 12]);
edge_guid[12..].copy_from_slice(&[0x00, 0x00, 0x01, 0xC1]);
let link = bridge.iter_active().find(|(g, _)| *g == &edge_guid);
assert!(link.is_some());
let (_g, l) = link.unwrap();
assert_eq!(l.not_before, 1_000);
assert_eq!(l.not_after, 1_300);
}
#[test]
fn rotate_ephemerals_repeated_calls_replace_old_delegation() {
let gw = [0xAA; 16];
let (mut bridge, _) = make_bridge(gw);
let identities = alloc::vec![EdgeIdentityConfig {
name: "ecu".into(),
mode: EdgeIdentityMode::Ephemeral,
guid_prefix: None,
lifetime_seconds: Some(60),
}];
let (_, _) =
bridge.rotate_ephemerals(&identities, 1_000, alloc::vec![], alloc::vec![], |_| {
[0x11; 12]
});
let (_, _) =
bridge.rotate_ephemerals(&identities, 2_000, alloc::vec![], alloc::vec![], |_| {
[0x22; 12]
});
assert_eq!(bridge.active_count(), 2);
}
#[test]
fn delegation_link_too_many_topics_propagates_as_bridge_error() {
let gw = [0xAA; 16];
let edge = [0xBB; 16];
let (mut bridge, _) = make_bridge(gw);
let topics: Vec<String> = (0..200).map(|i| alloc::format!("t{i}")).collect();
let err = bridge
.delegate_for(edge, topics, alloc::vec![], 0, 9_000)
.expect_err("must fail");
assert!(matches!(err, GatewayBridgeError::DelegationFailed(_)));
}
}