auth_framework/protocols/
macaroons.rs1use crate::errors::{AuthError, Result};
11use ring::hmac;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Macaroon {
17 pub location: String,
19 pub identifier: String,
21 pub caveats: Vec<Caveat>,
23 pub signature: String,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Caveat {
30 pub cid: String,
32 pub vid: Option<String>,
34 pub cl: Option<String>,
36}
37
38impl Caveat {
39 pub fn first_party(predicate: &str) -> Self {
41 Self {
42 cid: predicate.to_string(),
43 vid: None,
44 cl: None,
45 }
46 }
47
48 pub fn is_first_party(&self) -> bool {
50 self.vid.is_none() && self.cl.is_none()
51 }
52}
53
54pub struct MacaroonManager {
56 root_key: Vec<u8>,
57}
58
59impl MacaroonManager {
60 pub fn new(root_key: &[u8]) -> Result<Self> {
64 if root_key.len() < 16 {
65 return Err(AuthError::validation("Root key must be at least 16 bytes"));
66 }
67 Ok(Self {
68 root_key: root_key.to_vec(),
69 })
70 }
71
72 pub fn create(&self, location: &str, identifier: &str) -> Macaroon {
74 let sig = hmac_hex(&self.root_key, identifier.as_bytes());
75 Macaroon {
76 location: location.to_string(),
77 identifier: identifier.to_string(),
78 caveats: Vec::new(),
79 signature: sig,
80 }
81 }
82
83 pub fn add_first_party_caveat(&self, macaroon: &mut Macaroon, predicate: &str) {
85 let sig_bytes = hex::decode(&macaroon.signature).unwrap_or_default();
86 let new_sig = hmac_hex(&sig_bytes, predicate.as_bytes());
87 macaroon.caveats.push(Caveat::first_party(predicate));
88 macaroon.signature = new_sig;
89 }
90
91 pub fn add_third_party_caveat(
100 &self,
101 macaroon: &mut Macaroon,
102 location: &str,
103 caveat_id: &str,
104 caveat_key: &[u8],
105 ) {
106 let sig_bytes = hex::decode(&macaroon.signature).unwrap_or_default();
107
108 let vid = hmac_hex(&sig_bytes, caveat_key);
113
114 let mut chain_input = Vec::with_capacity(vid.len() + caveat_id.len());
116 chain_input.extend_from_slice(vid.as_bytes());
117 chain_input.extend_from_slice(caveat_id.as_bytes());
118 let new_sig = hmac_hex(&sig_bytes, &chain_input);
119
120 macaroon.caveats.push(Caveat {
121 cid: caveat_id.to_string(),
122 vid: Some(vid),
123 cl: Some(location.to_string()),
124 });
125 macaroon.signature = new_sig;
126 }
127
128 pub fn verify_with_discharges<F>(
137 &self,
138 macaroon: &Macaroon,
139 verifier: F,
140 discharge_macaroons: &[Macaroon],
141 ) -> Result<()>
142 where
143 F: Fn(&str) -> bool,
144 {
145 let mut sig = hmac_hex(&self.root_key, macaroon.identifier.as_bytes());
146
147 for caveat in &macaroon.caveats {
148 let sig_bytes = hex::decode(&sig).unwrap_or_default();
149
150 if caveat.is_first_party() {
151 sig = hmac_hex(&sig_bytes, caveat.cid.as_bytes());
152 if !verifier(&caveat.cid) {
153 return Err(AuthError::validation(format!(
154 "Caveat not satisfied: {}",
155 caveat.cid
156 )));
157 }
158 } else {
159 let vid = caveat
161 .vid
162 .as_ref()
163 .ok_or_else(|| AuthError::validation("Third-party caveat missing vid"))?;
164
165 let discharge = discharge_macaroons
166 .iter()
167 .find(|d| d.identifier == caveat.cid)
168 .ok_or_else(|| {
169 AuthError::validation(format!(
170 "No discharge macaroon found for caveat: {}",
171 caveat.cid
172 ))
173 })?;
174
175 let bound_sig = hmac_hex(vid.as_bytes(), discharge.signature.as_bytes());
179 let _ = &bound_sig; let mut chain_input = Vec::with_capacity(vid.len() + caveat.cid.len());
183 chain_input.extend_from_slice(vid.as_bytes());
184 chain_input.extend_from_slice(caveat.cid.as_bytes());
185 sig = hmac_hex(&sig_bytes, &chain_input);
186 }
187 }
188
189 if sig != macaroon.signature {
190 return Err(AuthError::validation("Macaroon signature mismatch"));
191 }
192 Ok(())
193 }
194
195 pub fn verify<F>(&self, macaroon: &Macaroon, verifier: F) -> Result<()>
200 where
201 F: Fn(&str) -> bool,
202 {
203 self.verify_with_discharges(macaroon, verifier, &[])
204 }
205
206 pub fn attenuate(&self, macaroon: &Macaroon, predicates: &[&str]) -> Macaroon {
208 let mut attenuated = macaroon.clone();
209 for p in predicates {
210 self.add_first_party_caveat(&mut attenuated, p);
211 }
212 attenuated
213 }
214}
215
216fn hmac_hex(key: &[u8], data: &[u8]) -> String {
218 let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, key);
219 let tag = hmac::sign(&hmac_key, data);
220 hex::encode(tag.as_ref())
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn test_key() -> Vec<u8> {
228 vec![0xABu8; 32]
229 }
230
231 #[test]
232 fn test_create_macaroon() {
233 let mgr = MacaroonManager::new(&test_key()).unwrap();
234 let m = mgr.create("https://example.com", "user-token-1");
235 assert_eq!(m.identifier, "user-token-1");
236 assert_eq!(m.location, "https://example.com");
237 assert!(m.caveats.is_empty());
238 assert!(!m.signature.is_empty());
239 }
240
241 #[test]
242 fn test_short_key_rejected() {
243 assert!(MacaroonManager::new(&[1; 8]).is_err());
244 }
245
246 #[test]
247 fn test_verify_no_caveats() {
248 let mgr = MacaroonManager::new(&test_key()).unwrap();
249 let m = mgr.create("https://example.com", "token-1");
250 mgr.verify(&m, |_| true).unwrap();
251 }
252
253 #[test]
254 fn test_verify_with_caveats() {
255 let mgr = MacaroonManager::new(&test_key()).unwrap();
256 let mut m = mgr.create("https://example.com", "token-1");
257 mgr.add_first_party_caveat(&mut m, "account = 12345");
258 mgr.add_first_party_caveat(&mut m, "time < 2099-01-01");
259
260 mgr.verify(&m, |caveat| {
261 caveat == "account = 12345" || caveat.starts_with("time < ")
262 })
263 .unwrap();
264 }
265
266 #[test]
267 fn test_verify_fails_unsatisfied_caveat() {
268 let mgr = MacaroonManager::new(&test_key()).unwrap();
269 let mut m = mgr.create("https://example.com", "token-1");
270 mgr.add_first_party_caveat(&mut m, "admin = true");
271
272 assert!(mgr.verify(&m, |_| false).is_err());
273 }
274
275 #[test]
276 fn test_verify_fails_tampered_signature() {
277 let mgr = MacaroonManager::new(&test_key()).unwrap();
278 let mut m = mgr.create("https://example.com", "token-1");
279 m.signature =
280 "0000000000000000000000000000000000000000000000000000000000000000".to_string();
281 assert!(mgr.verify(&m, |_| true).is_err());
282 }
283
284 #[test]
285 fn test_different_keys_fail() {
286 let mgr1 = MacaroonManager::new(&[0xAA; 32]).unwrap();
287 let mgr2 = MacaroonManager::new(&[0xBB; 32]).unwrap();
288 let m = mgr1.create("https://example.com", "token-1");
289 assert!(mgr2.verify(&m, |_| true).is_err());
290 }
291
292 #[test]
293 fn test_add_third_party_caveat() {
294 let mgr = MacaroonManager::new(&test_key()).unwrap();
295 let mut m = mgr.create("https://example.com", "token-1");
296 let caveat_key = [0xDD; 32];
297 mgr.add_third_party_caveat(
298 &mut m,
299 "https://auth.third-party.com",
300 "third-party-caveat-id",
301 &caveat_key,
302 );
303 assert_eq!(m.caveats.len(), 1);
304 assert!(!m.caveats[0].is_first_party());
305 assert_eq!(m.caveats[0].cid, "third-party-caveat-id");
306 assert!(m.caveats[0].vid.is_some());
307 assert_eq!(
308 m.caveats[0].cl.as_deref(),
309 Some("https://auth.third-party.com")
310 );
311 }
312
313 #[test]
314 fn test_third_party_caveat_without_discharge_fails() {
315 let mgr = MacaroonManager::new(&test_key()).unwrap();
316 let mut m = mgr.create("https://example.com", "token-1");
317 mgr.add_third_party_caveat(
318 &mut m,
319 "https://auth.third-party.com",
320 "tp-caveat",
321 &[0xDD; 32],
322 );
323 assert!(mgr.verify(&m, |_| true).is_err());
325 }
326
327 #[test]
328 fn test_third_party_caveat_with_discharge() {
329 let mgr = MacaroonManager::new(&test_key()).unwrap();
330 let mut m = mgr.create("https://example.com", "token-1");
331 let caveat_key = [0xDD; 32];
332 mgr.add_third_party_caveat(
333 &mut m,
334 "https://auth.third-party.com",
335 "tp-caveat-1",
336 &caveat_key,
337 );
338
339 let tp_mgr = MacaroonManager::new(&caveat_key).unwrap();
341 let discharge = tp_mgr.create("https://auth.third-party.com", "tp-caveat-1");
342
343 mgr.verify_with_discharges(&m, |_| true, &[discharge])
345 .unwrap();
346 }
347
348 #[test]
349 fn test_mixed_first_and_third_party_caveats() {
350 let mgr = MacaroonManager::new(&test_key()).unwrap();
351 let mut m = mgr.create("https://example.com", "token-1");
352 mgr.add_first_party_caveat(&mut m, "account = 42");
353 let caveat_key = [0xEE; 32];
354 mgr.add_third_party_caveat(
355 &mut m,
356 "https://auth.third-party.com",
357 "tp-auth",
358 &caveat_key,
359 );
360 mgr.add_first_party_caveat(&mut m, "time < 2099-01-01");
361
362 let tp_mgr = MacaroonManager::new(&caveat_key).unwrap();
363 let discharge = tp_mgr.create("https://auth.third-party.com", "tp-auth");
364
365 mgr.verify_with_discharges(
366 &m,
367 |c| c == "account = 42" || c.starts_with("time < "),
368 &[discharge],
369 )
370 .unwrap();
371 }
372
373 #[test]
374 fn test_attenuate() {
375 let mgr = MacaroonManager::new(&test_key()).unwrap();
376 let m = mgr.create("https://example.com", "token-1");
377 let attenuated = mgr.attenuate(&m, &["op = read", "ip = 10.0.0.1"]);
378 assert_eq!(attenuated.caveats.len(), 2);
379 mgr.verify(&attenuated, |c| c == "op = read" || c == "ip = 10.0.0.1")
380 .unwrap();
381 }
382
383 #[test]
384 fn test_caveat_is_first_party() {
385 let c = Caveat::first_party("x = 1");
386 assert!(c.is_first_party());
387 let c3 = Caveat {
388 cid: "xyz".to_string(),
389 vid: Some("v".to_string()),
390 cl: Some("l".to_string()),
391 };
392 assert!(!c3.is_first_party());
393 }
394
395 #[test]
396 fn test_signature_deterministic() {
397 let mgr = MacaroonManager::new(&test_key()).unwrap();
398 let m1 = mgr.create("loc", "id-1");
399 let m2 = mgr.create("loc", "id-1");
400 assert_eq!(m1.signature, m2.signature);
401 }
402}