Verify a hex-encoded token against `secret`. Returns the bound `(for_node, expiry_unix_secs)` on success. The HMAC comparison is constant-time (via `hmac::Mac::verify_slice` which uses the `subtle` crate internally).
(token_hex: &str, secret: &[u8; 32])
| 98 | /// The HMAC comparison is constant-time (via `hmac::Mac::verify_slice` |
| 99 | /// which uses the `subtle` crate internally). |
| 100 | pub 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 | |
| 127 | fn hex_decode(s: &str) -> Result<Vec<u8>, TokenError> { |
| 128 | let mut out = Vec::with_capacity(s.len() / 2); |