1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
34#[repr(u8)]
35pub enum MeshTier {
36 Enterprise = 0,
38 Regional = 1,
40 Tactical = 2,
42 Edge = 3,
44}
45
46impl MeshTier {
47 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 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 pub fn to_byte(self) -> u8 {
70 self as u8
71 }
72
73 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
91pub mod permissions {
95 pub const RELAY: u8 = 0b0000_0001;
97 pub const EMERGENCY: u8 = 0b0000_0010;
99 pub const ENROLL: u8 = 0b0000_0100;
101 pub const ADMIN: u8 = 0b1000_0000;
103 pub const STANDARD: u8 = RELAY | EMERGENCY;
105 pub const AUTHORITY: u8 = RELAY | EMERGENCY | ENROLL | ADMIN;
107}
108
109#[derive(Clone, Debug)]
118pub struct MeshCertificate {
119 pub subject_public_key: [u8; 32],
121 pub mesh_id: String,
123 pub node_id: String,
126 pub tier: MeshTier,
128 pub permissions: u8,
130 pub issued_at_ms: u64,
132 pub expires_at_ms: u64,
134 pub issuer_public_key: [u8; 32],
136 pub signature: [u8; 64],
138}
139
140impl MeshCertificate {
141 #[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 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 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 pub fn signed(mut self, issuer: &DeviceKeypair) -> Self {
199 self.sign_with(issuer);
200 self
201 }
202
203 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 pub fn is_root(&self) -> bool {
215 self.subject_public_key == self.issuer_public_key
216 }
217
218 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 pub fn has_permission(&self, perm: u8) -> bool {
225 self.permissions & perm == perm
226 }
227
228 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 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 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 pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
261 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#[derive(Debug, Default)]
340pub struct CertificateBundle {
341 authorities: Vec<[u8; 32]>,
343 certificates: HashMap<[u8; 32], MeshCertificate>,
345 node_id_index: HashMap<String, [u8; 32]>,
347}
348
349impl CertificateBundle {
350 pub fn new() -> Self {
352 Self::default()
353 }
354
355 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 pub fn add_certificate(&mut self, cert: MeshCertificate) -> Result<(), SecurityError> {
372 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 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 fn is_trusted_issuer(&self, issuer_key: &[u8; 32], now_ms: u64) -> bool {
401 if self.authorities.contains(issuer_key) {
403 return true;
404 }
405
406 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 if issuer_cert.verify().is_ok() {
411 return true;
412 }
413 }
414 }
415
416 false
417 }
418
419 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 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 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 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 pub fn get_certificate(&self, public_key: &[u8; 32]) -> Option<&MeshCertificate> {
469 self.certificates.get(public_key)
470 }
471
472 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 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 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 pub fn len(&self) -> usize {
508 self.certificates.len()
509 }
510
511 pub fn is_empty(&self) -> bool {
513 self.certificates.is_empty()
514 }
515
516 pub fn authority_count(&self) -> usize {
518 self.authorities.len()
519 }
520
521 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 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 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 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 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 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 assert!(bundle.validate_node_id("tactical-west-3", now));
872 assert!(!bundle.validate_node_id("unknown-node", now));
873
874 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 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 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 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 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 bundle.add_certificate(delegator_cert).unwrap();
1069
1070 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 let non_delegator_cert = make_cert(
1086 &authority,
1087 &non_delegator,
1088 "standard-node",
1089 MeshTier::Tactical,
1090 permissions::STANDARD, now,
1092 now + one_hour_ms(),
1093 );
1094
1095 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 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 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(), );
1134
1135 let mut bundle = CertificateBundle::new();
1136 bundle.add_authority(authority.public_key_bytes());
1137 bundle.add_certificate_unchecked(delegator_cert);
1139
1140 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 let result = bundle.add_certificate(delegated_cert);
1155 assert!(result.is_err());
1156 }
1157}