Skip to main content

subc_protocol/
machine_id.rs

1//! The machine id: one opaque name per machine, owned by the daemon.
2
3use std::{error::Error, fmt, str::FromStr};
4
5use serde::{Deserialize, Serialize};
6
7/// This machine's name, minted once by the daemon and served to every module
8/// that registers with it (on `HELLO_ACK` and `server.describe`).
9///
10/// It is 16 random bytes rendered as exactly 32 lowercase hex characters. It is
11/// opaque: it carries no structure and is not derived from a key, a hostname or
12/// any other host fact. The daemon stores it at `<data home>/cortexkit/machine-id`
13/// and never rewrites that file; an operator changes it only with
14/// `ck machine adopt`, which takes effect at the next daemon start.
15///
16/// # A name, never an authority
17///
18/// Nothing may admit a peer, grant trust or skip a check because two messages
19/// carry the same machine id. Authority stays on keys (a peer's roster entry, the
20/// vault). The id deliberately outlives a key rotation, so treating it as an
21/// identity would let a revoked key's history vouch for its replacement. Two
22/// machines restored from one backup also carry the same id, which is exactly
23/// why it can name a machine but never prove one.
24///
25/// Construction validates the shape, so a value of this type is always 32
26/// lowercase hex characters. A module talking to a daemon that predates the id
27/// sees no value at all and must read that as "the daemon predates the machine
28/// id", never as "there is no machine" and never as a cue to mint its own.
29#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
30#[serde(try_from = "String", into = "String")]
31pub struct MachineId(String);
32
33impl MachineId {
34    /// Length of the rendered id in characters (16 bytes, two hex digits each).
35    pub const HEX_LEN: usize = 32;
36
37    /// Validate `value` as a machine id: exactly 32 characters, each `0-9` or
38    /// `a-f`. Uppercase hex is refused rather than folded, so one machine has
39    /// exactly one spelling and string comparison is identity of the name.
40    pub fn parse(value: &str) -> Result<Self, MachineIdError> {
41        if value.len() != Self::HEX_LEN {
42            return Err(MachineIdError::WrongLength { len: value.len() });
43        }
44        if let Some(position) = value
45            .bytes()
46            .position(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
47        {
48            return Err(MachineIdError::NotLowercaseHex { position });
49        }
50        Ok(Self(value.to_owned()))
51    }
52
53    /// Render 16 bytes as a machine id. The caller supplies the randomness; the
54    /// daemon uses the operating system's CSPRNG.
55    pub fn from_bytes(bytes: [u8; 16]) -> Self {
56        Self(format!("{:032x}", u128::from_be_bytes(bytes)))
57    }
58
59    /// The 32-character lowercase hex rendering.
60    pub fn as_str(&self) -> &str {
61        &self.0
62    }
63}
64
65impl fmt::Display for MachineId {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.write_str(&self.0)
68    }
69}
70
71impl FromStr for MachineId {
72    type Err = MachineIdError;
73
74    fn from_str(value: &str) -> Result<Self, Self::Err> {
75        Self::parse(value)
76    }
77}
78
79impl TryFrom<String> for MachineId {
80    type Error = MachineIdError;
81
82    fn try_from(value: String) -> Result<Self, Self::Error> {
83        Self::parse(&value)
84    }
85}
86
87impl From<MachineId> for String {
88    fn from(value: MachineId) -> Self {
89        value.0
90    }
91}
92
93/// Why a string is not a machine id.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum MachineIdError {
96    /// The value is not exactly [`MachineId::HEX_LEN`] bytes long.
97    WrongLength { len: usize },
98    /// The byte at `position` is not a lowercase hex digit.
99    NotLowercaseHex { position: usize },
100}
101
102impl fmt::Display for MachineIdError {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            Self::WrongLength { len } => write!(
106                f,
107                "a machine id is exactly {} lowercase hex characters, got {len} bytes",
108                MachineId::HEX_LEN
109            ),
110            Self::NotLowercaseHex { position } => write!(
111                f,
112                "a machine id is exactly {} lowercase hex characters; byte {position} is not 0-9 or a-f",
113                MachineId::HEX_LEN
114            ),
115        }
116    }
117}
118
119impl Error for MachineIdError {}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn accepts_exactly_32_lowercase_hex() {
127        let id = MachineId::parse("0123456789abcdef0123456789abcdef").expect("valid");
128        assert_eq!(id.as_str(), "0123456789abcdef0123456789abcdef");
129    }
130
131    #[test]
132    fn refuses_wrong_length_uppercase_and_non_hex() {
133        assert_eq!(
134            MachineId::parse("abc"),
135            Err(MachineIdError::WrongLength { len: 3 })
136        );
137        assert_eq!(
138            MachineId::parse("0123456789abcdef0123456789abcdef\n"),
139            Err(MachineIdError::WrongLength { len: 33 })
140        );
141        assert_eq!(
142            MachineId::parse("0123456789ABCDEF0123456789abcdef"),
143            Err(MachineIdError::NotLowercaseHex { position: 10 })
144        );
145        assert_eq!(
146            MachineId::parse("0123456789abcdeg0123456789abcdef"),
147            Err(MachineIdError::NotLowercaseHex { position: 15 })
148        );
149    }
150
151    #[test]
152    fn from_bytes_renders_32_lowercase_hex_with_leading_zeros() {
153        let mut bytes = [0u8; 16];
154        bytes[15] = 0xab;
155        let id = MachineId::from_bytes(bytes);
156        assert_eq!(id.as_str(), "000000000000000000000000000000ab");
157        assert_eq!(MachineId::parse(id.as_str()), Ok(id));
158    }
159
160    #[test]
161    fn serde_refuses_a_malformed_value() {
162        let ok: MachineId =
163            serde_json::from_str("\"0123456789abcdef0123456789abcdef\"").expect("valid");
164        assert_eq!(
165            serde_json::to_string(&ok).unwrap(),
166            "\"0123456789abcdef0123456789abcdef\""
167        );
168        assert!(serde_json::from_str::<MachineId>("\"not-an-id\"").is_err());
169    }
170}