Skip to main content

laser_wire/
keys.rs

1use serde::{Deserialize, Serialize};
2
3pub const KEY_RECORD_VERSION: u32 = 1;
4pub const KEY_ID_BYTES: usize = 8;
5pub const VERIFYING_KEY_BYTES: usize = 32;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum KeyKind {
10    Agent,
11    Operator,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15pub struct KeyRecord {
16    pub v: u32,
17    pub principal: String,
18    #[serde(with = "crate::encoding::bin_bytes")]
19    pub key_id: Vec<u8>,
20    #[serde(with = "crate::encoding::bin_bytes")]
21    pub verifying_key: Vec<u8>,
22    pub kind: KeyKind,
23    pub valid_from_micros: u64,
24    pub valid_to_micros: Option<u64>,
25    pub revoked: bool,
26}
27
28impl KeyRecord {
29    #[must_use]
30    pub fn new(
31        principal: impl Into<String>,
32        key_id: Vec<u8>,
33        verifying_key: Vec<u8>,
34        kind: KeyKind,
35    ) -> Self {
36        Self {
37            v: KEY_RECORD_VERSION,
38            principal: principal.into(),
39            key_id,
40            verifying_key,
41            kind,
42            valid_from_micros: 0,
43            valid_to_micros: None,
44            revoked: false,
45        }
46    }
47
48    pub fn validate(&self) -> Result<(), &'static str> {
49        if self.v != KEY_RECORD_VERSION {
50            return Err("unsupported key record version");
51        }
52        if self.principal.is_empty() {
53            return Err("key principal must not be empty");
54        }
55        if self.key_id.len() != KEY_ID_BYTES {
56            return Err("Ed25519 key id must be 8 bytes");
57        }
58        if self.verifying_key.len() != VERIFYING_KEY_BYTES {
59            return Err("Ed25519 public key must be 32 bytes");
60        }
61        if self
62            .valid_to_micros
63            .is_some_and(|end| end <= self.valid_from_micros)
64        {
65            return Err("key validity end must be after its start");
66        }
67        Ok(())
68    }
69}
70
71#[cfg(all(test, feature = "cbor"))]
72mod tests {
73    use super::*;
74    use crate::framing::{decode_named, encode_named};
75
76    #[test]
77    fn given_a_key_record_when_round_tripped_then_should_preserve_its_lifecycle() {
78        let record = KeyRecord {
79            v: KEY_RECORD_VERSION,
80            principal: "operator-1".to_owned(),
81            key_id: vec![3; KEY_ID_BYTES],
82            verifying_key: vec![7; VERIFYING_KEY_BYTES],
83            kind: KeyKind::Operator,
84            valid_from_micros: 100,
85            valid_to_micros: Some(200),
86            revoked: true,
87        };
88
89        let encoded = encode_named(&record).expect("key record encodes");
90        let decoded: KeyRecord = decode_named(&encoded).expect("key record decodes");
91
92        assert_eq!(decoded, record);
93        assert!(decoded.validate().is_ok());
94    }
95}