#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use super::genesis::MeshGenesis;
use super::identity::{verify_signature, DeviceIdentity};
pub const MAX_CALLSIGN_LEN: usize = 12;
pub const MESH_ID_SIZE: usize = 4;
pub const TOKEN_WIRE_SIZE: usize = 32 + 4 + 12 + 8 + 8 + 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MembershipToken {
pub public_key: [u8; 32],
pub mesh_id: [u8; MESH_ID_SIZE],
pub callsign: [u8; MAX_CALLSIGN_LEN],
pub issued_at_ms: u64,
pub expires_at_ms: u64,
pub authority_signature: [u8; 64],
}
impl MembershipToken {
pub fn issue(
authority: &DeviceIdentity,
genesis: &MeshGenesis,
member_public_key: [u8; 32],
callsign: &str,
validity_ms: u64,
) -> Self {
assert!(
callsign.len() <= MAX_CALLSIGN_LEN,
"callsign must be <= {} chars",
MAX_CALLSIGN_LEN
);
let mesh_id = Self::mesh_id_bytes(&genesis.mesh_id());
let mut callsign_bytes = [0u8; MAX_CALLSIGN_LEN];
callsign_bytes[..callsign.len()].copy_from_slice(callsign.as_bytes());
let now_ms = Self::now_ms();
let expires_at_ms = if validity_ms == 0 {
0
} else {
now_ms.saturating_add(validity_ms)
};
let mut token = Self {
public_key: member_public_key,
mesh_id,
callsign: callsign_bytes,
issued_at_ms: now_ms,
expires_at_ms,
authority_signature: [0u8; 64],
};
let signable = token.signable_bytes();
token.authority_signature = authority.sign(&signable);
token
}
pub fn issue_at(
authority: &DeviceIdentity,
mesh_id: [u8; MESH_ID_SIZE],
member_public_key: [u8; 32],
callsign: &str,
issued_at_ms: u64,
expires_at_ms: u64,
) -> Self {
assert!(
callsign.len() <= MAX_CALLSIGN_LEN,
"callsign must be <= {} chars",
MAX_CALLSIGN_LEN
);
let mut callsign_bytes = [0u8; MAX_CALLSIGN_LEN];
callsign_bytes[..callsign.len()].copy_from_slice(callsign.as_bytes());
let mut token = Self {
public_key: member_public_key,
mesh_id,
callsign: callsign_bytes,
issued_at_ms,
expires_at_ms,
authority_signature: [0u8; 64],
};
let signable = token.signable_bytes();
token.authority_signature = authority.sign(&signable);
token
}
pub fn verify(&self, authority_public_key: &[u8; 32]) -> bool {
let signable = self.signable_bytes();
verify_signature(authority_public_key, &signable, &self.authority_signature)
}
pub fn is_expired(&self, now_ms: u64) -> bool {
self.expires_at_ms != 0 && now_ms > self.expires_at_ms
}
pub fn is_valid(&self, authority_public_key: &[u8; 32], now_ms: u64) -> bool {
self.verify(authority_public_key) && !self.is_expired(now_ms)
}
pub fn callsign_str(&self) -> &str {
let len = self
.callsign
.iter()
.position(|&b| b == 0)
.unwrap_or(MAX_CALLSIGN_LEN);
core::str::from_utf8(&self.callsign[..len]).unwrap_or("")
}
pub fn mesh_id_hex(&self) -> String {
format!(
"{:02X}{:02X}{:02X}{:02X}",
self.mesh_id[0], self.mesh_id[1], self.mesh_id[2], self.mesh_id[3]
)
}
pub fn encode(&self) -> [u8; TOKEN_WIRE_SIZE] {
let mut buf = [0u8; TOKEN_WIRE_SIZE];
let mut offset = 0;
buf[offset..offset + 32].copy_from_slice(&self.public_key);
offset += 32;
buf[offset..offset + MESH_ID_SIZE].copy_from_slice(&self.mesh_id);
offset += MESH_ID_SIZE;
buf[offset..offset + MAX_CALLSIGN_LEN].copy_from_slice(&self.callsign);
offset += MAX_CALLSIGN_LEN;
buf[offset..offset + 8].copy_from_slice(&self.issued_at_ms.to_le_bytes());
offset += 8;
buf[offset..offset + 8].copy_from_slice(&self.expires_at_ms.to_le_bytes());
offset += 8;
buf[offset..offset + 64].copy_from_slice(&self.authority_signature);
buf
}
pub fn decode(data: &[u8]) -> Option<Self> {
if data.len() != TOKEN_WIRE_SIZE {
return None;
}
let mut offset = 0;
let mut public_key = [0u8; 32];
public_key.copy_from_slice(&data[offset..offset + 32]);
offset += 32;
let mut mesh_id = [0u8; MESH_ID_SIZE];
mesh_id.copy_from_slice(&data[offset..offset + MESH_ID_SIZE]);
offset += MESH_ID_SIZE;
let mut callsign = [0u8; MAX_CALLSIGN_LEN];
callsign.copy_from_slice(&data[offset..offset + MAX_CALLSIGN_LEN]);
offset += MAX_CALLSIGN_LEN;
let issued_at_ms = u64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]);
offset += 8;
let expires_at_ms = u64::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]);
offset += 8;
let mut authority_signature = [0u8; 64];
authority_signature.copy_from_slice(&data[offset..offset + 64]);
Some(Self {
public_key,
mesh_id,
callsign,
issued_at_ms,
expires_at_ms,
authority_signature,
})
}
fn signable_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(TOKEN_WIRE_SIZE - 64);
buf.extend_from_slice(&self.public_key);
buf.extend_from_slice(&self.mesh_id);
buf.extend_from_slice(&self.callsign);
buf.extend_from_slice(&self.issued_at_ms.to_le_bytes());
buf.extend_from_slice(&self.expires_at_ms.to_le_bytes());
buf
}
fn mesh_id_bytes(hex: &str) -> [u8; MESH_ID_SIZE] {
let mut bytes = [0u8; MESH_ID_SIZE];
if hex.len() == 8 {
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
if i < MESH_ID_SIZE {
let s = core::str::from_utf8(chunk).unwrap_or("00");
bytes[i] = u8::from_str_radix(s, 16).unwrap_or(0);
}
}
}
bytes
}
#[cfg(feature = "std")]
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(not(feature = "std"))]
fn now_ms() -> u64 {
0 }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::MembershipPolicy;
#[test]
fn test_issue_and_verify() {
let authority = DeviceIdentity::generate();
let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
let member = DeviceIdentity::generate();
let token = MembershipToken::issue(
&authority,
&genesis,
member.public_key(),
"BRAVO-07",
3600_000, );
assert!(token.verify(&authority.public_key()));
assert_eq!(token.callsign_str(), "BRAVO-07");
assert_eq!(token.public_key, member.public_key());
}
#[test]
fn test_wrong_authority_fails() {
let authority = DeviceIdentity::generate();
let other = DeviceIdentity::generate();
let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
let member = DeviceIdentity::generate();
let token = MembershipToken::issue(
&authority,
&genesis,
member.public_key(),
"BRAVO-07",
3600_000,
);
assert!(!token.verify(&other.public_key()));
}
#[test]
fn test_tampered_token_fails() {
let authority = DeviceIdentity::generate();
let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
let member = DeviceIdentity::generate();
let mut token = MembershipToken::issue(
&authority,
&genesis,
member.public_key(),
"BRAVO-07",
3600_000,
);
token.callsign[0] = b'X';
assert!(!token.verify(&authority.public_key()));
}
#[test]
fn test_expiration() {
let authority = DeviceIdentity::generate();
let mesh_id = [0x12, 0x34, 0x56, 0x78];
let member = DeviceIdentity::generate();
let token = MembershipToken::issue_at(
&authority,
mesh_id,
member.public_key(),
"ALPHA-01",
1000, 2000, );
assert!(!token.is_expired(1500));
assert!(token.is_valid(&authority.public_key(), 1500));
assert!(token.is_expired(2500));
assert!(!token.is_valid(&authority.public_key(), 2500));
}
#[test]
fn test_no_expiration() {
let authority = DeviceIdentity::generate();
let mesh_id = [0x12, 0x34, 0x56, 0x78];
let member = DeviceIdentity::generate();
let token = MembershipToken::issue_at(
&authority,
mesh_id,
member.public_key(),
"ALPHA-01",
1000,
0, );
assert!(!token.is_expired(u64::MAX));
}
#[test]
fn test_encode_decode_roundtrip() {
let authority = DeviceIdentity::generate();
let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
let member = DeviceIdentity::generate();
let token = MembershipToken::issue(
&authority,
&genesis,
member.public_key(),
"CHARLIE-12",
86400_000, );
let encoded = token.encode();
assert_eq!(encoded.len(), TOKEN_WIRE_SIZE);
let decoded = MembershipToken::decode(&encoded).unwrap();
assert_eq!(decoded, token);
assert!(decoded.verify(&authority.public_key()));
}
#[test]
fn test_callsign_str_trimmed() {
let authority = DeviceIdentity::generate();
let mesh_id = [0x12, 0x34, 0x56, 0x78];
let member = DeviceIdentity::generate();
let token =
MembershipToken::issue_at(&authority, mesh_id, member.public_key(), "A-1", 0, 0);
assert_eq!(token.callsign_str(), "A-1");
let token = MembershipToken::issue_at(
&authority,
mesh_id,
member.public_key(),
"ALPHA-BRAVO1",
0,
0,
);
assert_eq!(token.callsign_str(), "ALPHA-BRAVO1");
}
#[test]
fn test_mesh_id_hex() {
let authority = DeviceIdentity::generate();
let mesh_id = [0xAB, 0xCD, 0xEF, 0x12];
let member = DeviceIdentity::generate();
let token =
MembershipToken::issue_at(&authority, mesh_id, member.public_key(), "TEST", 0, 0);
assert_eq!(token.mesh_id_hex(), "ABCDEF12");
}
#[test]
fn test_wire_size() {
assert_eq!(TOKEN_WIRE_SIZE, 128);
assert_eq!(
TOKEN_WIRE_SIZE,
32 + MESH_ID_SIZE + MAX_CALLSIGN_LEN + 8 + 8 + 64
);
}
#[test]
#[should_panic(expected = "callsign must be <= 12 chars")]
fn test_callsign_too_long_panics() {
let authority = DeviceIdentity::generate();
let mesh_id = [0x12, 0x34, 0x56, 0x78];
let member = DeviceIdentity::generate();
MembershipToken::issue_at(
&authority,
mesh_id,
member.public_key(),
"THIS-IS-TOO-LONG",
0,
0,
);
}
}