use std::collections::HashMap;
use std::path::Path;
use ed25519_dalek::{Verifier, VerifyingKey};
use tracing::{debug, warn};
use super::error::SecurityError;
use super::keypair::DeviceKeypair;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum MeshTier {
Enterprise = 0,
Regional = 1,
Tactical = 2,
Edge = 3,
}
impl MeshTier {
pub fn from_str_name(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"enterprise" => Some(Self::Enterprise),
"regional" => Some(Self::Regional),
"tactical" => Some(Self::Tactical),
"edge" => Some(Self::Edge),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Enterprise => "Enterprise",
Self::Regional => "Regional",
Self::Tactical => "Tactical",
Self::Edge => "Edge",
}
}
pub fn to_byte(self) -> u8 {
self as u8
}
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(Self::Enterprise),
1 => Some(Self::Regional),
2 => Some(Self::Tactical),
3 => Some(Self::Edge),
_ => None,
}
}
}
impl std::fmt::Display for MeshTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub mod permissions {
pub const RELAY: u8 = 0b0000_0001;
pub const EMERGENCY: u8 = 0b0000_0010;
pub const ENROLL: u8 = 0b0000_0100;
pub const ADMIN: u8 = 0b1000_0000;
pub const STANDARD: u8 = RELAY | EMERGENCY;
pub const AUTHORITY: u8 = RELAY | EMERGENCY | ENROLL | ADMIN;
}
#[derive(Clone, Debug)]
pub struct MeshCertificate {
pub subject_public_key: [u8; 32],
pub mesh_id: String,
pub node_id: String,
pub tier: MeshTier,
pub permissions: u8,
pub issued_at_ms: u64,
pub expires_at_ms: u64,
pub issuer_public_key: [u8; 32],
pub signature: [u8; 64],
}
impl MeshCertificate {
#[allow(clippy::too_many_arguments)]
pub fn new(
subject_public_key: [u8; 32],
mesh_id: String,
node_id: String,
tier: MeshTier,
permissions: u8,
issued_at_ms: u64,
expires_at_ms: u64,
issuer_public_key: [u8; 32],
) -> Self {
Self {
subject_public_key,
mesh_id,
node_id,
tier,
permissions,
issued_at_ms,
expires_at_ms,
issuer_public_key,
signature: [0u8; 64],
}
}
pub fn new_root(
authority: &DeviceKeypair,
mesh_id: String,
node_id: String,
tier: MeshTier,
issued_at_ms: u64,
expires_at_ms: u64,
) -> Self {
let pubkey = authority.public_key_bytes();
let mut cert = Self::new(
pubkey,
mesh_id,
node_id,
tier,
permissions::AUTHORITY,
issued_at_ms,
expires_at_ms,
pubkey,
);
cert.sign_with(authority);
cert
}
pub fn sign_with(&mut self, issuer: &DeviceKeypair) {
let signable = self.signable_bytes();
let sig = issuer.sign(&signable);
self.signature = sig.to_bytes();
}
pub fn signed(mut self, issuer: &DeviceKeypair) -> Self {
self.sign_with(issuer);
self
}
pub fn verify(&self) -> Result<(), SecurityError> {
let vk = VerifyingKey::from_bytes(&self.issuer_public_key)
.map_err(|e| SecurityError::InvalidPublicKey(e.to_string()))?;
let sig = ed25519_dalek::Signature::from_bytes(&self.signature);
let signable = self.signable_bytes();
vk.verify(&signable, &sig)
.map_err(|e| SecurityError::InvalidSignature(e.to_string()))
}
pub fn is_root(&self) -> bool {
self.subject_public_key == self.issuer_public_key
}
pub fn is_valid(&self, now_ms: u64) -> bool {
now_ms >= self.issued_at_ms && (self.expires_at_ms == 0 || now_ms < self.expires_at_ms)
}
pub fn has_permission(&self, perm: u8) -> bool {
self.permissions & perm == perm
}
pub fn time_remaining_ms(&self, now_ms: u64) -> u64 {
if self.expires_at_ms == 0 {
return u64::MAX;
}
self.expires_at_ms.saturating_sub(now_ms)
}
fn signable_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(83 + self.mesh_id.len() + self.node_id.len());
buf.extend_from_slice(&self.subject_public_key);
buf.push(self.mesh_id.len() as u8);
buf.extend_from_slice(self.mesh_id.as_bytes());
buf.push(self.node_id.len() as u8);
buf.extend_from_slice(self.node_id.as_bytes());
buf.push(self.tier.to_byte());
buf.push(self.permissions);
buf.extend_from_slice(&self.issued_at_ms.to_le_bytes());
buf.extend_from_slice(&self.expires_at_ms.to_le_bytes());
buf.extend_from_slice(&self.issuer_public_key);
buf
}
pub fn encode(&self) -> Vec<u8> {
let mut buf = self.signable_bytes();
buf.extend_from_slice(&self.signature);
buf
}
pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
if data.len() < 148 {
return Err(SecurityError::SerializationError(format!(
"certificate too short: {} bytes (min 148)",
data.len()
)));
}
let mut pos = 0;
let mut subject_public_key = [0u8; 32];
subject_public_key.copy_from_slice(&data[pos..pos + 32]);
pos += 32;
let mesh_id_len = data[pos] as usize;
pos += 1;
if pos + mesh_id_len >= data.len() {
return Err(SecurityError::SerializationError(
"certificate truncated at mesh_id".to_string(),
));
}
let mesh_id = String::from_utf8(data[pos..pos + mesh_id_len].to_vec())
.map_err(|e| SecurityError::SerializationError(format!("invalid mesh_id: {e}")))?;
pos += mesh_id_len;
let node_id_len = data[pos] as usize;
pos += 1;
if pos + node_id_len + 1 + 1 + 8 + 8 + 32 + 64 > data.len() {
return Err(SecurityError::SerializationError(
"certificate truncated at node_id".to_string(),
));
}
let node_id = String::from_utf8(data[pos..pos + node_id_len].to_vec())
.map_err(|e| SecurityError::SerializationError(format!("invalid node_id: {e}")))?;
pos += node_id_len;
let tier = MeshTier::from_byte(data[pos])
.ok_or_else(|| SecurityError::SerializationError("invalid tier byte".to_string()))?;
pos += 1;
let permissions = data[pos];
pos += 1;
let issued_at_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
pos += 8;
let expires_at_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
pos += 8;
let mut issuer_public_key = [0u8; 32];
issuer_public_key.copy_from_slice(&data[pos..pos + 32]);
pos += 32;
let mut signature = [0u8; 64];
signature.copy_from_slice(&data[pos..pos + 64]);
Ok(Self {
subject_public_key,
mesh_id,
node_id,
tier,
permissions,
issued_at_ms,
expires_at_ms,
issuer_public_key,
signature,
})
}
}
#[derive(Debug, Default)]
pub struct CertificateBundle {
authorities: Vec<[u8; 32]>,
certificates: HashMap<[u8; 32], MeshCertificate>,
node_id_index: HashMap<String, [u8; 32]>,
}
impl CertificateBundle {
pub fn new() -> Self {
Self::default()
}
pub fn add_authority(&mut self, public_key: [u8; 32]) {
if !self.authorities.contains(&public_key) {
self.authorities.push(public_key);
}
}
pub fn add_certificate(&mut self, cert: MeshCertificate) -> Result<(), SecurityError> {
cert.verify()?;
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
if !cert.is_root() && !self.is_trusted_issuer(&cert.issuer_public_key, now_ms) {
return Err(SecurityError::CertificateError(
"issuer not in trusted authorities and has no ENROLL delegation".to_string(),
));
}
if !cert.node_id.is_empty() {
self.node_id_index
.insert(cert.node_id.clone(), cert.subject_public_key);
}
self.certificates.insert(cert.subject_public_key, cert);
Ok(())
}
fn is_trusted_issuer(&self, issuer_key: &[u8; 32], now_ms: u64) -> bool {
if self.authorities.contains(issuer_key) {
return true;
}
if let Some(issuer_cert) = self.certificates.get(issuer_key) {
if issuer_cert.has_permission(permissions::ENROLL) && issuer_cert.is_valid(now_ms) {
if issuer_cert.verify().is_ok() {
return true;
}
}
}
false
}
pub fn add_certificate_unchecked(&mut self, cert: MeshCertificate) {
if !cert.node_id.is_empty() {
self.node_id_index
.insert(cert.node_id.clone(), cert.subject_public_key);
}
self.certificates.insert(cert.subject_public_key, cert);
}
pub fn validate_peer(&self, peer_public_key: &[u8; 32], now_ms: u64) -> bool {
match self.certificates.get(peer_public_key) {
Some(cert) => {
if !cert.is_valid(now_ms) {
debug!(
peer = hex::encode(peer_public_key),
"peer certificate expired"
);
return false;
}
if cert.verify().is_err() {
warn!(
peer = hex::encode(peer_public_key),
"peer certificate signature invalid"
);
return false;
}
true
}
None => false,
}
}
pub fn get_peer_tier(&self, peer_public_key: &[u8; 32]) -> Option<MeshTier> {
self.certificates.get(peer_public_key).map(|c| c.tier)
}
pub fn get_peer_permissions(&self, peer_public_key: &[u8; 32]) -> Option<u8> {
self.certificates
.get(peer_public_key)
.map(|c| c.permissions)
}
pub fn get_certificate(&self, public_key: &[u8; 32]) -> Option<&MeshCertificate> {
self.certificates.get(public_key)
}
pub fn get_certificate_by_node_id(&self, node_id: &str) -> Option<&MeshCertificate> {
self.node_id_index
.get(node_id)
.and_then(|pk| self.certificates.get(pk))
}
pub fn validate_node_id(&self, node_id: &str, now_ms: u64) -> bool {
match self.get_certificate_by_node_id(node_id) {
Some(cert) => {
if !cert.is_valid(now_ms) {
debug!(node_id, "peer certificate expired");
return false;
}
if cert.verify().is_err() {
warn!(node_id, "peer certificate signature invalid");
return false;
}
true
}
None => false,
}
}
pub fn get_node_tier(&self, node_id: &str) -> Option<MeshTier> {
self.get_certificate_by_node_id(node_id).map(|c| c.tier)
}
pub fn len(&self) -> usize {
self.certificates.len()
}
pub fn is_empty(&self) -> bool {
self.certificates.is_empty()
}
pub fn authority_count(&self) -> usize {
self.authorities.len()
}
pub fn remove_expired(&mut self, now_ms: u64) -> usize {
let before = self.certificates.len();
self.certificates.retain(|_, cert| {
let valid = cert.is_valid(now_ms);
if !valid && !cert.node_id.is_empty() {
self.node_id_index.remove(&cert.node_id);
}
valid
});
before - self.certificates.len()
}
pub fn load_authorities_from_dir(&mut self, dir: &Path) -> Result<usize, SecurityError> {
let mut count = 0;
let entries = std::fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let bytes = std::fs::read(&path)?;
if bytes.len() == 32 {
let mut key = [0u8; 32];
key.copy_from_slice(&bytes);
if VerifyingKey::from_bytes(&key).is_ok() {
self.add_authority(key);
count += 1;
debug!(path = ?path, "loaded authority key");
} else {
warn!(path = ?path, "invalid Ed25519 public key, skipping");
}
} else {
warn!(path = ?path, len = bytes.len(), "expected 32-byte key, skipping");
}
}
}
Ok(count)
}
pub fn load_certificates_from_dir(&mut self, dir: &Path) -> Result<usize, SecurityError> {
let mut count = 0;
let entries = std::fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let bytes = std::fs::read(&path)?;
match MeshCertificate::decode(&bytes) {
Ok(cert) => match self.add_certificate(cert) {
Ok(()) => {
count += 1;
debug!(path = ?path, "loaded certificate");
}
Err(e) => {
warn!(path = ?path, error = %e, "certificate rejected");
}
},
Err(e) => {
warn!(path = ?path, error = %e, "failed to decode certificate");
}
}
}
}
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
fn one_hour_ms() -> u64 {
60 * 60 * 1000
}
#[test]
fn test_mesh_tier_roundtrip() {
for tier in [
MeshTier::Enterprise,
MeshTier::Regional,
MeshTier::Tactical,
MeshTier::Edge,
] {
assert_eq!(MeshTier::from_byte(tier.to_byte()), Some(tier));
assert_eq!(MeshTier::from_str_name(tier.as_str()), Some(tier));
}
assert_eq!(MeshTier::from_byte(99), None);
assert_eq!(MeshTier::from_str_name("unknown"), None);
}
#[test]
fn test_mesh_tier_case_insensitive() {
assert_eq!(
MeshTier::from_str_name("enterprise"),
Some(MeshTier::Enterprise)
);
assert_eq!(
MeshTier::from_str_name("TACTICAL"),
Some(MeshTier::Tactical)
);
assert_eq!(MeshTier::from_str_name(" Edge "), Some(MeshTier::Edge));
}
#[test]
fn test_mesh_tier_ordering() {
assert!(MeshTier::Enterprise < MeshTier::Regional);
assert!(MeshTier::Regional < MeshTier::Tactical);
assert!(MeshTier::Tactical < MeshTier::Edge);
}
fn make_cert(
authority: &DeviceKeypair,
member: &DeviceKeypair,
node_id: &str,
tier: MeshTier,
perms: u8,
issued: u64,
expires: u64,
) -> MeshCertificate {
MeshCertificate::new(
member.public_key_bytes(),
"DEADBEEF".to_string(),
node_id.to_string(),
tier,
perms,
issued,
expires,
authority.public_key_bytes(),
)
.signed(authority)
}
#[test]
fn test_certificate_sign_verify() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tac-1",
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
);
assert!(cert.verify().is_ok());
assert!(cert.is_valid(now));
assert!(!cert.is_root());
assert!(cert.has_permission(permissions::RELAY));
assert!(cert.has_permission(permissions::EMERGENCY));
assert!(!cert.has_permission(permissions::ADMIN));
assert_eq!(cert.node_id, "tac-1");
}
#[test]
fn test_certificate_root() {
let authority = DeviceKeypair::generate();
let now = now_ms();
let cert = MeshCertificate::new_root(
&authority,
"DEADBEEF".to_string(),
"enterprise-0".to_string(),
MeshTier::Enterprise,
now,
now + one_hour_ms(),
);
assert!(cert.verify().is_ok());
assert!(cert.is_root());
assert!(cert.has_permission(permissions::AUTHORITY));
assert_eq!(cert.node_id, "enterprise-0");
}
#[test]
fn test_certificate_expired() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tac-1",
MeshTier::Tactical,
permissions::STANDARD,
now - 2 * one_hour_ms(),
now - one_hour_ms(),
);
assert!(cert.verify().is_ok());
assert!(!cert.is_valid(now));
}
#[test]
fn test_certificate_no_expiration() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tac-1",
MeshTier::Tactical,
permissions::STANDARD,
now,
0,
);
assert!(cert.is_valid(now));
assert!(cert.is_valid(now + 365 * 24 * one_hour_ms()));
assert_eq!(cert.time_remaining_ms(now), u64::MAX);
}
#[test]
fn test_certificate_wrong_signer() {
let authority = DeviceKeypair::generate();
let imposter = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = MeshCertificate::new(
member.public_key_bytes(),
"DEADBEEF".to_string(),
"tac-1".to_string(),
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
authority.public_key_bytes(),
)
.signed(&imposter);
assert!(cert.verify().is_err());
}
#[test]
fn test_certificate_encode_decode() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = MeshCertificate::new(
member.public_key_bytes(),
"A1B2C3D4".to_string(),
"regional-hub-1".to_string(),
MeshTier::Regional,
permissions::STANDARD | permissions::ENROLL,
now,
now + one_hour_ms(),
authority.public_key_bytes(),
)
.signed(&authority);
let encoded = cert.encode();
let decoded = MeshCertificate::decode(&encoded).unwrap();
assert_eq!(decoded.subject_public_key, cert.subject_public_key);
assert_eq!(decoded.mesh_id, cert.mesh_id);
assert_eq!(decoded.node_id, "regional-hub-1");
assert_eq!(decoded.tier, cert.tier);
assert_eq!(decoded.permissions, cert.permissions);
assert_eq!(decoded.issued_at_ms, cert.issued_at_ms);
assert_eq!(decoded.expires_at_ms, cert.expires_at_ms);
assert_eq!(decoded.issuer_public_key, cert.issuer_public_key);
assert_eq!(decoded.signature, cert.signature);
assert!(decoded.verify().is_ok());
}
#[test]
fn test_certificate_decode_too_short() {
assert!(MeshCertificate::decode(&[0u8; 10]).is_err());
}
#[test]
fn test_bundle_validate_peer() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tac-1",
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
);
let mut bundle = CertificateBundle::new();
bundle.add_authority(authority.public_key_bytes());
bundle.add_certificate(cert).unwrap();
assert!(bundle.validate_peer(&member.public_key_bytes(), now));
assert_eq!(
bundle.get_peer_tier(&member.public_key_bytes()),
Some(MeshTier::Tactical)
);
assert_eq!(
bundle.get_peer_permissions(&member.public_key_bytes()),
Some(permissions::STANDARD)
);
let stranger = DeviceKeypair::generate();
assert!(!bundle.validate_peer(&stranger.public_key_bytes(), now));
}
#[test]
fn test_bundle_validate_by_node_id() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tactical-west-3",
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
);
let mut bundle = CertificateBundle::new();
bundle.add_authority(authority.public_key_bytes());
bundle.add_certificate(cert).unwrap();
assert!(bundle.validate_node_id("tactical-west-3", now));
assert!(!bundle.validate_node_id("unknown-node", now));
assert_eq!(
bundle.get_node_tier("tactical-west-3"),
Some(MeshTier::Tactical)
);
assert_eq!(bundle.get_node_tier("unknown"), None);
let found = bundle
.get_certificate_by_node_id("tactical-west-3")
.unwrap();
assert_eq!(found.subject_public_key, member.public_key_bytes());
}
#[test]
fn test_bundle_rejects_untrusted_issuer() {
let untrusted = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = MeshCertificate::new(
member.public_key_bytes(),
"DEADBEEF".to_string(),
"tac-1".to_string(),
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
untrusted.public_key_bytes(),
)
.signed(&untrusted);
let mut bundle = CertificateBundle::new();
let result = bundle.add_certificate(cert);
assert!(result.is_err());
}
#[test]
fn test_bundle_accepts_root_cert() {
let authority = DeviceKeypair::generate();
let now = now_ms();
let root = MeshCertificate::new_root(
&authority,
"DEADBEEF".to_string(),
"enterprise-0".to_string(),
MeshTier::Enterprise,
now,
now + one_hour_ms(),
);
let mut bundle = CertificateBundle::new();
bundle.add_certificate(root).unwrap();
assert!(bundle.validate_peer(&authority.public_key_bytes(), now));
assert!(bundle.validate_node_id("enterprise-0", now));
}
#[test]
fn test_bundle_remove_expired() {
let authority = DeviceKeypair::generate();
let now = now_ms();
let expired_member = DeviceKeypair::generate();
let expired_cert = make_cert(
&authority,
&expired_member,
"expired-node",
MeshTier::Tactical,
permissions::STANDARD,
now - 2 * one_hour_ms(),
now - one_hour_ms(),
);
let valid_member = DeviceKeypair::generate();
let valid_cert = make_cert(
&authority,
&valid_member,
"valid-node",
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
);
let mut bundle = CertificateBundle::new();
bundle.add_authority(authority.public_key_bytes());
bundle.add_certificate_unchecked(expired_cert);
bundle.add_certificate(valid_cert).unwrap();
assert_eq!(bundle.len(), 2);
let removed = bundle.remove_expired(now);
assert_eq!(removed, 1);
assert_eq!(bundle.len(), 1);
assert!(!bundle.validate_node_id("expired-node", now));
assert!(bundle.validate_node_id("valid-node", now));
}
#[test]
fn test_bundle_load_from_dir() {
let dir = tempfile::tempdir().unwrap();
let authority = DeviceKeypair::generate();
let auth_dir = dir.path().join("authorities");
std::fs::create_dir(&auth_dir).unwrap();
std::fs::write(auth_dir.join("root.key"), authority.public_key_bytes()).unwrap();
let cert_dir = dir.path().join("certificates");
std::fs::create_dir(&cert_dir).unwrap();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tac-1",
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
);
std::fs::write(cert_dir.join("member.cert"), cert.encode()).unwrap();
let mut bundle = CertificateBundle::new();
let auth_count = bundle.load_authorities_from_dir(&auth_dir).unwrap();
assert_eq!(auth_count, 1);
let cert_count = bundle.load_certificates_from_dir(&cert_dir).unwrap();
assert_eq!(cert_count, 1);
assert!(bundle.validate_peer(&member.public_key_bytes(), now));
assert!(bundle.validate_node_id("tac-1", now));
}
#[test]
fn test_time_remaining() {
let authority = DeviceKeypair::generate();
let member = DeviceKeypair::generate();
let now = now_ms();
let cert = make_cert(
&authority,
&member,
"tac-1",
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
);
let remaining = cert.time_remaining_ms(now);
assert!(remaining > 0);
assert!(remaining <= one_hour_ms());
let remaining_expired = cert.time_remaining_ms(now + 2 * one_hour_ms());
assert_eq!(remaining_expired, 0);
}
#[test]
fn test_delegation_chain_enroll_permission() {
let authority = DeviceKeypair::generate();
let delegator = DeviceKeypair::generate();
let new_member = DeviceKeypair::generate();
let now = now_ms();
let delegator_cert = make_cert(
&authority,
&delegator,
"delegator",
MeshTier::Regional,
permissions::STANDARD | permissions::ENROLL,
now,
now + one_hour_ms(),
);
let delegated_cert = MeshCertificate::new(
new_member.public_key_bytes(),
"DEADBEEF".to_string(),
"new-node".to_string(),
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
delegator.public_key_bytes(),
)
.signed(&delegator);
let mut bundle = CertificateBundle::new();
bundle.add_authority(authority.public_key_bytes());
bundle.add_certificate(delegator_cert).unwrap();
bundle.add_certificate(delegated_cert).unwrap();
assert!(bundle.validate_node_id("new-node", now));
assert!(bundle.validate_node_id("delegator", now));
}
#[test]
fn test_delegation_chain_without_enroll_rejected() {
let authority = DeviceKeypair::generate();
let non_delegator = DeviceKeypair::generate();
let new_member = DeviceKeypair::generate();
let now = now_ms();
let non_delegator_cert = make_cert(
&authority,
&non_delegator,
"standard-node",
MeshTier::Tactical,
permissions::STANDARD, now,
now + one_hour_ms(),
);
let invalid_cert = MeshCertificate::new(
new_member.public_key_bytes(),
"DEADBEEF".to_string(),
"unauthorized-node".to_string(),
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
non_delegator.public_key_bytes(),
)
.signed(&non_delegator);
let mut bundle = CertificateBundle::new();
bundle.add_authority(authority.public_key_bytes());
bundle.add_certificate(non_delegator_cert).unwrap();
let result = bundle.add_certificate(invalid_cert);
assert!(result.is_err());
}
#[test]
fn test_delegation_rejected_when_issuer_expired() {
let authority = DeviceKeypair::generate();
let delegator = DeviceKeypair::generate();
let new_member = DeviceKeypair::generate();
let now = now_ms();
let delegator_cert = make_cert(
&authority,
&delegator,
"delegator",
MeshTier::Regional,
permissions::STANDARD | permissions::ENROLL,
now - 2 * one_hour_ms(),
now - one_hour_ms(), );
let mut bundle = CertificateBundle::new();
bundle.add_authority(authority.public_key_bytes());
bundle.add_certificate_unchecked(delegator_cert);
let delegated_cert = MeshCertificate::new(
new_member.public_key_bytes(),
"DEADBEEF".to_string(),
"new-node".to_string(),
MeshTier::Tactical,
permissions::STANDARD,
now,
now + one_hour_ms(),
delegator.public_key_bytes(),
)
.signed(&delegator);
let result = bundle.add_certificate(delegated_cert);
assert!(result.is_err());
}
}