1use anyhow::{Result, anyhow, bail};
8use base64::Engine as _;
9use hmac::{Hmac, Mac};
10use serde::{Deserialize, Serialize};
11use sha2::Sha256;
12
13type HmacSha256 = Hmac<Sha256>;
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Macaroon {
17 pub root_key_id: String,
18 pub identifier: String,
19 pub caveats: Vec<Caveat>,
20 pub signature: String,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(tag = "type", content = "value")]
25pub enum Caveat {
26 Sender(String),
27 Recipient(String),
28 Kind(u32),
29 Expiry(String),
30 MaxRate { max: u32, window_secs: u64 },
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct VerifyContext {
35 pub sender: String,
36 pub recipient: String,
37 pub kind: u32,
38 pub now: String,
39 pub rate_count: Option<u32>,
40}
41
42impl Macaroon {
43 pub fn mint(
44 root_key_id: impl Into<String>,
45 identifier: impl Into<String>,
46 caveats: Vec<Caveat>,
47 root_key: &[u8],
48 ) -> Result<Self> {
49 let root_key_id = root_key_id.into();
50 let identifier = identifier.into();
51 let signature = compute_signature(root_key, &identifier, &caveats)?;
52 Ok(Self {
53 root_key_id,
54 identifier,
55 caveats,
56 signature,
57 })
58 }
59
60 pub fn verify(&self, root_key: &[u8], context: &VerifyContext) -> Result<()> {
61 let expected = compute_signature(root_key, &self.identifier, &self.caveats)?;
62 if self.signature != expected {
63 bail!("macaroon signature mismatch");
64 }
65 for caveat in &self.caveats {
66 match caveat {
67 Caveat::Sender(sender) if sender != &context.sender => {
68 bail!("sender caveat mismatch")
69 }
70 Caveat::Recipient(recipient) if recipient != &context.recipient => {
71 bail!("recipient caveat mismatch")
72 }
73 Caveat::Kind(kind) if kind != &context.kind => bail!("kind caveat mismatch"),
74 Caveat::Expiry(expiry) => {
75 let expiry = parse_rfc3339(expiry)?;
76 let now = parse_rfc3339(&context.now)?;
77 if now > expiry {
78 bail!("expiry caveat elapsed");
79 }
80 }
81 Caveat::MaxRate { max, .. }
82 if context.rate_count.is_some_and(|count| count >= *max) =>
83 {
84 bail!("max-rate caveat exceeded");
85 }
86 _ => {}
87 }
88 }
89 Ok(())
90 }
91
92 pub fn serialize(&self) -> Result<String> {
93 let bytes = serde_json::to_vec(self)?;
94 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes))
95 }
96
97 pub fn deserialize(encoded: &str) -> Result<Self> {
98 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
99 .decode(encoded)
100 .map_err(|e| anyhow!("macaroon base64 decode failed: {e}"))?;
101 Ok(serde_json::from_slice(&bytes)?)
102 }
103}
104
105fn compute_signature(root_key: &[u8], identifier: &str, caveats: &[Caveat]) -> Result<String> {
106 let mut sig = hmac_bytes(root_key, identifier.as_bytes())?;
107 for caveat in caveats {
108 let body = serde_json::to_vec(caveat)?;
109 sig = hmac_bytes(&sig, &body)?;
110 }
111 Ok(hex::encode(sig))
112}
113
114fn hmac_bytes(key: &[u8], body: &[u8]) -> Result<Vec<u8>> {
115 let mut mac = HmacSha256::new_from_slice(key)?;
116 mac.update(body);
117 Ok(mac.finalize().into_bytes().to_vec())
118}
119
120fn parse_rfc3339(s: &str) -> Result<time::OffsetDateTime> {
121 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
122 .map_err(|e| anyhow!("invalid RFC3339 timestamp {s:?}: {e}"))
123}