cratefield_core/
signer.rs1use base64::Engine;
11use base64::engine::general_purpose::URL_SAFE_NO_PAD;
12use hmac::{Hmac, KeyInit, Mac};
13use serde::{Deserialize, Serialize};
14use sha2::Sha256;
15use subtle::ConstantTimeEq;
16
17use crate::ports::signer::{Kid, Payload, Signer};
18
19type HmacSha256 = Hmac<Sha256>;
20
21pub const MIN_SECRET_BYTES: usize = 32;
23
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
26pub enum SignerError {
27 #[error("harness secret must be at least {MIN_SECRET_BYTES} bytes")]
28 SecretTooShort,
29}
30
31#[derive(Serialize, Deserialize)]
32struct PayloadJson {
33 purpose: String,
34 subject: String,
35 #[serde(skip_serializing_if = "Option::is_none", default)]
36 exp: Option<u64>,
37 kid: String,
38}
39
40#[derive(Debug, Clone)]
42pub struct HmacSigner {
43 cur: Vec<u8>,
44 prev: Option<Vec<u8>>,
45}
46
47impl HmacSigner {
48 pub fn new(
53 current_secret: impl Into<String>,
54 previous_secret: Option<String>,
55 ) -> Result<Self, SignerError> {
56 let cur: Vec<u8> = current_secret.into().into_bytes();
57 if cur.len() < MIN_SECRET_BYTES {
58 return Err(SignerError::SecretTooShort);
59 }
60 Ok(Self {
61 cur,
62 prev: previous_secret.map(String::into_bytes),
63 })
64 }
65
66 fn mac(key: &[u8], encoded_payload: &str) -> [u8; 32] {
67 let mut mac =
68 <HmacSha256 as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
69 mac.update(encoded_payload.as_bytes());
70 let mut out = [0u8; 32];
71 out.copy_from_slice(&mac.finalize().into_bytes());
72 out
73 }
74
75 fn key_for(&self, kid: Kid) -> (&[u8], Option<Kid>) {
76 match (kid, self.prev.as_deref()) {
77 (Kid::Cur, Some(_)) => (&self.cur, Some(Kid::Prev)),
78 (Kid::Prev, Some(prev)) => (prev, Some(Kid::Cur)),
79 (Kid::Cur | Kid::Prev, None) => (&self.cur, None),
80 }
81 }
82}
83
84impl Signer for HmacSigner {
85 fn sign(&self, payload: &Payload) -> String {
86 let (kid, key) = match (payload.kid, self.prev.as_deref()) {
89 (Kid::Prev, Some(prev)) => (Kid::Prev, prev),
90 _ => (Kid::Cur, &self.cur[..]),
91 };
92 let json = PayloadJson {
93 purpose: payload.purpose.clone(),
94 subject: payload.subject.clone(),
95 exp: payload.exp,
96 kid: kid_name(kid).to_string(),
97 };
98 let encoded =
99 URL_SAFE_NO_PAD.encode(serde_json::to_string(&json).expect("payload serializes"));
100 let mac = Self::mac(key, &encoded);
101 format!("{encoded}.{}", URL_SAFE_NO_PAD.encode(mac))
102 }
103
104 fn verify(&self, token: &str, expected_purpose: &str) -> Option<Payload> {
105 let (encoded_payload, encoded_mac) = token.split_once('.')?;
106 if encoded_payload.is_empty() || encoded_mac.is_empty() {
107 return None;
108 }
109 if token.matches('.').count() != 1 {
110 return None;
111 }
112
113 let mac: [u8; 32] = URL_SAFE_NO_PAD.decode(encoded_mac).ok()?.try_into().ok()?;
114 let json = URL_SAFE_NO_PAD
115 .decode(encoded_payload)
116 .ok()
117 .and_then(|bytes| String::from_utf8(bytes).ok())?;
118 let payload: PayloadJson = serde_json::from_str(&json).ok()?;
119 let kid = parse_kid(&payload.kid)?;
120
121 let (named_key, fallback_kid) = self.key_for(kid);
125 let named_ok = bool::from(mac.ct_eq(&Self::mac(named_key, encoded_payload)));
126 let verified = named_ok
127 || match fallback_kid {
128 Some(other) => {
129 let (other_key, _) = self.key_for(other);
130 bool::from(mac.ct_eq(&Self::mac(other_key, encoded_payload)))
131 }
132 None => false,
133 };
134 if !verified {
135 return None;
136 }
137
138 if payload.purpose != expected_purpose {
139 return None;
140 }
141 if let Some(exp) = payload.exp {
142 let now = time::OffsetDateTime::now_utc()
143 .unix_timestamp()
144 .max(0)
145 .cast_unsigned();
146 if now >= exp {
147 return None;
148 }
149 }
150 Some(Payload {
151 purpose: payload.purpose,
152 subject: payload.subject,
153 exp: payload.exp,
154 kid,
155 })
156 }
157}
158
159fn kid_name(kid: Kid) -> &'static str {
160 match kid {
161 Kid::Cur => "cur",
162 Kid::Prev => "prev",
163 }
164}
165
166fn parse_kid(name: &str) -> Option<Kid> {
167 match name {
168 "cur" => Some(Kid::Cur),
169 "prev" => Some(Kid::Prev),
170 _ => None,
171 }
172}