| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; |
| 7 | use hmac::{KeyInit, Mac}; |
| 8 | use serde::{Serialize, de::DeserializeOwned}; |
| 9 | |
| 10 | type HmacSha256 = hmac::Hmac<sha2::Sha256>; |
| 11 | |
| 12 | #[derive(serde::Serialize, serde::Deserialize)] |
| 13 | struct Envelope<T> { |
| 14 | exp: i64, |
| 15 | data: T, |
| 16 | } |
| 17 | |
| 18 | fn mac(key: &[u8], purpose: &str, payload: &str) -> Vec<u8> { |
| 19 | let mut mac = HmacSha256::new_from_slice(key).expect("hmac accepts any key length"); |
| 20 | mac.update(purpose.as_bytes()); |
| 21 | mac.update(b"\0"); |
| 22 | mac.update(payload.as_bytes()); |
| 23 | mac.finalize().into_bytes().to_vec() |
| 24 | } |
| 25 | |
| 26 | pub fn sign<T: Serialize>(key: &[u8], purpose: &str, data: &T, ttl_secs: i64) -> String { |
| 27 | let envelope = Envelope { exp: chrono::Utc::now().timestamp() + ttl_secs, data }; |
| 28 | let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&envelope).expect("serializable")); |
| 29 | let signature = URL_SAFE_NO_PAD.encode(mac(key, purpose, &payload)); |
| 30 | format!("{payload}.{signature}") |
| 31 | } |
| 32 | |
| 33 | pub fn verify<T: DeserializeOwned>(key: &[u8], purpose: &str, token: &str) -> Option<T> { |
| 34 | let (payload, signature) = token.split_once('.')?; |
| 35 | let signature = URL_SAFE_NO_PAD.decode(signature).ok()?; |
| 36 | let mut check = HmacSha256::new_from_slice(key).ok()?; |
| 37 | check.update(purpose.as_bytes()); |
| 38 | check.update(b"\0"); |
| 39 | check.update(payload.as_bytes()); |
| 40 | check.verify_slice(&signature).ok()?; |
| 41 | let envelope: Envelope<T> = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(payload).ok()?).ok()?; |
| 42 | (envelope.exp > chrono::Utc::now().timestamp()).then_some(envelope.data) |
| 43 | } |
| 44 | |
| 45 | #[cfg(test)] |
| 46 | mod tests { |
| 47 | use super::*; |
| 48 | |
| 49 | #[test] |
| 50 | fn roundtrip_and_purpose_binding() { |
| 51 | let key = [7u8; 32]; |
| 52 | let token = sign(&key, "lfs", &("alice", 3), 60); |
| 53 | assert_eq!(verify::<(String, i32)>(&key, "lfs", &token), Some(("alice".into(), 3))); |
| 54 | assert_eq!(verify::<(String, i32)>(&key, "registry", &token), None); |
| 55 | assert_eq!(verify::<(String, i32)>(&[8u8; 32], "lfs", &token), None); |
| 56 | } |
| 57 | |
| 58 | #[test] |
| 59 | fn expired_tokens_fail() { |
| 60 | let key = [1u8; 32]; |
| 61 | let token = sign(&key, "x", &1, -5); |
| 62 | assert_eq!(verify::<i32>(&key, "x", &token), None); |
| 63 | } |
| 64 | } |