1use rand_core::{OsRng, RngCore};
39
40use super::certificate::{MeshCertificate, MeshTier};
41use super::error::SecurityError;
42use super::keypair::DeviceKeypair;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum MembershipPolicy {
47 Open,
49
50 #[default]
52 Controlled,
53
54 Strict,
56}
57
58impl MembershipPolicy {
59 pub fn to_byte(self) -> u8 {
61 match self {
62 Self::Open => 0,
63 Self::Controlled => 1,
64 Self::Strict => 2,
65 }
66 }
67
68 pub fn from_byte(b: u8) -> Option<Self> {
70 match b {
71 0 => Some(Self::Open),
72 1 => Some(Self::Controlled),
73 2 => Some(Self::Strict),
74 _ => None,
75 }
76 }
77
78 pub fn from_str_name(s: &str) -> Option<Self> {
80 match s.trim().to_lowercase().as_str() {
81 "open" => Some(Self::Open),
82 "controlled" => Some(Self::Controlled),
83 "strict" => Some(Self::Strict),
84 _ => None,
85 }
86 }
87
88 pub fn as_str(&self) -> &'static str {
90 match self {
91 Self::Open => "Open",
92 Self::Controlled => "Controlled",
93 Self::Strict => "Strict",
94 }
95 }
96}
97
98impl std::fmt::Display for MembershipPolicy {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.write_str(self.as_str())
101 }
102}
103
104#[derive(Clone)]
120pub struct MeshGenesis {
121 pub mesh_name: String,
123
124 mesh_seed: [u8; 32],
126
127 authority: DeviceKeypair,
129
130 pub created_at_ms: u64,
132
133 pub policy: MembershipPolicy,
135}
136
137impl MeshGenesis {
138 const MESH_ID_CONTEXT: &'static str = "peat-mesh:mesh-id";
140
141 const FORMATION_SECRET_CONTEXT: &'static str = "peat-mesh:formation-secret";
143
144 const AUTHORITY_CONTEXT: &'static str = "peat-mesh:authority-keypair";
146
147 pub fn create(mesh_name: &str, policy: MembershipPolicy) -> Self {
151 let mut mesh_seed = [0u8; 32];
152 OsRng.fill_bytes(&mut mesh_seed);
153 Self::with_seed(mesh_name, mesh_seed, policy)
154 }
155
156 pub fn with_seed(mesh_name: &str, mesh_seed: [u8; 32], policy: MembershipPolicy) -> Self {
162 let authority =
163 DeviceKeypair::from_seed(&mesh_seed, Self::AUTHORITY_CONTEXT).expect("HKDF infallible");
164 Self {
165 mesh_name: mesh_name.into(),
166 mesh_seed,
167 authority,
168 created_at_ms: now_ms(),
169 policy,
170 }
171 }
172
173 pub fn with_authority(
178 mesh_name: &str,
179 mesh_seed: [u8; 32],
180 authority: DeviceKeypair,
181 policy: MembershipPolicy,
182 ) -> Self {
183 Self {
184 mesh_name: mesh_name.into(),
185 mesh_seed,
186 authority,
187 created_at_ms: now_ms(),
188 policy,
189 }
190 }
191
192 pub fn mesh_id(&self) -> String {
197 let hash = self.derive(Self::MESH_ID_CONTEXT);
198 format!(
199 "{:02X}{:02X}{:02X}{:02X}",
200 hash[0], hash[1], hash[2], hash[3]
201 )
202 }
203
204 pub fn formation_secret(&self) -> [u8; 32] {
213 self.derive(Self::FORMATION_SECRET_CONTEXT)
214 }
215
216 pub fn authority(&self) -> &DeviceKeypair {
218 &self.authority
219 }
220
221 pub fn authority_public_key(&self) -> [u8; 32] {
223 self.authority.public_key_bytes()
224 }
225
226 pub fn mesh_seed(&self) -> &[u8; 32] {
230 &self.mesh_seed
231 }
232
233 pub fn root_certificate(&self, node_id: &str) -> MeshCertificate {
241 let now = now_ms();
242 MeshCertificate::new_root(
243 &self.authority,
244 self.mesh_id(),
245 node_id.to_string(),
246 MeshTier::Enterprise,
247 now,
248 0, )
250 }
251
252 #[allow(clippy::too_many_arguments)]
256 pub fn issue_certificate(
257 &self,
258 subject_public_key: [u8; 32],
259 node_id: &str,
260 tier: MeshTier,
261 permissions: u8,
262 validity_ms: u64,
263 ) -> MeshCertificate {
264 let now = now_ms();
265 let expires = if validity_ms == 0 {
266 0
267 } else {
268 now + validity_ms
269 };
270 MeshCertificate::new(
271 subject_public_key,
272 self.mesh_id(),
273 node_id.to_string(),
274 tier,
275 permissions,
276 now,
277 expires,
278 self.authority.public_key_bytes(),
279 )
280 .signed(&self.authority)
281 }
282
283 pub fn credentials(&self) -> MeshCredentials {
285 MeshCredentials {
286 mesh_id: self.mesh_id(),
287 mesh_name: self.mesh_name.clone(),
288 formation_secret: self.formation_secret(),
289 authority_public_key: self.authority_public_key(),
290 policy: self.policy,
291 }
292 }
293
294 pub fn encode(&self) -> Vec<u8> {
306 let name_bytes = self.mesh_name.as_bytes();
307 let mut buf = Vec::with_capacity(75 + name_bytes.len());
308
309 buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
310 buf.extend_from_slice(name_bytes);
311 buf.extend_from_slice(&self.mesh_seed);
312 buf.extend_from_slice(&self.authority.secret_key_bytes());
313 buf.extend_from_slice(&self.created_at_ms.to_le_bytes());
314 buf.push(self.policy.to_byte());
315
316 buf
317 }
318
319 pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
321 if data.len() < 75 {
323 return Err(SecurityError::SerializationError(format!(
324 "genesis too short: {} bytes (min 75)",
325 data.len()
326 )));
327 }
328
329 let name_len = u16::from_le_bytes([data[0], data[1]]) as usize;
330 if data.len() < 75 + name_len {
331 return Err(SecurityError::SerializationError(
332 "genesis truncated at mesh_name".to_string(),
333 ));
334 }
335
336 let mesh_name = String::from_utf8(data[2..2 + name_len].to_vec())
337 .map_err(|e| SecurityError::SerializationError(format!("invalid mesh_name: {e}")))?;
338 let offset = 2 + name_len;
339
340 let mut mesh_seed = [0u8; 32];
341 mesh_seed.copy_from_slice(&data[offset..offset + 32]);
342
343 let authority = DeviceKeypair::from_secret_bytes(&data[offset + 32..offset + 64])?;
344
345 let created_at_ms = u64::from_le_bytes(data[offset + 64..offset + 72].try_into().unwrap());
346
347 let policy = MembershipPolicy::from_byte(data[offset + 72])
348 .ok_or_else(|| SecurityError::SerializationError("invalid policy byte".to_string()))?;
349
350 Ok(Self {
351 mesh_name,
352 mesh_seed,
353 authority,
354 created_at_ms,
355 policy,
356 })
357 }
358
359 fn derive(&self, context: &str) -> [u8; 32] {
361 use hkdf::Hkdf;
362 use sha2::Sha256;
363
364 let hk = Hkdf::<Sha256>::new(Some(self.mesh_name.as_bytes()), &self.mesh_seed);
365 let mut okm = [0u8; 32];
366 hk.expand(context.as_bytes(), &mut okm)
367 .expect("32-byte output is within HKDF limit");
368 okm
369 }
370}
371
372impl std::fmt::Debug for MeshGenesis {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.debug_struct("MeshGenesis")
375 .field("mesh_name", &self.mesh_name)
376 .field("mesh_id", &self.mesh_id())
377 .field("authority_device_id", &self.authority.device_id())
378 .field("created_at_ms", &self.created_at_ms)
379 .field("policy", &self.policy)
380 .field("mesh_seed", &"[REDACTED]")
381 .finish()
382 }
383}
384
385#[derive(Debug, Clone)]
391pub struct MeshCredentials {
392 pub mesh_id: String,
394
395 pub mesh_name: String,
397
398 pub formation_secret: [u8; 32],
400
401 pub authority_public_key: [u8; 32],
403
404 pub policy: MembershipPolicy,
406}
407
408impl MeshCredentials {
409 pub fn encode(&self) -> Vec<u8> {
421 let name_bytes = self.mesh_name.as_bytes();
422 let mesh_id_bytes = self.mesh_id.as_bytes();
423 let mut buf = Vec::with_capacity(75 + name_bytes.len());
424
425 buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
426 buf.extend_from_slice(name_bytes);
427 buf.extend_from_slice(mesh_id_bytes);
428 buf.extend_from_slice(&self.formation_secret);
429 buf.extend_from_slice(&self.authority_public_key);
430 buf.push(self.policy.to_byte());
431
432 buf
433 }
434
435 pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
437 if data.len() < 75 {
439 return Err(SecurityError::SerializationError(format!(
440 "credentials too short: {} bytes (min 75)",
441 data.len()
442 )));
443 }
444
445 let name_len = u16::from_le_bytes([data[0], data[1]]) as usize;
446 if data.len() < 75 + name_len {
447 return Err(SecurityError::SerializationError(
448 "credentials truncated at mesh_name".to_string(),
449 ));
450 }
451
452 let mesh_name = String::from_utf8(data[2..2 + name_len].to_vec())
453 .map_err(|e| SecurityError::SerializationError(format!("invalid mesh_name: {e}")))?;
454 let offset = 2 + name_len;
455
456 let mesh_id = String::from_utf8(data[offset..offset + 8].to_vec())
457 .map_err(|e| SecurityError::SerializationError(format!("invalid mesh_id: {e}")))?;
458
459 let mut formation_secret = [0u8; 32];
460 formation_secret.copy_from_slice(&data[offset + 8..offset + 40]);
461
462 let mut authority_public_key = [0u8; 32];
463 authority_public_key.copy_from_slice(&data[offset + 40..offset + 72]);
464
465 let policy = MembershipPolicy::from_byte(data[offset + 72])
466 .ok_or_else(|| SecurityError::SerializationError("invalid policy byte".to_string()))?;
467
468 Ok(Self {
469 mesh_id,
470 mesh_name,
471 formation_secret,
472 authority_public_key,
473 policy,
474 })
475 }
476}
477
478fn now_ms() -> u64 {
480 std::time::SystemTime::now()
481 .duration_since(std::time::UNIX_EPOCH)
482 .map(|d| d.as_millis() as u64)
483 .unwrap_or(0)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::super::certificate::permissions;
489 use super::*;
490
491 #[test]
492 fn test_create_genesis() {
493 let genesis = MeshGenesis::create("ALPHA-TEAM", MembershipPolicy::Controlled);
494
495 assert_eq!(genesis.mesh_name, "ALPHA-TEAM");
496 assert_eq!(genesis.policy, MembershipPolicy::Controlled);
497 assert!(genesis.created_at_ms > 0);
498 }
499
500 #[test]
501 fn test_mesh_id_format() {
502 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
503 let mesh_id = genesis.mesh_id();
504
505 assert_eq!(mesh_id.len(), 8);
506 assert!(mesh_id
507 .chars()
508 .all(|c| c.is_ascii_hexdigit() && !c.is_lowercase()));
509 }
510
511 #[test]
512 fn test_mesh_id_deterministic() {
513 let seed = [0x42u8; 32];
514 let genesis = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
515
516 assert_eq!(genesis.mesh_id(), genesis.mesh_id());
517 }
518
519 #[test]
520 fn test_different_names_different_ids() {
521 let seed = [0x42u8; 32];
522 let g1 = MeshGenesis::with_seed("ALPHA", seed, MembershipPolicy::Open);
523 let g2 = MeshGenesis::with_seed("BRAVO", seed, MembershipPolicy::Open);
524
525 assert_ne!(g1.mesh_id(), g2.mesh_id());
526 }
527
528 #[test]
529 fn test_different_seeds_different_ids() {
530 let g1 = MeshGenesis::with_seed("TEST", [0x42u8; 32], MembershipPolicy::Open);
531 let g2 = MeshGenesis::with_seed("TEST", [0x43u8; 32], MembershipPolicy::Open);
532
533 assert_ne!(g1.mesh_id(), g2.mesh_id());
534 }
535
536 #[test]
537 fn test_formation_secret_deterministic() {
538 let seed = [0x42u8; 32];
539 let genesis = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
540
541 let s1 = genesis.formation_secret();
542 let s2 = genesis.formation_secret();
543
544 assert_eq!(s1, s2);
545 assert_ne!(s1, seed); }
547
548 #[test]
549 fn test_formation_secret_differs_from_mesh_id_source() {
550 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
551 let formation = genesis.formation_secret();
552 let mesh_id_bytes = genesis.derive(MeshGenesis::MESH_ID_CONTEXT);
553
554 assert_ne!(formation, mesh_id_bytes); }
556
557 #[test]
558 fn test_authority_keypair_deterministic() {
559 let seed = [0x42u8; 32];
560 let g1 = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
561 let g2 = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
562
563 assert_eq!(g1.authority_public_key(), g2.authority_public_key());
564 }
565
566 #[test]
567 fn test_authority_can_sign_and_verify() {
568 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
569 let msg = b"hello mesh";
570 let sig = genesis.authority().sign(msg);
571 assert!(genesis.authority().verify(msg, &sig).is_ok());
572 }
573
574 #[test]
575 fn test_root_certificate() {
576 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Controlled);
577 let root = genesis.root_certificate("enterprise-0");
578
579 assert!(root.verify().is_ok());
580 assert!(root.is_root());
581 assert_eq!(root.mesh_id, genesis.mesh_id());
582 assert_eq!(root.node_id, "enterprise-0");
583 assert_eq!(root.tier, MeshTier::Enterprise);
584 assert_eq!(root.permissions, permissions::AUTHORITY);
585 assert_eq!(root.expires_at_ms, 0); assert_eq!(root.subject_public_key, genesis.authority_public_key());
587 assert_eq!(root.issuer_public_key, genesis.authority_public_key());
588 }
589
590 #[test]
591 fn test_issue_certificate() {
592 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Controlled);
593 let member = DeviceKeypair::generate();
594
595 let cert = genesis.issue_certificate(
596 member.public_key_bytes(),
597 "tac-west-1",
598 MeshTier::Tactical,
599 permissions::STANDARD,
600 24 * 60 * 60 * 1000, );
602
603 assert!(cert.verify().is_ok());
604 assert!(!cert.is_root());
605 assert_eq!(cert.mesh_id, genesis.mesh_id());
606 assert_eq!(cert.node_id, "tac-west-1");
607 assert_eq!(cert.tier, MeshTier::Tactical);
608 assert_eq!(cert.permissions, permissions::STANDARD);
609 assert_eq!(cert.subject_public_key, member.public_key_bytes());
610 assert_eq!(cert.issuer_public_key, genesis.authority_public_key());
611 assert!(cert.expires_at_ms > cert.issued_at_ms);
612 }
613
614 #[test]
615 fn test_issue_certificate_no_expiration() {
616 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
617 let member = DeviceKeypair::generate();
618
619 let cert = genesis.issue_certificate(
620 member.public_key_bytes(),
621 "hub-1",
622 MeshTier::Regional,
623 permissions::STANDARD | permissions::ENROLL,
624 0, );
626
627 assert!(cert.verify().is_ok());
628 assert_eq!(cert.expires_at_ms, 0);
629 }
630
631 #[test]
632 fn test_credentials() {
633 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Controlled);
634 let creds = genesis.credentials();
635
636 assert_eq!(creds.mesh_id, genesis.mesh_id());
637 assert_eq!(creds.mesh_name, genesis.mesh_name);
638 assert_eq!(creds.formation_secret, genesis.formation_secret());
639 assert_eq!(creds.authority_public_key, genesis.authority_public_key());
640 assert_eq!(creds.policy, genesis.policy);
641 }
642
643 #[test]
644 fn test_encode_decode_genesis_roundtrip() {
645 let genesis = MeshGenesis::create("ALPHA-TEAM", MembershipPolicy::Strict);
646 let encoded = genesis.encode();
647 let decoded = MeshGenesis::decode(&encoded).unwrap();
648
649 assert_eq!(decoded.mesh_name, genesis.mesh_name);
650 assert_eq!(decoded.mesh_id(), genesis.mesh_id());
651 assert_eq!(decoded.formation_secret(), genesis.formation_secret());
652 assert_eq!(
653 decoded.authority_public_key(),
654 genesis.authority_public_key()
655 );
656 assert_eq!(decoded.policy, genesis.policy);
657 }
658
659 #[test]
660 fn test_decode_genesis_too_short() {
661 assert!(MeshGenesis::decode(&[0u8; 10]).is_err());
662 }
663
664 #[test]
665 fn test_decode_genesis_invalid_policy() {
666 let genesis = MeshGenesis::create("X", MembershipPolicy::Open);
667 let mut encoded = genesis.encode();
668 *encoded.last_mut().unwrap() = 99;
670 assert!(MeshGenesis::decode(&encoded).is_err());
671 }
672
673 #[test]
674 fn test_encode_decode_credentials_roundtrip() {
675 let genesis = MeshGenesis::create("BRAVO-NET", MembershipPolicy::Controlled);
676 let creds = genesis.credentials();
677 let encoded = creds.encode();
678 let decoded = MeshCredentials::decode(&encoded).unwrap();
679
680 assert_eq!(decoded.mesh_id, creds.mesh_id);
681 assert_eq!(decoded.mesh_name, creds.mesh_name);
682 assert_eq!(decoded.formation_secret, creds.formation_secret);
683 assert_eq!(decoded.authority_public_key, creds.authority_public_key);
684 assert_eq!(decoded.policy, creds.policy);
685 }
686
687 #[test]
688 fn test_decode_credentials_too_short() {
689 assert!(MeshCredentials::decode(&[0u8; 10]).is_err());
690 }
691
692 #[test]
693 fn test_with_authority_external_keypair() {
694 let external_authority = DeviceKeypair::generate();
695 let seed = [0x42u8; 32];
696 let genesis = MeshGenesis::with_authority(
697 "HSM-MESH",
698 seed,
699 external_authority.clone(),
700 MembershipPolicy::Strict,
701 );
702
703 assert_eq!(
704 genesis.authority_public_key(),
705 external_authority.public_key_bytes()
706 );
707 let derived = MeshGenesis::with_seed("HSM-MESH", seed, MembershipPolicy::Strict);
709 assert_ne!(
710 genesis.authority_public_key(),
711 derived.authority_public_key()
712 );
713 }
714
715 #[test]
716 fn test_policy_default() {
717 assert_eq!(MembershipPolicy::default(), MembershipPolicy::Controlled);
718 }
719
720 #[test]
721 fn test_policy_from_str_name() {
722 assert_eq!(
723 MembershipPolicy::from_str_name("open"),
724 Some(MembershipPolicy::Open)
725 );
726 assert_eq!(
727 MembershipPolicy::from_str_name("CONTROLLED"),
728 Some(MembershipPolicy::Controlled)
729 );
730 assert_eq!(
731 MembershipPolicy::from_str_name(" Strict "),
732 Some(MembershipPolicy::Strict)
733 );
734 assert_eq!(MembershipPolicy::from_str_name("invalid"), None);
735 }
736
737 #[test]
738 fn test_policy_byte_roundtrip() {
739 for policy in [
740 MembershipPolicy::Open,
741 MembershipPolicy::Controlled,
742 MembershipPolicy::Strict,
743 ] {
744 assert_eq!(MembershipPolicy::from_byte(policy.to_byte()), Some(policy));
745 }
746 assert_eq!(MembershipPolicy::from_byte(99), None);
747 }
748
749 #[test]
750 fn test_debug_redacts_seed() {
751 let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
752 let debug_str = format!("{:?}", genesis);
753 assert!(debug_str.contains("REDACTED"));
754 assert!(debug_str.contains("mesh_id"));
755 let seed_hex = hex::encode(genesis.mesh_seed());
757 assert!(!debug_str.contains(&seed_hex));
758 }
759}