use rand_core::{OsRng, RngCore};
use super::certificate::{MeshCertificate, MeshTier};
use super::error::SecurityError;
use super::keypair::DeviceKeypair;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MembershipPolicy {
Open,
#[default]
Controlled,
Strict,
}
impl MembershipPolicy {
pub fn to_byte(self) -> u8 {
match self {
Self::Open => 0,
Self::Controlled => 1,
Self::Strict => 2,
}
}
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(Self::Open),
1 => Some(Self::Controlled),
2 => Some(Self::Strict),
_ => None,
}
}
pub fn from_str_name(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"open" => Some(Self::Open),
"controlled" => Some(Self::Controlled),
"strict" => Some(Self::Strict),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Open => "Open",
Self::Controlled => "Controlled",
Self::Strict => "Strict",
}
}
}
impl std::fmt::Display for MembershipPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone)]
pub struct MeshGenesis {
pub mesh_name: String,
mesh_seed: [u8; 32],
authority: DeviceKeypair,
pub created_at_ms: u64,
pub policy: MembershipPolicy,
}
impl MeshGenesis {
const MESH_ID_CONTEXT: &'static str = "peat-mesh:mesh-id";
const FORMATION_SECRET_CONTEXT: &'static str = "peat-mesh:formation-secret";
const AUTHORITY_CONTEXT: &'static str = "peat-mesh:authority-keypair";
pub fn create(mesh_name: &str, policy: MembershipPolicy) -> Self {
let mut mesh_seed = [0u8; 32];
OsRng.fill_bytes(&mut mesh_seed);
Self::with_seed(mesh_name, mesh_seed, policy)
}
pub fn with_seed(mesh_name: &str, mesh_seed: [u8; 32], policy: MembershipPolicy) -> Self {
let authority =
DeviceKeypair::from_seed(&mesh_seed, Self::AUTHORITY_CONTEXT).expect("HKDF infallible");
Self {
mesh_name: mesh_name.into(),
mesh_seed,
authority,
created_at_ms: now_ms(),
policy,
}
}
pub fn with_authority(
mesh_name: &str,
mesh_seed: [u8; 32],
authority: DeviceKeypair,
policy: MembershipPolicy,
) -> Self {
Self {
mesh_name: mesh_name.into(),
mesh_seed,
authority,
created_at_ms: now_ms(),
policy,
}
}
pub fn mesh_id(&self) -> String {
let hash = self.derive(Self::MESH_ID_CONTEXT);
format!(
"{:02X}{:02X}{:02X}{:02X}",
hash[0], hash[1], hash[2], hash[3]
)
}
pub fn formation_secret(&self) -> [u8; 32] {
self.derive(Self::FORMATION_SECRET_CONTEXT)
}
pub fn authority(&self) -> &DeviceKeypair {
&self.authority
}
pub fn authority_public_key(&self) -> [u8; 32] {
self.authority.public_key_bytes()
}
pub fn mesh_seed(&self) -> &[u8; 32] {
&self.mesh_seed
}
pub fn root_certificate(&self, node_id: &str) -> MeshCertificate {
let now = now_ms();
MeshCertificate::new_root(
&self.authority,
self.mesh_id(),
node_id.to_string(),
MeshTier::Enterprise,
now,
0, )
}
#[allow(clippy::too_many_arguments)]
pub fn issue_certificate(
&self,
subject_public_key: [u8; 32],
node_id: &str,
tier: MeshTier,
permissions: u8,
validity_ms: u64,
) -> MeshCertificate {
let now = now_ms();
let expires = if validity_ms == 0 {
0
} else {
now + validity_ms
};
MeshCertificate::new(
subject_public_key,
self.mesh_id(),
node_id.to_string(),
tier,
permissions,
now,
expires,
self.authority.public_key_bytes(),
)
.signed(&self.authority)
}
pub fn credentials(&self) -> MeshCredentials {
MeshCredentials {
mesh_id: self.mesh_id(),
mesh_name: self.mesh_name.clone(),
formation_secret: self.formation_secret(),
authority_public_key: self.authority_public_key(),
policy: self.policy,
}
}
pub fn encode(&self) -> Vec<u8> {
let name_bytes = self.mesh_name.as_bytes();
let mut buf = Vec::with_capacity(75 + name_bytes.len());
buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(name_bytes);
buf.extend_from_slice(&self.mesh_seed);
buf.extend_from_slice(&self.authority.secret_key_bytes());
buf.extend_from_slice(&self.created_at_ms.to_le_bytes());
buf.push(self.policy.to_byte());
buf
}
pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
if data.len() < 75 {
return Err(SecurityError::SerializationError(format!(
"genesis too short: {} bytes (min 75)",
data.len()
)));
}
let name_len = u16::from_le_bytes([data[0], data[1]]) as usize;
if data.len() < 75 + name_len {
return Err(SecurityError::SerializationError(
"genesis truncated at mesh_name".to_string(),
));
}
let mesh_name = String::from_utf8(data[2..2 + name_len].to_vec())
.map_err(|e| SecurityError::SerializationError(format!("invalid mesh_name: {e}")))?;
let offset = 2 + name_len;
let mut mesh_seed = [0u8; 32];
mesh_seed.copy_from_slice(&data[offset..offset + 32]);
let authority = DeviceKeypair::from_secret_bytes(&data[offset + 32..offset + 64])?;
let created_at_ms = u64::from_le_bytes(data[offset + 64..offset + 72].try_into().unwrap());
let policy = MembershipPolicy::from_byte(data[offset + 72])
.ok_or_else(|| SecurityError::SerializationError("invalid policy byte".to_string()))?;
Ok(Self {
mesh_name,
mesh_seed,
authority,
created_at_ms,
policy,
})
}
fn derive(&self, context: &str) -> [u8; 32] {
use hkdf::Hkdf;
use sha2::Sha256;
let hk = Hkdf::<Sha256>::new(Some(self.mesh_name.as_bytes()), &self.mesh_seed);
let mut okm = [0u8; 32];
hk.expand(context.as_bytes(), &mut okm)
.expect("32-byte output is within HKDF limit");
okm
}
}
impl std::fmt::Debug for MeshGenesis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MeshGenesis")
.field("mesh_name", &self.mesh_name)
.field("mesh_id", &self.mesh_id())
.field("authority_device_id", &self.authority.device_id())
.field("created_at_ms", &self.created_at_ms)
.field("policy", &self.policy)
.field("mesh_seed", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone)]
pub struct MeshCredentials {
pub mesh_id: String,
pub mesh_name: String,
pub formation_secret: [u8; 32],
pub authority_public_key: [u8; 32],
pub policy: MembershipPolicy,
}
impl MeshCredentials {
pub fn encode(&self) -> Vec<u8> {
let name_bytes = self.mesh_name.as_bytes();
let mesh_id_bytes = self.mesh_id.as_bytes();
let mut buf = Vec::with_capacity(75 + name_bytes.len());
buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(name_bytes);
buf.extend_from_slice(mesh_id_bytes);
buf.extend_from_slice(&self.formation_secret);
buf.extend_from_slice(&self.authority_public_key);
buf.push(self.policy.to_byte());
buf
}
pub fn decode(data: &[u8]) -> Result<Self, SecurityError> {
if data.len() < 75 {
return Err(SecurityError::SerializationError(format!(
"credentials too short: {} bytes (min 75)",
data.len()
)));
}
let name_len = u16::from_le_bytes([data[0], data[1]]) as usize;
if data.len() < 75 + name_len {
return Err(SecurityError::SerializationError(
"credentials truncated at mesh_name".to_string(),
));
}
let mesh_name = String::from_utf8(data[2..2 + name_len].to_vec())
.map_err(|e| SecurityError::SerializationError(format!("invalid mesh_name: {e}")))?;
let offset = 2 + name_len;
let mesh_id = String::from_utf8(data[offset..offset + 8].to_vec())
.map_err(|e| SecurityError::SerializationError(format!("invalid mesh_id: {e}")))?;
let mut formation_secret = [0u8; 32];
formation_secret.copy_from_slice(&data[offset + 8..offset + 40]);
let mut authority_public_key = [0u8; 32];
authority_public_key.copy_from_slice(&data[offset + 40..offset + 72]);
let policy = MembershipPolicy::from_byte(data[offset + 72])
.ok_or_else(|| SecurityError::SerializationError("invalid policy byte".to_string()))?;
Ok(Self {
mesh_id,
mesh_name,
formation_secret,
authority_public_key,
policy,
})
}
}
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::super::certificate::permissions;
use super::*;
#[test]
fn test_create_genesis() {
let genesis = MeshGenesis::create("ALPHA-TEAM", MembershipPolicy::Controlled);
assert_eq!(genesis.mesh_name, "ALPHA-TEAM");
assert_eq!(genesis.policy, MembershipPolicy::Controlled);
assert!(genesis.created_at_ms > 0);
}
#[test]
fn test_mesh_id_format() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
let mesh_id = genesis.mesh_id();
assert_eq!(mesh_id.len(), 8);
assert!(mesh_id
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_lowercase()));
}
#[test]
fn test_mesh_id_deterministic() {
let seed = [0x42u8; 32];
let genesis = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
assert_eq!(genesis.mesh_id(), genesis.mesh_id());
}
#[test]
fn test_different_names_different_ids() {
let seed = [0x42u8; 32];
let g1 = MeshGenesis::with_seed("ALPHA", seed, MembershipPolicy::Open);
let g2 = MeshGenesis::with_seed("BRAVO", seed, MembershipPolicy::Open);
assert_ne!(g1.mesh_id(), g2.mesh_id());
}
#[test]
fn test_different_seeds_different_ids() {
let g1 = MeshGenesis::with_seed("TEST", [0x42u8; 32], MembershipPolicy::Open);
let g2 = MeshGenesis::with_seed("TEST", [0x43u8; 32], MembershipPolicy::Open);
assert_ne!(g1.mesh_id(), g2.mesh_id());
}
#[test]
fn test_formation_secret_deterministic() {
let seed = [0x42u8; 32];
let genesis = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
let s1 = genesis.formation_secret();
let s2 = genesis.formation_secret();
assert_eq!(s1, s2);
assert_ne!(s1, seed); }
#[test]
fn test_formation_secret_differs_from_mesh_id_source() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
let formation = genesis.formation_secret();
let mesh_id_bytes = genesis.derive(MeshGenesis::MESH_ID_CONTEXT);
assert_ne!(formation, mesh_id_bytes); }
#[test]
fn test_authority_keypair_deterministic() {
let seed = [0x42u8; 32];
let g1 = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
let g2 = MeshGenesis::with_seed("TEST", seed, MembershipPolicy::Open);
assert_eq!(g1.authority_public_key(), g2.authority_public_key());
}
#[test]
fn test_authority_can_sign_and_verify() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
let msg = b"hello mesh";
let sig = genesis.authority().sign(msg);
assert!(genesis.authority().verify(msg, &sig).is_ok());
}
#[test]
fn test_root_certificate() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Controlled);
let root = genesis.root_certificate("enterprise-0");
assert!(root.verify().is_ok());
assert!(root.is_root());
assert_eq!(root.mesh_id, genesis.mesh_id());
assert_eq!(root.node_id, "enterprise-0");
assert_eq!(root.tier, MeshTier::Enterprise);
assert_eq!(root.permissions, permissions::AUTHORITY);
assert_eq!(root.expires_at_ms, 0); assert_eq!(root.subject_public_key, genesis.authority_public_key());
assert_eq!(root.issuer_public_key, genesis.authority_public_key());
}
#[test]
fn test_issue_certificate() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Controlled);
let member = DeviceKeypair::generate();
let cert = genesis.issue_certificate(
member.public_key_bytes(),
"tac-west-1",
MeshTier::Tactical,
permissions::STANDARD,
24 * 60 * 60 * 1000, );
assert!(cert.verify().is_ok());
assert!(!cert.is_root());
assert_eq!(cert.mesh_id, genesis.mesh_id());
assert_eq!(cert.node_id, "tac-west-1");
assert_eq!(cert.tier, MeshTier::Tactical);
assert_eq!(cert.permissions, permissions::STANDARD);
assert_eq!(cert.subject_public_key, member.public_key_bytes());
assert_eq!(cert.issuer_public_key, genesis.authority_public_key());
assert!(cert.expires_at_ms > cert.issued_at_ms);
}
#[test]
fn test_issue_certificate_no_expiration() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
let member = DeviceKeypair::generate();
let cert = genesis.issue_certificate(
member.public_key_bytes(),
"hub-1",
MeshTier::Regional,
permissions::STANDARD | permissions::ENROLL,
0, );
assert!(cert.verify().is_ok());
assert_eq!(cert.expires_at_ms, 0);
}
#[test]
fn test_credentials() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Controlled);
let creds = genesis.credentials();
assert_eq!(creds.mesh_id, genesis.mesh_id());
assert_eq!(creds.mesh_name, genesis.mesh_name);
assert_eq!(creds.formation_secret, genesis.formation_secret());
assert_eq!(creds.authority_public_key, genesis.authority_public_key());
assert_eq!(creds.policy, genesis.policy);
}
#[test]
fn test_encode_decode_genesis_roundtrip() {
let genesis = MeshGenesis::create("ALPHA-TEAM", MembershipPolicy::Strict);
let encoded = genesis.encode();
let decoded = MeshGenesis::decode(&encoded).unwrap();
assert_eq!(decoded.mesh_name, genesis.mesh_name);
assert_eq!(decoded.mesh_id(), genesis.mesh_id());
assert_eq!(decoded.formation_secret(), genesis.formation_secret());
assert_eq!(
decoded.authority_public_key(),
genesis.authority_public_key()
);
assert_eq!(decoded.policy, genesis.policy);
}
#[test]
fn test_decode_genesis_too_short() {
assert!(MeshGenesis::decode(&[0u8; 10]).is_err());
}
#[test]
fn test_decode_genesis_invalid_policy() {
let genesis = MeshGenesis::create("X", MembershipPolicy::Open);
let mut encoded = genesis.encode();
*encoded.last_mut().unwrap() = 99;
assert!(MeshGenesis::decode(&encoded).is_err());
}
#[test]
fn test_encode_decode_credentials_roundtrip() {
let genesis = MeshGenesis::create("BRAVO-NET", MembershipPolicy::Controlled);
let creds = genesis.credentials();
let encoded = creds.encode();
let decoded = MeshCredentials::decode(&encoded).unwrap();
assert_eq!(decoded.mesh_id, creds.mesh_id);
assert_eq!(decoded.mesh_name, creds.mesh_name);
assert_eq!(decoded.formation_secret, creds.formation_secret);
assert_eq!(decoded.authority_public_key, creds.authority_public_key);
assert_eq!(decoded.policy, creds.policy);
}
#[test]
fn test_decode_credentials_too_short() {
assert!(MeshCredentials::decode(&[0u8; 10]).is_err());
}
#[test]
fn test_with_authority_external_keypair() {
let external_authority = DeviceKeypair::generate();
let seed = [0x42u8; 32];
let genesis = MeshGenesis::with_authority(
"HSM-MESH",
seed,
external_authority.clone(),
MembershipPolicy::Strict,
);
assert_eq!(
genesis.authority_public_key(),
external_authority.public_key_bytes()
);
let derived = MeshGenesis::with_seed("HSM-MESH", seed, MembershipPolicy::Strict);
assert_ne!(
genesis.authority_public_key(),
derived.authority_public_key()
);
}
#[test]
fn test_policy_default() {
assert_eq!(MembershipPolicy::default(), MembershipPolicy::Controlled);
}
#[test]
fn test_policy_from_str_name() {
assert_eq!(
MembershipPolicy::from_str_name("open"),
Some(MembershipPolicy::Open)
);
assert_eq!(
MembershipPolicy::from_str_name("CONTROLLED"),
Some(MembershipPolicy::Controlled)
);
assert_eq!(
MembershipPolicy::from_str_name(" Strict "),
Some(MembershipPolicy::Strict)
);
assert_eq!(MembershipPolicy::from_str_name("invalid"), None);
}
#[test]
fn test_policy_byte_roundtrip() {
for policy in [
MembershipPolicy::Open,
MembershipPolicy::Controlled,
MembershipPolicy::Strict,
] {
assert_eq!(MembershipPolicy::from_byte(policy.to_byte()), Some(policy));
}
assert_eq!(MembershipPolicy::from_byte(99), None);
}
#[test]
fn test_debug_redacts_seed() {
let genesis = MeshGenesis::create("TEST", MembershipPolicy::Open);
let debug_str = format!("{:?}", genesis);
assert!(debug_str.contains("REDACTED"));
assert!(debug_str.contains("mesh_id"));
let seed_hex = hex::encode(genesis.mesh_seed());
assert!(!debug_str.contains(&seed_hex));
}
}