Skip to main content

peat_mesh/security/
certificate.rs

1//! Mesh-layer peer certificates and trust validation.
2//!
3//! Provides lightweight certificate types for peer authentication at the mesh
4//! transport layer. Higher-level certificate types (e.g., `MembershipCertificate`
5//! in peat-protocol) can convert to/from these types.
6//!
7//! # Trust Model
8//!
9//! ```text
10//! Authority (root)
11//!     │
12//!     └── signs ──► MeshCertificate
13//!                       ├── subject_public_key ──► derives EndpointId
14//!                       ├── mesh_id (formation identifier)
15//!                       ├── tier (Enterprise/Regional/Tactical/Edge)
16//!                       ├── permissions (bitflags)
17//!                       └── validity window (issued_at..expires_at)
18//! ```
19
20use std::collections::HashMap;
21use std::path::Path;
22
23use ed25519_dalek::{Verifier, VerifyingKey};
24use tracing::{debug, warn};
25
26use super::error::SecurityError;
27use super::keypair::DeviceKeypair;
28
29/// Tier classification for mesh nodes.
30///
31/// Mirrors the distribution hierarchy used by peat-registry for
32/// artifact routing. Lower ordinal = higher trust / more resources.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
34#[repr(u8)]
35pub enum MeshTier {
36    /// Tier 0 — enterprise data center, source of truth.
37    Enterprise = 0,
38    /// Tier 1 — regional hub, caches from enterprise.
39    Regional = 1,
40    /// Tier 2 — tactical node, intermittent connectivity.
41    Tactical = 2,
42    /// Tier 3 — edge device, minimal storage.
43    Edge = 3,
44}
45
46impl MeshTier {
47    /// Parse from a string (case-insensitive).
48    pub fn from_str_name(s: &str) -> Option<Self> {
49        match s.trim().to_lowercase().as_str() {
50            "enterprise" => Some(Self::Enterprise),
51            "regional" => Some(Self::Regional),
52            "tactical" => Some(Self::Tactical),
53            "edge" => Some(Self::Edge),
54            _ => None,
55        }
56    }
57
58    /// Get the tier name as a string.
59    pub fn as_str(&self) -> &'static str {
60        match self {
61            Self::Enterprise => "Enterprise",
62            Self::Regional => "Regional",
63            Self::Tactical => "Tactical",
64            Self::Edge => "Edge",
65        }
66    }
67
68    /// Encode as a single byte.
69    pub fn to_byte(self) -> u8 {
70        self as u8
71    }
72
73    /// Decode from a single byte.
74    pub fn from_byte(b: u8) -> Option<Self> {
75        match b {
76            0 => Some(Self::Enterprise),
77            1 => Some(Self::Regional),
78            2 => Some(Self::Tactical),
79            3 => Some(Self::Edge),
80            _ => None,
81        }
82    }
83}
84
85impl std::fmt::Display for MeshTier {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.write_str(self.as_str())
88    }
89}
90
91/// Permission flags for mesh certificates.
92///
93/// Compatible with peat-protocol's `MemberPermissions` bitflags.
94pub mod permissions {
95    /// Can relay messages for other nodes.
96    pub const RELAY: u8 = 0b0000_0001;
97    /// Can trigger emergency alerts.
98    pub const EMERGENCY: u8 = 0b0000_0010;
99    /// Can enroll new members (delegation).
100    pub const ENROLL: u8 = 0b0000_0100;
101    /// Full administrative privileges.
102    pub const ADMIN: u8 = 0b1000_0000;
103    /// Standard member: RELAY + EMERGENCY.
104    pub const STANDARD: u8 = RELAY | EMERGENCY;
105    /// Authority: all permissions.
106    pub const AUTHORITY: u8 = RELAY | EMERGENCY | ENROLL | ADMIN;
107}
108
109/// A signed certificate binding a public key to mesh membership.
110///
111/// Wire format (variable length):
112/// ```text
113/// [subject_pubkey:32][mesh_id_len:1][mesh_id:N][node_id_len:1][node_id:M]
114/// [tier:1][permissions:1][issued_at:8][expires_at:8][issuer_pubkey:32][signature:64]
115/// ```
116/// Minimum size: 148 bytes (with empty mesh_id and node_id).
117#[derive(Clone, Debug)]
118pub struct MeshCertificate {
119    /// Subject's Ed25519 public key (the node this cert is issued to).
120    pub subject_public_key: [u8; 32],
121    /// Mesh/formation identifier.
122    pub mesh_id: String,
123    /// Node identifier within the mesh (maps to discovery hostname).
124    /// Used to bridge between certificate identity and HKDF-derived Iroh EndpointId.
125    pub node_id: String,
126    /// Node's tier in the distribution hierarchy.
127    pub tier: MeshTier,
128    /// Permission bitflags (see [`permissions`] module).
129    pub permissions: u8,
130    /// Issuance time (Unix epoch milliseconds).
131    pub issued_at_ms: u64,
132    /// Expiration time (Unix epoch milliseconds). 0 = no expiration.
133    pub expires_at_ms: u64,
134    /// Issuer's (authority's) Ed25519 public key.
135    pub issuer_public_key: [u8; 32],
136    /// Ed25519 signature over the signable portion of the certificate.
137    pub signature: [u8; 64],
138}
139
140impl MeshCertificate {
141    /// Create a new unsigned certificate.
142    #[allow(clippy::too_many_arguments)]
143    pub fn new(
144        subject_public_key: [u8; 32],
145        mesh_id: String,
146        node_id: String,
147        tier: MeshTier,
148        permissions: u8,
149        issued_at_ms: u64,
150        expires_at_ms: u64,
151        issuer_public_key: [u8; 32],
152    ) -> Self {
153        Self {
154            subject_public_key,
155            mesh_id,
156            node_id,
157            tier,
158            permissions,
159            issued_at_ms,
160            expires_at_ms,
161            issuer_public_key,
162            signature: [0u8; 64],
163        }
164    }
165
166    /// Create a self-signed root (authority) certificate.
167    pub fn new_root(
168        authority: &DeviceKeypair,
169        mesh_id: String,
170        node_id: String,
171        tier: MeshTier,
172        issued_at_ms: u64,
173        expires_at_ms: u64,
174    ) -> Self {
175        let pubkey = authority.public_key_bytes();
176        let mut cert = Self::new(
177            pubkey,
178            mesh_id,
179            node_id,
180            tier,
181            permissions::AUTHORITY,
182            issued_at_ms,
183            expires_at_ms,
184            pubkey,
185        );
186        cert.sign_with(authority);
187        cert
188    }
189
190    /// Sign the certificate in-place with the given keypair.
191    pub fn sign_with(&mut self, issuer: &DeviceKeypair) {
192        let signable = self.signable_bytes();
193        let sig = issuer.sign(&signable);
194        self.signature = sig.to_bytes();
195    }
196
197    /// Return a signed copy (builder pattern).
198    pub fn signed(mut self, issuer: &DeviceKeypair) -> Self {
199        self.sign_with(issuer);
200        self
201    }
202
203    /// Verify the certificate's signature against the issuer public key.
204    pub fn verify(&self) -> Result<(), SecurityError> {
205        let vk = VerifyingKey::from_bytes(&self.issuer_public_key)
206            .map_err(|e| SecurityError::InvalidPublicKey(e.to_string()))?;
207        let sig = ed25519_dalek::Signature::from_bytes(&self.signature);
208        let signable = self.signable_bytes();
209        vk.verify(&signable, &sig)
210            .map_err(|e| SecurityError::InvalidSignature(e.to_string()))
211    }
212
213    /// Check if this is a self-signed root certificate.
214    pub fn is_root(&self) -> bool {
215        self.subject_public_key == self.issuer_public_key
216    }
217
218    /// Check if the certificate is valid at the given time.
219    pub fn is_valid(&self, now_ms: u64) -> bool {
220        now_ms >= self.issued_at_ms && (self.expires_at_ms == 0 || now_ms < self.expires_at_ms)
221    }
222
223    /// Check if a specific permission is set.
224    pub fn has_permission(&self, perm: u8) -> bool {
225        self.permissions & perm == perm
226    }
227
228    /// Milliseconds remaining until expiration. 0 if expired or no expiration.
229    pub fn time_remaining_ms(&self, now_ms: u64) -> u64 {
230        if self.expires_at_ms == 0 {
231            return u64::MAX;
232        }
233        self.expires_at_ms.saturating_sub(now_ms)
234    }
235
236    /// The bytes that are signed (everything except the signature itself).
237    fn signable_bytes(&self) -> Vec<u8> {
238        let mut buf = Vec::with_capacity(83 + self.mesh_id.len() + self.node_id.len());
239        buf.extend_from_slice(&self.subject_public_key);
240        buf.push(self.mesh_id.len() as u8);
241        buf.extend_from_slice(self.mesh_id.as_bytes());
242        buf.push(self.node_id.len() as u8);
243        buf.extend_from_slice(self.node_id.as_bytes());
244        buf.push(self.tier.to_byte());
245        buf.push(self.permissions);
246        buf.extend_from_slice(&self.issued_at_ms.to_le_bytes());
247        buf.extend_from_slice(&self.expires_at_ms.to_le_bytes());
248        buf.extend_from_slice(&self.issuer_public_key);
249        buf
250    }
251
252    /// Encode to wire format.
253    pub fn encode(&self) -> Vec<u8> {
254        let mut buf = self.signable_bytes();
255        buf.extend_from_slice(&self.signature);
256        buf
257    }
258
259    /// Decode from wire format.
260    pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
261        // Minimum: 32 + 1 + 0 + 1 + 0 + 1 + 1 + 8 + 8 + 32 + 64 = 148
262        if data.len() < 148 {
263            return Err(SecurityError::SerializationError(format!(
264                "certificate too short: {} bytes (min 148)",
265                data.len()
266            )));
267        }
268
269        let mut pos = 0;
270
271        let mut subject_public_key = [0u8; 32];
272        subject_public_key.copy_from_slice(&data[pos..pos + 32]);
273        pos += 32;
274
275        let mesh_id_len = data[pos] as usize;
276        pos += 1;
277
278        if pos + mesh_id_len >= data.len() {
279            return Err(SecurityError::SerializationError(
280                "certificate truncated at mesh_id".to_string(),
281            ));
282        }
283
284        let mesh_id = String::from_utf8(data[pos..pos + mesh_id_len].to_vec())
285            .map_err(|e| SecurityError::SerializationError(format!("invalid mesh_id: {e}")))?;
286        pos += mesh_id_len;
287
288        let node_id_len = data[pos] as usize;
289        pos += 1;
290
291        if pos + node_id_len + 1 + 1 + 8 + 8 + 32 + 64 > data.len() {
292            return Err(SecurityError::SerializationError(
293                "certificate truncated at node_id".to_string(),
294            ));
295        }
296
297        let node_id = String::from_utf8(data[pos..pos + node_id_len].to_vec())
298            .map_err(|e| SecurityError::SerializationError(format!("invalid node_id: {e}")))?;
299        pos += node_id_len;
300
301        let tier = MeshTier::from_byte(data[pos])
302            .ok_or_else(|| SecurityError::SerializationError("invalid tier byte".to_string()))?;
303        pos += 1;
304
305        let permissions = data[pos];
306        pos += 1;
307
308        let issued_at_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
309        pos += 8;
310
311        let expires_at_ms = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
312        pos += 8;
313
314        let mut issuer_public_key = [0u8; 32];
315        issuer_public_key.copy_from_slice(&data[pos..pos + 32]);
316        pos += 32;
317
318        let mut signature = [0u8; 64];
319        signature.copy_from_slice(&data[pos..pos + 64]);
320
321        Ok(Self {
322            subject_public_key,
323            mesh_id,
324            node_id,
325            tier,
326            permissions,
327            issued_at_ms,
328            expires_at_ms,
329            issuer_public_key,
330            signature,
331        })
332    }
333}
334
335/// A bundle of trusted authority keys and peer certificates.
336///
337/// Used to validate peer connections and determine peer tier/permissions.
338/// Maintains a dual index: by subject public key and by node_id (hostname).
339#[derive(Debug, Default)]
340pub struct CertificateBundle {
341    /// Trusted authority public keys.
342    authorities: Vec<[u8; 32]>,
343    /// Certificates indexed by subject public key.
344    certificates: HashMap<[u8; 32], MeshCertificate>,
345    /// Reverse index: node_id → subject public key.
346    node_id_index: HashMap<String, [u8; 32]>,
347}
348
349impl CertificateBundle {
350    /// Create an empty bundle.
351    pub fn new() -> Self {
352        Self::default()
353    }
354
355    /// Add a trusted authority public key.
356    pub fn add_authority(&mut self, public_key: [u8; 32]) {
357        if !self.authorities.contains(&public_key) {
358            self.authorities.push(public_key);
359        }
360    }
361
362    /// Add a certificate to the bundle.
363    ///
364    /// The certificate's signature is verified, then the issuer is checked:
365    /// 1. Self-signed root certs are always accepted.
366    /// 2. Certs signed by a trusted authority are accepted.
367    /// 3. Certs signed by a node with `ENROLL` permission (delegation) are accepted,
368    ///    provided the delegating node's own certificate is valid and non-expired.
369    ///
370    /// Returns an error if the issuer is not trusted or the signature is invalid.
371    pub fn add_certificate(&mut self, cert: MeshCertificate) -> Result<(), SecurityError> {
372        // Verify signature
373        cert.verify()?;
374
375        let now_ms = std::time::SystemTime::now()
376            .duration_since(std::time::UNIX_EPOCH)
377            .unwrap_or_default()
378            .as_millis() as u64;
379
380        // Check issuer is trusted
381        if !cert.is_root() && !self.is_trusted_issuer(&cert.issuer_public_key, now_ms) {
382            return Err(SecurityError::CertificateError(
383                "issuer not in trusted authorities and has no ENROLL delegation".to_string(),
384            ));
385        }
386
387        if !cert.node_id.is_empty() {
388            self.node_id_index
389                .insert(cert.node_id.clone(), cert.subject_public_key);
390        }
391        self.certificates.insert(cert.subject_public_key, cert);
392        Ok(())
393    }
394
395    /// Check if a public key is a trusted issuer.
396    ///
397    /// An issuer is trusted if it is:
398    /// 1. In the explicit trusted authorities list, OR
399    /// 2. A node with a valid, non-expired certificate that has the `ENROLL` permission.
400    fn is_trusted_issuer(&self, issuer_key: &[u8; 32], now_ms: u64) -> bool {
401        // Direct authority
402        if self.authorities.contains(issuer_key) {
403            return true;
404        }
405
406        // Delegation: issuer has a certificate with ENROLL permission
407        if let Some(issuer_cert) = self.certificates.get(issuer_key) {
408            if issuer_cert.has_permission(permissions::ENROLL) && issuer_cert.is_valid(now_ms) {
409                // Verify the delegator's cert is itself valid
410                if issuer_cert.verify().is_ok() {
411                    return true;
412                }
413            }
414        }
415
416        false
417    }
418
419    /// Add a certificate without validation (for loading pre-validated bundles).
420    pub fn add_certificate_unchecked(&mut self, cert: MeshCertificate) {
421        if !cert.node_id.is_empty() {
422            self.node_id_index
423                .insert(cert.node_id.clone(), cert.subject_public_key);
424        }
425        self.certificates.insert(cert.subject_public_key, cert);
426    }
427
428    /// Validate a peer's public key against the bundle.
429    ///
430    /// Returns `true` if the peer has a valid, non-expired certificate
431    /// signed by a trusted authority.
432    pub fn validate_peer(&self, peer_public_key: &[u8; 32], now_ms: u64) -> bool {
433        match self.certificates.get(peer_public_key) {
434            Some(cert) => {
435                if !cert.is_valid(now_ms) {
436                    debug!(
437                        peer = hex::encode(peer_public_key),
438                        "peer certificate expired"
439                    );
440                    return false;
441                }
442                if cert.verify().is_err() {
443                    warn!(
444                        peer = hex::encode(peer_public_key),
445                        "peer certificate signature invalid"
446                    );
447                    return false;
448                }
449                true
450            }
451            None => false,
452        }
453    }
454
455    /// Get the tier for a peer, if they have a valid certificate.
456    pub fn get_peer_tier(&self, peer_public_key: &[u8; 32]) -> Option<MeshTier> {
457        self.certificates.get(peer_public_key).map(|c| c.tier)
458    }
459
460    /// Get the permissions for a peer, if they have a valid certificate.
461    pub fn get_peer_permissions(&self, peer_public_key: &[u8; 32]) -> Option<u8> {
462        self.certificates
463            .get(peer_public_key)
464            .map(|c| c.permissions)
465    }
466
467    /// Get a certificate by subject public key.
468    pub fn get_certificate(&self, public_key: &[u8; 32]) -> Option<&MeshCertificate> {
469        self.certificates.get(public_key)
470    }
471
472    /// Get a certificate by node_id (discovery hostname).
473    pub fn get_certificate_by_node_id(&self, node_id: &str) -> Option<&MeshCertificate> {
474        self.node_id_index
475            .get(node_id)
476            .and_then(|pk| self.certificates.get(pk))
477    }
478
479    /// Validate a peer by node_id (discovery hostname).
480    ///
481    /// Returns `true` if the node_id maps to a valid, non-expired certificate.
482    /// This is the primary validation entry point for PeerConnector, which
483    /// discovers peers by hostname.
484    pub fn validate_node_id(&self, node_id: &str, now_ms: u64) -> bool {
485        match self.get_certificate_by_node_id(node_id) {
486            Some(cert) => {
487                if !cert.is_valid(now_ms) {
488                    debug!(node_id, "peer certificate expired");
489                    return false;
490                }
491                if cert.verify().is_err() {
492                    warn!(node_id, "peer certificate signature invalid");
493                    return false;
494                }
495                true
496            }
497            None => false,
498        }
499    }
500
501    /// Get the tier for a node_id, if it has a valid certificate.
502    pub fn get_node_tier(&self, node_id: &str) -> Option<MeshTier> {
503        self.get_certificate_by_node_id(node_id).map(|c| c.tier)
504    }
505
506    /// Number of certificates in the bundle.
507    pub fn len(&self) -> usize {
508        self.certificates.len()
509    }
510
511    /// Whether the bundle is empty.
512    pub fn is_empty(&self) -> bool {
513        self.certificates.is_empty()
514    }
515
516    /// Number of trusted authorities.
517    pub fn authority_count(&self) -> usize {
518        self.authorities.len()
519    }
520
521    /// Remove expired certificates. Returns the number removed.
522    pub fn remove_expired(&mut self, now_ms: u64) -> usize {
523        let before = self.certificates.len();
524        self.certificates.retain(|_, cert| {
525            let valid = cert.is_valid(now_ms);
526            if !valid && !cert.node_id.is_empty() {
527                self.node_id_index.remove(&cert.node_id);
528            }
529            valid
530        });
531        before - self.certificates.len()
532    }
533
534    /// Load authority public keys from a directory.
535    ///
536    /// Each file should contain a raw 32-byte Ed25519 public key.
537    pub fn load_authorities_from_dir(&mut self, dir: &Path) -> Result<usize, SecurityError> {
538        let mut count = 0;
539        let entries = std::fs::read_dir(dir)?;
540        for entry in entries {
541            let entry = entry?;
542            let path = entry.path();
543            if path.is_file() {
544                let bytes = std::fs::read(&path)?;
545                if bytes.len() == 32 {
546                    let mut key = [0u8; 32];
547                    key.copy_from_slice(&bytes);
548                    // Validate it's a valid Ed25519 public key
549                    if VerifyingKey::from_bytes(&key).is_ok() {
550                        self.add_authority(key);
551                        count += 1;
552                        debug!(path = ?path, "loaded authority key");
553                    } else {
554                        warn!(path = ?path, "invalid Ed25519 public key, skipping");
555                    }
556                } else {
557                    warn!(path = ?path, len = bytes.len(), "expected 32-byte key, skipping");
558                }
559            }
560        }
561        Ok(count)
562    }
563
564    /// Load certificates from a directory.
565    ///
566    /// Each file should contain a wire-encoded `MeshCertificate`.
567    /// Certificates with unknown issuers are skipped (warning logged).
568    pub fn load_certificates_from_dir(&mut self, dir: &Path) -> Result<usize, SecurityError> {
569        let mut count = 0;
570        let entries = std::fs::read_dir(dir)?;
571        for entry in entries {
572            let entry = entry?;
573            let path = entry.path();
574            if path.is_file() {
575                let bytes = std::fs::read(&path)?;
576                match MeshCertificate::decode(&bytes) {
577                    Ok(cert) => match self.add_certificate(cert) {
578                        Ok(()) => {
579                            count += 1;
580                            debug!(path = ?path, "loaded certificate");
581                        }
582                        Err(e) => {
583                            warn!(path = ?path, error = %e, "certificate rejected");
584                        }
585                    },
586                    Err(e) => {
587                        warn!(path = ?path, error = %e, "failed to decode certificate");
588                    }
589                }
590            }
591        }
592        Ok(count)
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599
600    fn now_ms() -> u64 {
601        std::time::SystemTime::now()
602            .duration_since(std::time::UNIX_EPOCH)
603            .unwrap()
604            .as_millis() as u64
605    }
606
607    fn one_hour_ms() -> u64 {
608        60 * 60 * 1000
609    }
610
611    #[test]
612    fn test_mesh_tier_roundtrip() {
613        for tier in [
614            MeshTier::Enterprise,
615            MeshTier::Regional,
616            MeshTier::Tactical,
617            MeshTier::Edge,
618        ] {
619            assert_eq!(MeshTier::from_byte(tier.to_byte()), Some(tier));
620            assert_eq!(MeshTier::from_str_name(tier.as_str()), Some(tier));
621        }
622        assert_eq!(MeshTier::from_byte(99), None);
623        assert_eq!(MeshTier::from_str_name("unknown"), None);
624    }
625
626    #[test]
627    fn test_mesh_tier_case_insensitive() {
628        assert_eq!(
629            MeshTier::from_str_name("enterprise"),
630            Some(MeshTier::Enterprise)
631        );
632        assert_eq!(
633            MeshTier::from_str_name("TACTICAL"),
634            Some(MeshTier::Tactical)
635        );
636        assert_eq!(MeshTier::from_str_name(" Edge "), Some(MeshTier::Edge));
637    }
638
639    #[test]
640    fn test_mesh_tier_ordering() {
641        assert!(MeshTier::Enterprise < MeshTier::Regional);
642        assert!(MeshTier::Regional < MeshTier::Tactical);
643        assert!(MeshTier::Tactical < MeshTier::Edge);
644    }
645
646    /// Helper: create a test cert with node_id.
647    fn make_cert(
648        authority: &DeviceKeypair,
649        member: &DeviceKeypair,
650        node_id: &str,
651        tier: MeshTier,
652        perms: u8,
653        issued: u64,
654        expires: u64,
655    ) -> MeshCertificate {
656        MeshCertificate::new(
657            member.public_key_bytes(),
658            "DEADBEEF".to_string(),
659            node_id.to_string(),
660            tier,
661            perms,
662            issued,
663            expires,
664            authority.public_key_bytes(),
665        )
666        .signed(authority)
667    }
668
669    #[test]
670    fn test_certificate_sign_verify() {
671        let authority = DeviceKeypair::generate();
672        let member = DeviceKeypair::generate();
673        let now = now_ms();
674
675        let cert = make_cert(
676            &authority,
677            &member,
678            "tac-1",
679            MeshTier::Tactical,
680            permissions::STANDARD,
681            now,
682            now + one_hour_ms(),
683        );
684
685        assert!(cert.verify().is_ok());
686        assert!(cert.is_valid(now));
687        assert!(!cert.is_root());
688        assert!(cert.has_permission(permissions::RELAY));
689        assert!(cert.has_permission(permissions::EMERGENCY));
690        assert!(!cert.has_permission(permissions::ADMIN));
691        assert_eq!(cert.node_id, "tac-1");
692    }
693
694    #[test]
695    fn test_certificate_root() {
696        let authority = DeviceKeypair::generate();
697        let now = now_ms();
698
699        let cert = MeshCertificate::new_root(
700            &authority,
701            "DEADBEEF".to_string(),
702            "enterprise-0".to_string(),
703            MeshTier::Enterprise,
704            now,
705            now + one_hour_ms(),
706        );
707
708        assert!(cert.verify().is_ok());
709        assert!(cert.is_root());
710        assert!(cert.has_permission(permissions::AUTHORITY));
711        assert_eq!(cert.node_id, "enterprise-0");
712    }
713
714    #[test]
715    fn test_certificate_expired() {
716        let authority = DeviceKeypair::generate();
717        let member = DeviceKeypair::generate();
718        let now = now_ms();
719
720        let cert = make_cert(
721            &authority,
722            &member,
723            "tac-1",
724            MeshTier::Tactical,
725            permissions::STANDARD,
726            now - 2 * one_hour_ms(),
727            now - one_hour_ms(),
728        );
729
730        assert!(cert.verify().is_ok());
731        assert!(!cert.is_valid(now));
732    }
733
734    #[test]
735    fn test_certificate_no_expiration() {
736        let authority = DeviceKeypair::generate();
737        let member = DeviceKeypair::generate();
738        let now = now_ms();
739
740        let cert = make_cert(
741            &authority,
742            &member,
743            "tac-1",
744            MeshTier::Tactical,
745            permissions::STANDARD,
746            now,
747            0,
748        );
749
750        assert!(cert.is_valid(now));
751        assert!(cert.is_valid(now + 365 * 24 * one_hour_ms()));
752        assert_eq!(cert.time_remaining_ms(now), u64::MAX);
753    }
754
755    #[test]
756    fn test_certificate_wrong_signer() {
757        let authority = DeviceKeypair::generate();
758        let imposter = DeviceKeypair::generate();
759        let member = DeviceKeypair::generate();
760        let now = now_ms();
761
762        // Claims authority issued it, but imposter signed it
763        let cert = MeshCertificate::new(
764            member.public_key_bytes(),
765            "DEADBEEF".to_string(),
766            "tac-1".to_string(),
767            MeshTier::Tactical,
768            permissions::STANDARD,
769            now,
770            now + one_hour_ms(),
771            authority.public_key_bytes(),
772        )
773        .signed(&imposter);
774
775        assert!(cert.verify().is_err());
776    }
777
778    #[test]
779    fn test_certificate_encode_decode() {
780        let authority = DeviceKeypair::generate();
781        let member = DeviceKeypair::generate();
782        let now = now_ms();
783
784        let cert = MeshCertificate::new(
785            member.public_key_bytes(),
786            "A1B2C3D4".to_string(),
787            "regional-hub-1".to_string(),
788            MeshTier::Regional,
789            permissions::STANDARD | permissions::ENROLL,
790            now,
791            now + one_hour_ms(),
792            authority.public_key_bytes(),
793        )
794        .signed(&authority);
795
796        let encoded = cert.encode();
797        let decoded = MeshCertificate::decode(&encoded).unwrap();
798
799        assert_eq!(decoded.subject_public_key, cert.subject_public_key);
800        assert_eq!(decoded.mesh_id, cert.mesh_id);
801        assert_eq!(decoded.node_id, "regional-hub-1");
802        assert_eq!(decoded.tier, cert.tier);
803        assert_eq!(decoded.permissions, cert.permissions);
804        assert_eq!(decoded.issued_at_ms, cert.issued_at_ms);
805        assert_eq!(decoded.expires_at_ms, cert.expires_at_ms);
806        assert_eq!(decoded.issuer_public_key, cert.issuer_public_key);
807        assert_eq!(decoded.signature, cert.signature);
808        assert!(decoded.verify().is_ok());
809    }
810
811    #[test]
812    fn test_certificate_decode_too_short() {
813        assert!(MeshCertificate::decode(&[0u8; 10]).is_err());
814    }
815
816    #[test]
817    fn test_bundle_validate_peer() {
818        let authority = DeviceKeypair::generate();
819        let member = DeviceKeypair::generate();
820        let now = now_ms();
821
822        let cert = make_cert(
823            &authority,
824            &member,
825            "tac-1",
826            MeshTier::Tactical,
827            permissions::STANDARD,
828            now,
829            now + one_hour_ms(),
830        );
831
832        let mut bundle = CertificateBundle::new();
833        bundle.add_authority(authority.public_key_bytes());
834        bundle.add_certificate(cert).unwrap();
835
836        assert!(bundle.validate_peer(&member.public_key_bytes(), now));
837        assert_eq!(
838            bundle.get_peer_tier(&member.public_key_bytes()),
839            Some(MeshTier::Tactical)
840        );
841        assert_eq!(
842            bundle.get_peer_permissions(&member.public_key_bytes()),
843            Some(permissions::STANDARD)
844        );
845
846        let stranger = DeviceKeypair::generate();
847        assert!(!bundle.validate_peer(&stranger.public_key_bytes(), now));
848    }
849
850    #[test]
851    fn test_bundle_validate_by_node_id() {
852        let authority = DeviceKeypair::generate();
853        let member = DeviceKeypair::generate();
854        let now = now_ms();
855
856        let cert = make_cert(
857            &authority,
858            &member,
859            "tactical-west-3",
860            MeshTier::Tactical,
861            permissions::STANDARD,
862            now,
863            now + one_hour_ms(),
864        );
865
866        let mut bundle = CertificateBundle::new();
867        bundle.add_authority(authority.public_key_bytes());
868        bundle.add_certificate(cert).unwrap();
869
870        // Validate by node_id (the PeerConnector path)
871        assert!(bundle.validate_node_id("tactical-west-3", now));
872        assert!(!bundle.validate_node_id("unknown-node", now));
873
874        // Tier lookup by node_id
875        assert_eq!(
876            bundle.get_node_tier("tactical-west-3"),
877            Some(MeshTier::Tactical)
878        );
879        assert_eq!(bundle.get_node_tier("unknown"), None);
880
881        // Certificate lookup by node_id
882        let found = bundle
883            .get_certificate_by_node_id("tactical-west-3")
884            .unwrap();
885        assert_eq!(found.subject_public_key, member.public_key_bytes());
886    }
887
888    #[test]
889    fn test_bundle_rejects_untrusted_issuer() {
890        let untrusted = DeviceKeypair::generate();
891        let member = DeviceKeypair::generate();
892        let now = now_ms();
893
894        let cert = MeshCertificate::new(
895            member.public_key_bytes(),
896            "DEADBEEF".to_string(),
897            "tac-1".to_string(),
898            MeshTier::Tactical,
899            permissions::STANDARD,
900            now,
901            now + one_hour_ms(),
902            untrusted.public_key_bytes(),
903        )
904        .signed(&untrusted);
905
906        let mut bundle = CertificateBundle::new();
907        let result = bundle.add_certificate(cert);
908        assert!(result.is_err());
909    }
910
911    #[test]
912    fn test_bundle_accepts_root_cert() {
913        let authority = DeviceKeypair::generate();
914        let now = now_ms();
915
916        let root = MeshCertificate::new_root(
917            &authority,
918            "DEADBEEF".to_string(),
919            "enterprise-0".to_string(),
920            MeshTier::Enterprise,
921            now,
922            now + one_hour_ms(),
923        );
924
925        let mut bundle = CertificateBundle::new();
926        bundle.add_certificate(root).unwrap();
927        assert!(bundle.validate_peer(&authority.public_key_bytes(), now));
928        assert!(bundle.validate_node_id("enterprise-0", now));
929    }
930
931    #[test]
932    fn test_bundle_remove_expired() {
933        let authority = DeviceKeypair::generate();
934        let now = now_ms();
935
936        let expired_member = DeviceKeypair::generate();
937        let expired_cert = make_cert(
938            &authority,
939            &expired_member,
940            "expired-node",
941            MeshTier::Tactical,
942            permissions::STANDARD,
943            now - 2 * one_hour_ms(),
944            now - one_hour_ms(),
945        );
946
947        let valid_member = DeviceKeypair::generate();
948        let valid_cert = make_cert(
949            &authority,
950            &valid_member,
951            "valid-node",
952            MeshTier::Tactical,
953            permissions::STANDARD,
954            now,
955            now + one_hour_ms(),
956        );
957
958        let mut bundle = CertificateBundle::new();
959        bundle.add_authority(authority.public_key_bytes());
960        bundle.add_certificate_unchecked(expired_cert);
961        bundle.add_certificate(valid_cert).unwrap();
962        assert_eq!(bundle.len(), 2);
963
964        let removed = bundle.remove_expired(now);
965        assert_eq!(removed, 1);
966        assert_eq!(bundle.len(), 1);
967        // node_id index cleaned up
968        assert!(!bundle.validate_node_id("expired-node", now));
969        assert!(bundle.validate_node_id("valid-node", now));
970    }
971
972    #[test]
973    fn test_bundle_load_from_dir() {
974        let dir = tempfile::tempdir().unwrap();
975        let authority = DeviceKeypair::generate();
976
977        let auth_dir = dir.path().join("authorities");
978        std::fs::create_dir(&auth_dir).unwrap();
979        std::fs::write(auth_dir.join("root.key"), authority.public_key_bytes()).unwrap();
980
981        let cert_dir = dir.path().join("certificates");
982        std::fs::create_dir(&cert_dir).unwrap();
983
984        let member = DeviceKeypair::generate();
985        let now = now_ms();
986        let cert = make_cert(
987            &authority,
988            &member,
989            "tac-1",
990            MeshTier::Tactical,
991            permissions::STANDARD,
992            now,
993            now + one_hour_ms(),
994        );
995
996        std::fs::write(cert_dir.join("member.cert"), cert.encode()).unwrap();
997
998        let mut bundle = CertificateBundle::new();
999        let auth_count = bundle.load_authorities_from_dir(&auth_dir).unwrap();
1000        assert_eq!(auth_count, 1);
1001
1002        let cert_count = bundle.load_certificates_from_dir(&cert_dir).unwrap();
1003        assert_eq!(cert_count, 1);
1004
1005        assert!(bundle.validate_peer(&member.public_key_bytes(), now));
1006        assert!(bundle.validate_node_id("tac-1", now));
1007    }
1008
1009    #[test]
1010    fn test_time_remaining() {
1011        let authority = DeviceKeypair::generate();
1012        let member = DeviceKeypair::generate();
1013        let now = now_ms();
1014
1015        let cert = make_cert(
1016            &authority,
1017            &member,
1018            "tac-1",
1019            MeshTier::Tactical,
1020            permissions::STANDARD,
1021            now,
1022            now + one_hour_ms(),
1023        );
1024
1025        let remaining = cert.time_remaining_ms(now);
1026        assert!(remaining > 0);
1027        assert!(remaining <= one_hour_ms());
1028
1029        let remaining_expired = cert.time_remaining_ms(now + 2 * one_hour_ms());
1030        assert_eq!(remaining_expired, 0);
1031    }
1032
1033    #[test]
1034    fn test_delegation_chain_enroll_permission() {
1035        let authority = DeviceKeypair::generate();
1036        let delegator = DeviceKeypair::generate();
1037        let new_member = DeviceKeypair::generate();
1038        let now = now_ms();
1039
1040        // Authority issues cert to delegator with ENROLL permission
1041        let delegator_cert = make_cert(
1042            &authority,
1043            &delegator,
1044            "delegator",
1045            MeshTier::Regional,
1046            permissions::STANDARD | permissions::ENROLL,
1047            now,
1048            now + one_hour_ms(),
1049        );
1050
1051        // Delegator issues cert to new_member
1052        let delegated_cert = MeshCertificate::new(
1053            new_member.public_key_bytes(),
1054            "DEADBEEF".to_string(),
1055            "new-node".to_string(),
1056            MeshTier::Tactical,
1057            permissions::STANDARD,
1058            now,
1059            now + one_hour_ms(),
1060            delegator.public_key_bytes(),
1061        )
1062        .signed(&delegator);
1063
1064        let mut bundle = CertificateBundle::new();
1065        bundle.add_authority(authority.public_key_bytes());
1066
1067        // Add delegator cert first (signed by authority)
1068        bundle.add_certificate(delegator_cert).unwrap();
1069
1070        // Add delegated cert (signed by delegator with ENROLL)
1071        bundle.add_certificate(delegated_cert).unwrap();
1072
1073        assert!(bundle.validate_node_id("new-node", now));
1074        assert!(bundle.validate_node_id("delegator", now));
1075    }
1076
1077    #[test]
1078    fn test_delegation_chain_without_enroll_rejected() {
1079        let authority = DeviceKeypair::generate();
1080        let non_delegator = DeviceKeypair::generate();
1081        let new_member = DeviceKeypair::generate();
1082        let now = now_ms();
1083
1084        // Authority issues cert WITHOUT ENROLL permission
1085        let non_delegator_cert = make_cert(
1086            &authority,
1087            &non_delegator,
1088            "standard-node",
1089            MeshTier::Tactical,
1090            permissions::STANDARD, // No ENROLL
1091            now,
1092            now + one_hour_ms(),
1093        );
1094
1095        // Non-delegator tries to issue cert to new_member
1096        let invalid_cert = MeshCertificate::new(
1097            new_member.public_key_bytes(),
1098            "DEADBEEF".to_string(),
1099            "unauthorized-node".to_string(),
1100            MeshTier::Tactical,
1101            permissions::STANDARD,
1102            now,
1103            now + one_hour_ms(),
1104            non_delegator.public_key_bytes(),
1105        )
1106        .signed(&non_delegator);
1107
1108        let mut bundle = CertificateBundle::new();
1109        bundle.add_authority(authority.public_key_bytes());
1110        bundle.add_certificate(non_delegator_cert).unwrap();
1111
1112        // Should be rejected — non_delegator doesn't have ENROLL
1113        let result = bundle.add_certificate(invalid_cert);
1114        assert!(result.is_err());
1115    }
1116
1117    #[test]
1118    fn test_delegation_rejected_when_issuer_expired() {
1119        let authority = DeviceKeypair::generate();
1120        let delegator = DeviceKeypair::generate();
1121        let new_member = DeviceKeypair::generate();
1122        let now = now_ms();
1123
1124        // Authority issues cert to delegator with ENROLL, but it expired an hour ago
1125        let delegator_cert = make_cert(
1126            &authority,
1127            &delegator,
1128            "delegator",
1129            MeshTier::Regional,
1130            permissions::STANDARD | permissions::ENROLL,
1131            now - 2 * one_hour_ms(),
1132            now - one_hour_ms(), // already expired
1133        );
1134
1135        let mut bundle = CertificateBundle::new();
1136        bundle.add_authority(authority.public_key_bytes());
1137        // Use unchecked to force the expired cert into the bundle
1138        bundle.add_certificate_unchecked(delegator_cert);
1139
1140        // Delegator tries to issue cert to new_member
1141        let delegated_cert = MeshCertificate::new(
1142            new_member.public_key_bytes(),
1143            "DEADBEEF".to_string(),
1144            "new-node".to_string(),
1145            MeshTier::Tactical,
1146            permissions::STANDARD,
1147            now,
1148            now + one_hour_ms(),
1149            delegator.public_key_bytes(),
1150        )
1151        .signed(&delegator);
1152
1153        // Should be rejected — delegator's cert is expired
1154        let result = bundle.add_certificate(delegated_cert);
1155        assert!(result.is_err());
1156    }
1157}