1#[cfg(not(feature = "std"))]
65use alloc::string::String;
66#[cfg(not(feature = "std"))]
67use alloc::vec::Vec;
68
69use super::genesis::MeshGenesis;
70use super::identity::{verify_signature, DeviceIdentity};
71
72pub const MAX_CALLSIGN_LEN: usize = 12;
74
75pub const MESH_ID_SIZE: usize = 4;
77
78pub const TOKEN_WIRE_SIZE: usize = 32 + 4 + 12 + 8 + 8 + 64; #[derive(Debug, Clone, PartialEq, Eq)]
85pub struct MembershipToken {
86 pub public_key: [u8; 32],
88
89 pub mesh_id: [u8; MESH_ID_SIZE],
91
92 pub callsign: [u8; MAX_CALLSIGN_LEN],
94
95 pub issued_at_ms: u64,
97
98 pub expires_at_ms: u64,
101
102 pub authority_signature: [u8; 64],
104}
105
106impl MembershipToken {
107 pub fn issue(
119 authority: &DeviceIdentity,
120 genesis: &MeshGenesis,
121 member_public_key: [u8; 32],
122 callsign: &str,
123 validity_ms: u64,
124 ) -> Self {
125 assert!(
126 callsign.len() <= MAX_CALLSIGN_LEN,
127 "callsign must be <= {} chars",
128 MAX_CALLSIGN_LEN
129 );
130
131 let mesh_id = Self::mesh_id_bytes(&genesis.mesh_id());
132 let mut callsign_bytes = [0u8; MAX_CALLSIGN_LEN];
133 callsign_bytes[..callsign.len()].copy_from_slice(callsign.as_bytes());
134
135 let now_ms = Self::now_ms();
136 let expires_at_ms = if validity_ms == 0 {
137 0
138 } else {
139 now_ms.saturating_add(validity_ms)
140 };
141
142 let mut token = Self {
143 public_key: member_public_key,
144 mesh_id,
145 callsign: callsign_bytes,
146 issued_at_ms: now_ms,
147 expires_at_ms,
148 authority_signature: [0u8; 64],
149 };
150
151 let signable = token.signable_bytes();
153 token.authority_signature = authority.sign(&signable);
154
155 token
156 }
157
158 pub fn issue_at(
160 authority: &DeviceIdentity,
161 mesh_id: [u8; MESH_ID_SIZE],
162 member_public_key: [u8; 32],
163 callsign: &str,
164 issued_at_ms: u64,
165 expires_at_ms: u64,
166 ) -> Self {
167 assert!(
168 callsign.len() <= MAX_CALLSIGN_LEN,
169 "callsign must be <= {} chars",
170 MAX_CALLSIGN_LEN
171 );
172
173 let mut callsign_bytes = [0u8; MAX_CALLSIGN_LEN];
174 callsign_bytes[..callsign.len()].copy_from_slice(callsign.as_bytes());
175
176 let mut token = Self {
177 public_key: member_public_key,
178 mesh_id,
179 callsign: callsign_bytes,
180 issued_at_ms,
181 expires_at_ms,
182 authority_signature: [0u8; 64],
183 };
184
185 let signable = token.signable_bytes();
186 token.authority_signature = authority.sign(&signable);
187
188 token
189 }
190
191 pub fn verify(&self, authority_public_key: &[u8; 32]) -> bool {
199 let signable = self.signable_bytes();
200 verify_signature(authority_public_key, &signable, &self.authority_signature)
201 }
202
203 pub fn is_expired(&self, now_ms: u64) -> bool {
211 self.expires_at_ms != 0 && now_ms > self.expires_at_ms
212 }
213
214 pub fn is_valid(&self, authority_public_key: &[u8; 32], now_ms: u64) -> bool {
220 self.verify(authority_public_key) && !self.is_expired(now_ms)
221 }
222
223 pub fn callsign_str(&self) -> &str {
225 let len = self
226 .callsign
227 .iter()
228 .position(|&b| b == 0)
229 .unwrap_or(MAX_CALLSIGN_LEN);
230 core::str::from_utf8(&self.callsign[..len]).unwrap_or("")
232 }
233
234 pub fn mesh_id_hex(&self) -> String {
236 format!(
237 "{:02X}{:02X}{:02X}{:02X}",
238 self.mesh_id[0], self.mesh_id[1], self.mesh_id[2], self.mesh_id[3]
239 )
240 }
241
242 pub fn encode(&self) -> [u8; TOKEN_WIRE_SIZE] {
244 let mut buf = [0u8; TOKEN_WIRE_SIZE];
245 let mut offset = 0;
246
247 buf[offset..offset + 32].copy_from_slice(&self.public_key);
248 offset += 32;
249
250 buf[offset..offset + MESH_ID_SIZE].copy_from_slice(&self.mesh_id);
251 offset += MESH_ID_SIZE;
252
253 buf[offset..offset + MAX_CALLSIGN_LEN].copy_from_slice(&self.callsign);
254 offset += MAX_CALLSIGN_LEN;
255
256 buf[offset..offset + 8].copy_from_slice(&self.issued_at_ms.to_le_bytes());
257 offset += 8;
258
259 buf[offset..offset + 8].copy_from_slice(&self.expires_at_ms.to_le_bytes());
260 offset += 8;
261
262 buf[offset..offset + 64].copy_from_slice(&self.authority_signature);
263
264 buf
265 }
266
267 pub fn decode(data: &[u8]) -> Option<Self> {
271 if data.len() != TOKEN_WIRE_SIZE {
272 return None;
273 }
274
275 let mut offset = 0;
276
277 let mut public_key = [0u8; 32];
278 public_key.copy_from_slice(&data[offset..offset + 32]);
279 offset += 32;
280
281 let mut mesh_id = [0u8; MESH_ID_SIZE];
282 mesh_id.copy_from_slice(&data[offset..offset + MESH_ID_SIZE]);
283 offset += MESH_ID_SIZE;
284
285 let mut callsign = [0u8; MAX_CALLSIGN_LEN];
286 callsign.copy_from_slice(&data[offset..offset + MAX_CALLSIGN_LEN]);
287 offset += MAX_CALLSIGN_LEN;
288
289 let issued_at_ms = u64::from_le_bytes([
290 data[offset],
291 data[offset + 1],
292 data[offset + 2],
293 data[offset + 3],
294 data[offset + 4],
295 data[offset + 5],
296 data[offset + 6],
297 data[offset + 7],
298 ]);
299 offset += 8;
300
301 let expires_at_ms = u64::from_le_bytes([
302 data[offset],
303 data[offset + 1],
304 data[offset + 2],
305 data[offset + 3],
306 data[offset + 4],
307 data[offset + 5],
308 data[offset + 6],
309 data[offset + 7],
310 ]);
311 offset += 8;
312
313 let mut authority_signature = [0u8; 64];
314 authority_signature.copy_from_slice(&data[offset..offset + 64]);
315
316 Some(Self {
317 public_key,
318 mesh_id,
319 callsign,
320 issued_at_ms,
321 expires_at_ms,
322 authority_signature,
323 })
324 }
325
326 fn signable_bytes(&self) -> Vec<u8> {
328 let mut buf = Vec::with_capacity(TOKEN_WIRE_SIZE - 64);
329 buf.extend_from_slice(&self.public_key);
330 buf.extend_from_slice(&self.mesh_id);
331 buf.extend_from_slice(&self.callsign);
332 buf.extend_from_slice(&self.issued_at_ms.to_le_bytes());
333 buf.extend_from_slice(&self.expires_at_ms.to_le_bytes());
334 buf
335 }
336
337 fn mesh_id_bytes(hex: &str) -> [u8; MESH_ID_SIZE] {
339 let mut bytes = [0u8; MESH_ID_SIZE];
340 if hex.len() == 8 {
342 for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
343 if i < MESH_ID_SIZE {
344 let s = core::str::from_utf8(chunk).unwrap_or("00");
345 bytes[i] = u8::from_str_radix(s, 16).unwrap_or(0);
346 }
347 }
348 }
349 bytes
350 }
351
352 #[cfg(feature = "std")]
353 fn now_ms() -> u64 {
354 std::time::SystemTime::now()
355 .duration_since(std::time::UNIX_EPOCH)
356 .map(|d| d.as_millis() as u64)
357 .unwrap_or(0)
358 }
359
360 #[cfg(not(feature = "std"))]
361 fn now_ms() -> u64 {
362 0 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::security::MembershipPolicy;
370
371 #[test]
372 fn test_issue_and_verify() {
373 let authority = DeviceIdentity::generate();
374 let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
375 let member = DeviceIdentity::generate();
376
377 let token = MembershipToken::issue(
378 &authority,
379 &genesis,
380 member.public_key(),
381 "BRAVO-07",
382 3600_000, );
384
385 assert!(token.verify(&authority.public_key()));
386 assert_eq!(token.callsign_str(), "BRAVO-07");
387 assert_eq!(token.public_key, member.public_key());
388 }
389
390 #[test]
391 fn test_wrong_authority_fails() {
392 let authority = DeviceIdentity::generate();
393 let other = DeviceIdentity::generate();
394 let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
395 let member = DeviceIdentity::generate();
396
397 let token = MembershipToken::issue(
398 &authority,
399 &genesis,
400 member.public_key(),
401 "BRAVO-07",
402 3600_000,
403 );
404
405 assert!(!token.verify(&other.public_key()));
407 }
408
409 #[test]
410 fn test_tampered_token_fails() {
411 let authority = DeviceIdentity::generate();
412 let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
413 let member = DeviceIdentity::generate();
414
415 let mut token = MembershipToken::issue(
416 &authority,
417 &genesis,
418 member.public_key(),
419 "BRAVO-07",
420 3600_000,
421 );
422
423 token.callsign[0] = b'X';
425
426 assert!(!token.verify(&authority.public_key()));
427 }
428
429 #[test]
430 fn test_expiration() {
431 let authority = DeviceIdentity::generate();
432 let mesh_id = [0x12, 0x34, 0x56, 0x78];
433 let member = DeviceIdentity::generate();
434
435 let token = MembershipToken::issue_at(
436 &authority,
437 mesh_id,
438 member.public_key(),
439 "ALPHA-01",
440 1000, 2000, );
443
444 assert!(!token.is_expired(1500));
446 assert!(token.is_valid(&authority.public_key(), 1500));
447
448 assert!(token.is_expired(2500));
450 assert!(!token.is_valid(&authority.public_key(), 2500));
451 }
452
453 #[test]
454 fn test_no_expiration() {
455 let authority = DeviceIdentity::generate();
456 let mesh_id = [0x12, 0x34, 0x56, 0x78];
457 let member = DeviceIdentity::generate();
458
459 let token = MembershipToken::issue_at(
460 &authority,
461 mesh_id,
462 member.public_key(),
463 "ALPHA-01",
464 1000,
465 0, );
467
468 assert!(!token.is_expired(u64::MAX));
470 }
471
472 #[test]
473 fn test_encode_decode_roundtrip() {
474 let authority = DeviceIdentity::generate();
475 let genesis = MeshGenesis::create("ALPHA", &authority, MembershipPolicy::Controlled);
476 let member = DeviceIdentity::generate();
477
478 let token = MembershipToken::issue(
479 &authority,
480 &genesis,
481 member.public_key(),
482 "CHARLIE-12",
483 86400_000, );
485
486 let encoded = token.encode();
487 assert_eq!(encoded.len(), TOKEN_WIRE_SIZE);
488
489 let decoded = MembershipToken::decode(&encoded).unwrap();
490 assert_eq!(decoded, token);
491 assert!(decoded.verify(&authority.public_key()));
492 }
493
494 #[test]
495 fn test_callsign_str_trimmed() {
496 let authority = DeviceIdentity::generate();
497 let mesh_id = [0x12, 0x34, 0x56, 0x78];
498 let member = DeviceIdentity::generate();
499
500 let token =
502 MembershipToken::issue_at(&authority, mesh_id, member.public_key(), "A-1", 0, 0);
503 assert_eq!(token.callsign_str(), "A-1");
504
505 let token = MembershipToken::issue_at(
507 &authority,
508 mesh_id,
509 member.public_key(),
510 "ALPHA-BRAVO1",
511 0,
512 0,
513 );
514 assert_eq!(token.callsign_str(), "ALPHA-BRAVO1");
515 }
516
517 #[test]
518 fn test_mesh_id_hex() {
519 let authority = DeviceIdentity::generate();
520 let mesh_id = [0xAB, 0xCD, 0xEF, 0x12];
521 let member = DeviceIdentity::generate();
522
523 let token =
524 MembershipToken::issue_at(&authority, mesh_id, member.public_key(), "TEST", 0, 0);
525
526 assert_eq!(token.mesh_id_hex(), "ABCDEF12");
527 }
528
529 #[test]
530 fn test_wire_size() {
531 assert_eq!(TOKEN_WIRE_SIZE, 128);
533 assert_eq!(
534 TOKEN_WIRE_SIZE,
535 32 + MESH_ID_SIZE + MAX_CALLSIGN_LEN + 8 + 8 + 64
536 );
537 }
538
539 #[test]
540 #[should_panic(expected = "callsign must be <= 12 chars")]
541 fn test_callsign_too_long_panics() {
542 let authority = DeviceIdentity::generate();
543 let mesh_id = [0x12, 0x34, 0x56, 0x78];
544 let member = DeviceIdentity::generate();
545
546 MembershipToken::issue_at(
547 &authority,
548 mesh_id,
549 member.public_key(),
550 "THIS-IS-TOO-LONG",
551 0,
552 0,
553 );
554 }
555}