nodedb_cluster/auth/
join_token.rs1use std::time::{SystemTime, UNIX_EPOCH};
19
20use hmac::{Hmac, Mac};
21use sha2::Sha256;
22
23pub const TOKEN_HEADER_LEN: usize = 8 + 8;
25pub const TOKEN_MAC_LEN: usize = 32;
27pub const TOKEN_BYTE_LEN: usize = TOKEN_HEADER_LEN + TOKEN_MAC_LEN;
29pub const TOKEN_HEX_LEN: usize = TOKEN_BYTE_LEN * 2;
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34pub enum TokenError {
35 #[error("token wrong length")]
36 WrongLength,
37 #[error("token contains invalid hex")]
38 InvalidHex,
39 #[error("invalid token MAC")]
40 InvalidMac,
41 #[error("token expired")]
42 Expired,
43 #[error("hmac key length invalid")]
44 HmacKeyLength,
45}
46
47pub fn issue_token_bytes(
53 secret: &[u8; 32],
54 for_node: u64,
55 expiry_unix_secs: u64,
56) -> Result<[u8; TOKEN_BYTE_LEN], TokenError> {
57 let mut buf = [0u8; TOKEN_BYTE_LEN];
58 buf[..8].copy_from_slice(&for_node.to_le_bytes());
59 buf[8..16].copy_from_slice(&expiry_unix_secs.to_le_bytes());
60 let mut mac = <Hmac<Sha256>>::new_from_slice(secret).map_err(|_| TokenError::HmacKeyLength)?;
61 mac.update(&buf[..TOKEN_HEADER_LEN]);
62 let tag = mac.finalize().into_bytes();
63 buf[TOKEN_HEADER_LEN..].copy_from_slice(&tag);
64 Ok(buf)
65}
66
67pub fn issue_token(
69 secret: &[u8; 32],
70 for_node: u64,
71 expiry_unix_secs: u64,
72) -> Result<String, TokenError> {
73 let bytes = issue_token_bytes(secret, for_node, expiry_unix_secs)?;
74 Ok(token_to_hex(&bytes))
75}
76
77pub fn token_to_hex(bytes: &[u8]) -> String {
79 use std::fmt::Write as _;
80 let mut out = String::with_capacity(bytes.len() * 2);
81 for b in bytes {
82 let _ = write!(out, "{b:02x}");
83 }
84 out
85}
86
87pub fn token_hash(token_hex: &str) -> Result<[u8; 32], TokenError> {
90 use sha2::Digest;
91 let bytes = hex_decode(token_hex)?;
92 Ok(sha2::Sha256::digest(&bytes).into())
93}
94
95pub fn verify_token(token_hex: &str, secret: &[u8; 32]) -> Result<(u64, u64), TokenError> {
101 if token_hex.len() != TOKEN_HEX_LEN {
102 return Err(TokenError::WrongLength);
103 }
104 let bytes = hex_decode(token_hex)?;
105 let (body, tag) = bytes.split_at(TOKEN_HEADER_LEN);
106 let mut mac = <Hmac<Sha256>>::new_from_slice(secret).map_err(|_| TokenError::HmacKeyLength)?;
107 mac.update(body);
108 mac.verify_slice(tag).map_err(|_| TokenError::InvalidMac)?;
109 let for_node = u64::from_le_bytes(body[..8].try_into().expect("slice is 8 bytes"));
110 let expiry = u64::from_le_bytes(body[8..].try_into().expect("slice is 8 bytes"));
111 let now = SystemTime::now()
112 .duration_since(UNIX_EPOCH)
113 .map(|d| d.as_secs())
114 .unwrap_or_else(|_| {
115 tracing::error!(
116 "system clock is before UNIX_EPOCH during token verification; \
117 using 0 (epoch) — check NTP/RTC configuration"
118 );
119 0
120 });
121 if now > expiry {
122 return Err(TokenError::Expired);
123 }
124 Ok((for_node, expiry))
125}
126
127fn hex_decode(s: &str) -> Result<Vec<u8>, TokenError> {
128 let mut out = Vec::with_capacity(s.len() / 2);
129 for chunk in s.as_bytes().chunks(2) {
130 if chunk.len() != 2 {
131 return Err(TokenError::InvalidHex);
132 }
133 let hi = hex_digit(chunk[0]).ok_or(TokenError::InvalidHex)?;
134 let lo = hex_digit(chunk[1]).ok_or(TokenError::InvalidHex)?;
135 out.push((hi << 4) | lo);
136 }
137 Ok(out)
138}
139
140fn hex_digit(b: u8) -> Option<u8> {
141 match b {
142 b'0'..=b'9' => Some(b - b'0'),
143 b'a'..=b'f' => Some(10 + b - b'a'),
144 b'A'..=b'F' => Some(10 + b - b'A'),
145 _ => None,
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::time::Duration;
153
154 #[test]
155 fn roundtrip_verify_accepts_fresh_token() {
156 let secret = [0x11u8; 32];
157 let for_node = 42u64;
158 let expiry = SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .unwrap()
161 .as_secs()
162 + 60;
163 let hex = issue_token(&secret, for_node, expiry).unwrap();
164 let (got_node, got_exp) = verify_token(&hex, &secret).unwrap();
165 assert_eq!(got_node, for_node);
166 assert_eq!(got_exp, expiry);
167 }
168
169 #[test]
170 fn rejects_tampered_mac() {
171 let secret = [0x11u8; 32];
172 let expiry = SystemTime::now()
173 .duration_since(UNIX_EPOCH)
174 .unwrap()
175 .as_secs()
176 + 60;
177 let hex = issue_token(&secret, 1, expiry).unwrap();
178 let mut tampered = hex.clone();
182 let len = tampered.len();
183 let orig = u8::from_str_radix(&tampered[len - 2..len], 16).unwrap();
184 tampered.replace_range(len - 2..len, &format!("{:02x}", orig ^ 0xFF));
185 assert_eq!(
186 verify_token(&tampered, &secret).unwrap_err(),
187 TokenError::InvalidMac
188 );
189 }
190
191 #[test]
192 fn rejects_wrong_secret() {
193 let secret = [0x11u8; 32];
194 let expiry = SystemTime::now()
195 .duration_since(UNIX_EPOCH)
196 .unwrap()
197 .as_secs()
198 + 60;
199 let hex = issue_token(&secret, 5, expiry).unwrap();
200 let other = [0x22u8; 32];
201 assert_eq!(
202 verify_token(&hex, &other).unwrap_err(),
203 TokenError::InvalidMac
204 );
205 }
206
207 #[test]
208 fn rejects_expired() {
209 let secret = [0xAAu8; 32];
210 let expiry = 1u64;
212 let hex = issue_token(&secret, 1, expiry).unwrap();
213 assert_eq!(
214 verify_token(&hex, &secret).unwrap_err(),
215 TokenError::Expired
216 );
217 }
218
219 #[test]
220 fn rejects_wrong_length() {
221 let secret = [0x55u8; 32];
222 assert_eq!(
223 verify_token("deadbeef", &secret).unwrap_err(),
224 TokenError::WrongLength
225 );
226 }
227
228 #[test]
229 fn token_hash_stable() {
230 let secret = [0x33u8; 32];
231 let expiry = SystemTime::now()
232 .duration_since(UNIX_EPOCH)
233 .unwrap()
234 .as_secs()
235 + 60;
236 let hex = issue_token(&secret, 7, expiry).unwrap();
237 let h1 = token_hash(&hex).unwrap();
238 let h2 = token_hash(&hex).unwrap();
239 assert_eq!(h1, h2);
240 let _bytes = hex_decode(&hex).unwrap();
242 assert!(h1.iter().any(|&b| b != 0));
244 }
245
246 #[test]
252 fn invalid_mac_error_variant_confirms_constant_time_path() {
253 let secret = [0x77u8; 32];
254 let expiry = SystemTime::now()
255 .duration_since(UNIX_EPOCH)
256 .unwrap()
257 .as_secs()
258 + 60;
259 let hex = issue_token(&secret, 9, expiry).unwrap();
260 let header_hex_len = TOKEN_HEADER_LEN * 2;
262 let zero_mac = "00".repeat(TOKEN_MAC_LEN);
263 let tampered = format!("{}{}", &hex[..header_hex_len], zero_mac);
264 assert_eq!(tampered.len(), TOKEN_HEX_LEN);
265 assert_eq!(
266 verify_token(&tampered, &secret).unwrap_err(),
267 TokenError::InvalidMac,
268 "rejection must be via constant-time verify_slice, not short-circuit"
269 );
270 }
271
272 #[test]
275 fn issue_token_with_duration() {
276 let secret = [0xBBu8; 32];
277 let ttl = Duration::from_secs(300);
278 let now = SystemTime::now()
279 .duration_since(UNIX_EPOCH)
280 .unwrap()
281 .as_secs();
282 let expiry = now + ttl.as_secs();
283 let hex = issue_token(&secret, 3, expiry).unwrap();
284 assert_eq!(hex.len(), TOKEN_HEX_LEN);
285 let (node, exp) = verify_token(&hex, &secret).unwrap();
286 assert_eq!(node, 3);
287 assert!(exp >= now + ttl.as_secs() - 2); }
289}