Skip to main content

peat_mesh/security/
genesis.rs

1//! Mesh genesis protocol for creating new Peat mesh formations.
2//!
3//! A mesh is created through a genesis event where:
4//! - A 256-bit cryptographic seed is generated (CSPRNG)
5//! - The mesh_id is derived from the name and seed
6//! - The formation_secret is derived from the seed (used for Iroh EndpointId HKDF)
7//! - The creator's keypair becomes the initial authority
8//! - A self-signed root certificate is issued
9//!
10//! # Key Derivation
11//!
12//! All secrets are derived from the mesh_seed via HKDF-SHA256 with distinct
13//! context strings, ensuring domain separation:
14//!
15//! ```text
16//! mesh_seed (256-bit, CSPRNG)
17//!     │
18//!     ├── HKDF("peat-mesh:mesh-id") ──► mesh_id (first 4 bytes → 8 hex chars)
19//!     ├── HKDF("peat-mesh:formation-secret") ──► formation_secret (32 bytes)
20//!     └── HKDF("peat-mesh:authority-keypair") ──► authority Ed25519 keypair
21//! ```
22//!
23//! # Example
24//!
25//! ```
26//! use peat_mesh::security::{MeshGenesis, MembershipPolicy, DeviceKeypair};
27//!
28//! // Create a new mesh (authority keypair derived from seed)
29//! let genesis = MeshGenesis::create("ALPHA-TEAM", MembershipPolicy::Controlled);
30//!
31//! // Get derived values
32//! let mesh_id = genesis.mesh_id();             // e.g., "A1B2C3D4"
33//! let formation_secret = genesis.formation_secret();  // 32 bytes
34//! let authority = genesis.authority();          // DeviceKeypair
35//! let root_cert = genesis.root_certificate("authority-0");  // self-signed MeshCertificate
36//! ```
37
38use rand_core::{OsRng, RngCore};
39
40use super::certificate::{MeshCertificate, MeshTier};
41use super::error::SecurityError;
42use super::keypair::DeviceKeypair;
43
44/// Membership policy controlling how nodes can join the mesh.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum MembershipPolicy {
47    /// Anyone with formation_secret can join. Least secure.
48    Open,
49
50    /// Explicit enrollment by an authority is required. Default.
51    #[default]
52    Controlled,
53
54    /// Only pre-provisioned devices can join. Highest security.
55    Strict,
56}
57
58impl MembershipPolicy {
59    /// Encode as a single byte.
60    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    /// Decode from a single byte.
69    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    /// Parse from a string (case-insensitive).
79    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    /// Get the policy name as a string.
89    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/// Genesis event for creating a new mesh formation.
105///
106/// Contains all cryptographic material needed to bootstrap a mesh from zero.
107/// The genesis artifact is the root of trust — the authority keypair signs
108/// all certificates and the formation_secret authenticates transport.
109///
110/// # Security
111///
112/// The `mesh_seed` is the root secret. Protect it carefully:
113/// - Store encrypted at rest
114/// - Never transmit over the network
115/// - Only the genesis creator needs it (for recovery)
116///
117/// Shareable credentials (via [`MeshCredentials`]) exclude the seed and
118/// authority private key.
119#[derive(Clone)]
120pub struct MeshGenesis {
121    /// Human-readable mesh name.
122    pub mesh_name: String,
123
124    /// 256-bit cryptographic seed (generated from CSPRNG).
125    mesh_seed: [u8; 32],
126
127    /// Authority Ed25519 keypair (derived from mesh_seed).
128    authority: DeviceKeypair,
129
130    /// Timestamp of creation (milliseconds since Unix epoch).
131    pub created_at_ms: u64,
132
133    /// Membership policy for this mesh.
134    pub policy: MembershipPolicy,
135}
136
137impl MeshGenesis {
138    /// HKDF context for mesh_id derivation.
139    const MESH_ID_CONTEXT: &'static str = "peat-mesh:mesh-id";
140
141    /// HKDF context for formation secret derivation.
142    const FORMATION_SECRET_CONTEXT: &'static str = "peat-mesh:formation-secret";
143
144    /// HKDF context for authority keypair derivation.
145    const AUTHORITY_CONTEXT: &'static str = "peat-mesh:authority-keypair";
146
147    /// Create a new mesh formation with a random seed.
148    ///
149    /// The authority keypair is deterministically derived from the seed.
150    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    /// Create a genesis with a specific seed (for testing or deterministic creation).
157    ///
158    /// # Safety
159    ///
160    /// Only use with cryptographically random seeds in production.
161    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    /// Create a genesis with a specific seed and an externally-provided authority keypair.
174    ///
175    /// Use when the authority keypair is generated independently (e.g., from a
176    /// hardware security module) rather than derived from the seed.
177    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    /// Derive the mesh_id from name and seed.
193    ///
194    /// The mesh_id is 8 hex characters derived from HKDF-SHA256.
195    /// Format: uppercase hex, e.g., "A1B2C3D4".
196    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    /// Derive the formation secret.
205    ///
206    /// The formation secret is shared with all mesh members and used for
207    /// HKDF-based Iroh EndpointId derivation:
208    ///
209    /// ```text
210    /// HKDF(formation_secret, "iroh:" + node_id) → EndpointId
211    /// ```
212    pub fn formation_secret(&self) -> [u8; 32] {
213        self.derive(Self::FORMATION_SECRET_CONTEXT)
214    }
215
216    /// Get the authority keypair.
217    pub fn authority(&self) -> &DeviceKeypair {
218        &self.authority
219    }
220
221    /// Get the authority's public key bytes.
222    pub fn authority_public_key(&self) -> [u8; 32] {
223        self.authority.public_key_bytes()
224    }
225
226    /// Get the mesh seed for secure storage.
227    ///
228    /// **Security**: This is the root secret. Protect it carefully.
229    pub fn mesh_seed(&self) -> &[u8; 32] {
230        &self.mesh_seed
231    }
232
233    /// Generate a self-signed root certificate for the authority node.
234    ///
235    /// The root cert identifies the genesis authority in the mesh:
236    /// - `subject_public_key` = `issuer_public_key` (self-signed)
237    /// - `tier` = Enterprise (highest trust)
238    /// - `permissions` = AUTHORITY (all permissions)
239    /// - `expires_at_ms` = 0 (no expiration, root cert is permanent)
240    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, // No expiration for root cert
249        )
250    }
251
252    /// Issue a signed certificate for a new member.
253    ///
254    /// This is a convenience method for the genesis authority to enroll a node.
255    #[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    /// Build shareable credentials (no seed, no authority private key).
284    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    /// Encode genesis data for secure persistence.
295    ///
296    /// Format:
297    /// - mesh_name length (2 bytes, LE)
298    /// - mesh_name (variable)
299    /// - mesh_seed (32 bytes)
300    /// - authority secret key (32 bytes) — SENSITIVE!
301    /// - created_at_ms (8 bytes, LE)
302    /// - policy (1 byte)
303    ///
304    /// Total: 75 + mesh_name.len() bytes
305    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    /// Decode genesis data from bytes.
320    pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
321        // Minimum: 2 + 0 + 32 + 32 + 8 + 1 = 75
322        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    /// Derive 32 bytes from the mesh_seed with a context string.
360    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/// Shareable mesh credentials (no seed, no authority private key).
386///
387/// This can be distributed to nodes joining the mesh. It includes everything
388/// needed for transport (formation_secret) and certificate validation
389/// (authority_public_key), but NOT the ability to issue certificates.
390#[derive(Debug, Clone)]
391pub struct MeshCredentials {
392    /// The mesh_id (derived, for verification).
393    pub mesh_id: String,
394
395    /// Mesh name.
396    pub mesh_name: String,
397
398    /// Formation secret for HKDF-based Iroh EndpointId derivation.
399    pub formation_secret: [u8; 32],
400
401    /// Authority's public key (for certificate verification).
402    pub authority_public_key: [u8; 32],
403
404    /// Membership policy.
405    pub policy: MembershipPolicy,
406}
407
408impl MeshCredentials {
409    /// Encode for distribution (e.g., QR code, config file).
410    ///
411    /// Format:
412    /// - mesh_name length (2 bytes, LE)
413    /// - mesh_name (variable)
414    /// - mesh_id (8 bytes, ASCII hex)
415    /// - formation_secret (32 bytes)
416    /// - authority_public_key (32 bytes)
417    /// - policy (1 byte)
418    ///
419    /// Total: 75 + mesh_name.len() bytes
420    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    /// Decode from bytes.
436    pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
437        // Minimum: 2 + 0 + 8 + 32 + 32 + 1 = 75
438        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
478/// Get current timestamp in milliseconds.
479fn 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); // Derived, not the seed itself
546    }
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); // Different HKDF contexts
555    }
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); // No expiration
586        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, // 24 hours
601        );
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, // No expiration
625        );
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        // Corrupt the policy byte (last byte)
669        *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        // External authority ≠ seed-derived authority
708        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        // Seed value should not appear (only the field name with [REDACTED])
756        let seed_hex = hex::encode(genesis.mesh_seed());
757        assert!(!debug_str.contains(&seed_hex));
758    }
759}