libmacaroon/lib.rs
1//! Implementation of [Macaroons](http://research.google.com/pubs/pub41892.html) for Rust, which are
2//! flexible authorization tokens for distributed systems. They are similar to cookies, but allow for
3//! more narrowly-focused authorization based on contextual caveats.
4//!
5//! # What Are Macaroons?
6//!
7//! Macaroons are bearer tokens (similar to cookies) which encode within them criteria within which the
8//! authorization is allowed to take place (referred to as "caveats"). For instance, authorization could
9//! be restricted to a particular user, account, time of day, really anything. These criteria can be either
10//! evaluated locally (a "first-party caveat"), or using special macaroons ("discharge macaroons") generated
11//! by a third party (a "third-party caveat").
12//!
13//! A first-party caveat consists simply of a predicate which, when evaluated as true, authorizes the caveat.
14//! The predicate is a string which is either evaluated using strict string comparison (`satisfy_exact`),
15//! or interpreted using a provided function (`satisfy_general`).
16//!
17//! A third-party caveat consists of a location string, an identifier, and a specially-generated signing key
18//! to authenticate the generated discharge macaroons. The key and identifier is passed to the third-party
19//! who generates the discharge macaroons. The receiver then binds each discharge macaroon to the original
20//! macaroon.
21//!
22//! During verification of a third-party caveat, a discharge macaroon is found from those received whose identifier
23//! matches that of the caveat. The binding signature is verified, and the discharge macaroon's caveats are verified
24//! using the same process as the original macaroon.
25//!
26//! The macaroon is considered authorized only if all its caveats are authorized by the above process.
27//!
28//! # Example
29//!
30//! ```rust
31//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! use libmacaroon::{Macaroon, Verifier, MacaroonKey};
33//!
34//! // Create our key.
35//! let key = MacaroonKey::generate(b"key");
36//!
37//! // Create our macaroon. A location is optional.
38//! let mut macaroon = Macaroon::create(Some("location"), &key, "id")?;
39//!
40//! // Add a first-party caveat: only someone identified as account 12345678
41//! // is authorized to use this macaroon. Multiple caveats with different
42//! // predicates can be layered on.
43//! macaroon.add_first_party_caveat("account = 12345678")?;
44//!
45//! // Build a verifier with the predicates we're willing to accept.
46//! let mut verifier = Verifier::default();
47//! verifier.satisfy_exact("account = 12345678");
48//!
49//! // Verify. Returns Ok(()) on success.
50//! verifier.verify(&macaroon, &key, &[])?;
51//!
52//! // Now a third-party caveat: verification requires a discharge macaroon
53//! // issued by a third party under a separate key.
54//! let other_key = MacaroonKey::generate(b"different key");
55//! macaroon.add_third_party_caveat("https://auth.mybank", &other_key, "caveat id")?;
56//!
57//! // The third party creates the discharge using the same caveat id and key.
58//! let mut discharge = Macaroon::create(
59//! Some("http://auth.mybank/"),
60//! &other_key,
61//! "caveat id",
62//! )?;
63//! discharge.add_first_party_caveat("account = 12345678")?;
64//!
65//! // Bind the discharge to the original macaroon so it cannot be reused
66//! // against a different authorizing macaroon.
67//! macaroon.bind(&mut discharge);
68//!
69//! // Same verifier, now with the discharge supplied.
70//! verifier.verify(&macaroon, &key, &[discharge])?;
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! # Supported Features
76//!
77//! This crate supports all the following features:
78//!
79//! - verification of first-party caveats either via exact string match or passed-in function
80//! - verification of third-party caveats using discharge macaroons (including ones that themselves have embedded third-party caveats)
81//! - serialization and deserialization of caveats via version 1, 2 or 2J serialization formats (fully compatible with libmacaroons)
82
83use base64::{
84 alphabet,
85 engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
86 Engine as _,
87};
88use log::debug;
89
90/// Base64 engines that tolerate both padded and unpadded input when decoding,
91/// matching the lenient behavior of the `base64 0.13` `STANDARD` / `URL_SAFE`
92/// configs. Needed so that macaroons serialized by other libraries — which may
93/// omit or include `=` padding — round-trip correctly.
94pub(crate) const STANDARD: GeneralPurpose = GeneralPurpose::new(
95 &alphabet::STANDARD,
96 GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
97);
98pub(crate) const URL_SAFE: GeneralPurpose = GeneralPurpose::new(
99 &alphabet::URL_SAFE,
100 GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
101);
102/// Used on the emit side for V2JSON fields (`i64`, `v64`, `s64`) so our
103/// output matches what libmacaroons/pymacaroons produce: URL-safe alphabet,
104/// no `=` padding. The decode side tolerates both padded and unpadded.
105pub(crate) const URL_SAFE_NO_PAD: GeneralPurpose = GeneralPurpose::new(
106 &alphabet::URL_SAFE,
107 GeneralPurposeConfig::new()
108 .with_encode_padding(false)
109 .with_decode_padding_mode(DecodePaddingMode::Indifferent),
110);
111
112mod caveat;
113mod crypto;
114mod error;
115mod serialization;
116mod verifier;
117
118pub use caveat::{Caveat, FirstParty, ThirdParty};
119pub use crypto::MacaroonKey;
120pub use error::MacaroonError;
121pub use serialization::Format;
122pub use verifier::Verifier;
123
124#[cfg(feature = "v2json")]
125use serde::de::Visitor;
126#[cfg(feature = "v2json")]
127use serde::{Deserialize, Deserializer, Serialize, Serializer};
128use std::fmt;
129
130pub type Result<T> = std::result::Result<T, MacaroonError>;
131
132/// Maximum number of caveats allowed on a single macaroon. Applied on both
133/// construction (`add_first_party_caveat`, `add_third_party_caveat`) and
134/// deserialization to bound memory and verification work.
135pub const MAX_CAVEATS: usize = 1000;
136
137/// Maximum byte length accepted for any single field (identifier, location,
138/// predicate, VID, signature) during construction and deserialization.
139///
140/// Bounds memory and parsing work on untrusted input, and ensures the V1
141/// packet format (whose size header is four hex digits, capping a packet at
142/// `0xFFFF` bytes) can represent every macaroon this crate produces. The cap
143/// applies symmetrically: a macaroon that this crate can serialize is always
144/// one this crate can parse back.
145pub const MAX_FIELD_SIZE_BYTES: usize = 65535;
146
147pub(crate) fn check_field_size(field: &'static str, size: usize) -> Result<()> {
148 if size > MAX_FIELD_SIZE_BYTES {
149 return Err(MacaroonError::FieldTooLarge { field, size });
150 }
151 Ok(())
152}
153
154// Internal type representing binary data. By spec, most fields in a macaroon
155// support binary encoded as base64.
156#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
157pub(crate) struct ByteString(pub(crate) Vec<u8>);
158
159impl AsRef<[u8]> for ByteString {
160 fn as_ref(&self) -> &[u8] {
161 &self.0
162 }
163}
164
165impl std::borrow::Borrow<[u8]> for ByteString {
166 fn borrow(&self) -> &[u8] {
167 &self.0
168 }
169}
170
171impl From<Vec<u8>> for ByteString {
172 fn from(v: Vec<u8>) -> ByteString {
173 ByteString(v)
174 }
175}
176
177impl From<&[u8]> for ByteString {
178 fn from(s: &[u8]) -> ByteString {
179 ByteString(s.to_vec())
180 }
181}
182
183impl From<&str> for ByteString {
184 fn from(s: &str) -> ByteString {
185 ByteString(s.as_bytes().to_vec())
186 }
187}
188
189impl From<String> for ByteString {
190 fn from(s: String) -> ByteString {
191 ByteString(s.as_bytes().to_vec())
192 }
193}
194
195impl From<[u8; 32]> for ByteString {
196 fn from(b: [u8; 32]) -> ByteString {
197 ByteString(b.to_vec())
198 }
199}
200
201impl fmt::Display for ByteString {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 // Emit URL-safe base64 without padding — matches libmacaroons /
204 // pymacaroons V2JSON output, which is the wire format these fields
205 // (`i64`, `v64`) travel in.
206 write!(f, "{}", URL_SAFE_NO_PAD.encode(&self.0))
207 }
208}
209
210#[cfg(feature = "v2json")]
211impl Serialize for ByteString {
212 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
213 where
214 S: Serializer,
215 {
216 serializer.serialize_str(&self.to_string())
217 }
218}
219
220#[cfg(feature = "v2json")]
221struct ByteStringVisitor;
222
223#[cfg(feature = "v2json")]
224impl<'de> Visitor<'de> for ByteStringVisitor {
225 type Value = ByteString;
226
227 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
228 formatter.write_str("base64 encoded string of bytes")
229 }
230
231 fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
232 where
233 E: serde::de::Error,
234 {
235 // Tolerate both standard and URL-safe alphabets, padded or unpadded:
236 // libmacaroons and pymacaroons emit URL-safe no-pad, but some clients
237 // use the standard alphabet. Accepting both here fixes a real interop
238 // break on V2JSON `i64` / `v64` fields.
239 let raw = base64_decode_flexible(value.as_bytes())
240 .map_err(|_| E::custom("unable to base64 decode value"))?;
241 Ok(ByteString(raw))
242 }
243}
244
245#[cfg(feature = "v2json")]
246impl<'de> Deserialize<'de> for ByteString {
247 fn deserialize<D>(deserializer: D) -> std::result::Result<ByteString, D::Error>
248 where
249 D: Deserializer<'de>,
250 {
251 deserializer.deserialize_str(ByteStringVisitor)
252 }
253}
254
255/// Internal helper to decode base64 tokens in either URL-safe or non-URL-safe format, with or
256/// without padding. The macaroons format specifies that macaroons should be accepted in any of
257/// these variations.
258///
259/// Logic is based on pymacaroons helper:
260/// https://github.com/ecordell/pymacaroons/blob/master/pymacaroons/utils.py#L109
261fn base64_decode_flexible(b: &[u8]) -> Result<Vec<u8>> {
262 if b.is_empty() {
263 return Err(MacaroonError::DeserializationError(
264 "empty token to deserialize".to_string(),
265 ));
266 }
267 if b.contains(&b'_') || b.contains(&b'-') {
268 Ok(URL_SAFE.decode(b)?)
269 } else {
270 Ok(STANDARD.decode(b)?)
271 }
272}
273
274// https://github.com/rescrv/libmacaroons/blob/master/doc/format.txt#L87
275#[test]
276fn test_base64_decode_flexible() {
277 let val = b"Ou?T".to_vec();
278 assert_eq!(val, base64_decode_flexible(b"T3U/VA==").unwrap());
279 assert_eq!(val, base64_decode_flexible(b"T3U_VA==").unwrap());
280 assert_eq!(val, base64_decode_flexible(b"T3U/VA").unwrap());
281 assert_eq!(val, base64_decode_flexible(b"T3U_VA").unwrap());
282
283 assert!(base64_decode_flexible(b"...").is_err());
284 assert!(base64_decode_flexible(b"").is_err());
285}
286
287#[derive(Clone, Debug, PartialEq, Eq)]
288pub struct Macaroon {
289 identifier: ByteString,
290 location: Option<String>,
291 signature: MacaroonKey,
292 caveats: Vec<Caveat>,
293}
294
295impl Macaroon {
296 /// Construct a macaroon, given a location and identifier, and a key to sign
297 /// it with. You can use a bare str or &[u8] containing arbitrary data with
298 /// `into` to automatically generate a suitable key.
299 ///
300 /// # Errors
301 ///
302 /// - [`MacaroonError::IncompleteMacaroon`] if the identifier is empty.
303 /// - [`MacaroonError::FieldTooLarge`] if the identifier or location
304 /// exceeds [`MAX_FIELD_SIZE_BYTES`].
305 pub fn create(
306 location: Option<&str>,
307 key: &MacaroonKey,
308 identifier: impl AsRef<[u8]>,
309 ) -> Result<Macaroon> {
310 let identifier_bytes = identifier.as_ref();
311 check_field_size("identifier", identifier_bytes.len())?;
312 if let Some(loc) = location {
313 check_field_size("location", loc.len())?;
314 }
315 let identifier = ByteString(identifier_bytes.to_vec());
316 let signature = crypto::hmac(key, &identifier);
317 let macaroon = Macaroon {
318 location: location.map(str::to_string),
319 identifier,
320 signature,
321 caveats: Vec::new(),
322 };
323 debug!("Macaroon::create: {:?}", macaroon);
324 macaroon.validate()
325 }
326
327 /// Returns the identifier for the macaroon as a byte slice
328 pub fn identifier(&self) -> &[u8] {
329 &self.identifier.0
330 }
331
332 /// Returns a reference to the location for the macaroon
333 pub fn location(&self) -> Option<&str> {
334 self.location.as_deref()
335 }
336
337 /// Returns the macaroon's signature
338 ///
339 /// The [MacaroonKey] type is used because it is the same size and format as a signature, but
340 /// the signature is not and should not be used as a cryptographic key.
341 pub fn signature(&self) -> &MacaroonKey {
342 &self.signature
343 }
344
345 /// Returns a reference to the list of caveats
346 pub fn caveats(&self) -> &[Caveat] {
347 &self.caveats
348 }
349
350 /// Retrieve a list of references to the first-party caveats
351 pub fn first_party_caveats(&self) -> Vec<&Caveat> {
352 self.caveats
353 .iter()
354 .filter(|c| matches!(c, caveat::Caveat::FirstParty(_)))
355 .collect()
356 }
357
358 /// Retrieve a list of references to the third-party caveats
359 pub fn third_party_caveats(&self) -> Vec<&Caveat> {
360 self.caveats
361 .iter()
362 .filter(|c| matches!(c, caveat::Caveat::ThirdParty(_)))
363 .collect()
364 }
365
366 /// Validate that a Macaroon has all the expected fields
367 ///
368 /// This is a low-level function to confirm that a macaroon was constructured correctly. It
369 /// does *not* verify the signature, caveats, or in any way confirm that a macaroon is
370 /// authentic from a security standpoint.
371 fn validate(self) -> Result<Self> {
372 if self.identifier.0.is_empty() {
373 return Err(MacaroonError::IncompleteMacaroon("no identifier found"));
374 }
375 // The `signature` field is required by the struct definition and is
376 // always produced by `crypto::hmac` during construction or set from a
377 // validated 32-byte field during deserialization, so there is no
378 // "missing signature" state to guard against here.
379 Ok(self)
380 }
381
382 /// Add a first-party caveat to the macaroon
383 ///
384 /// A first-party caveat is just a string predicate in some
385 /// DSL which can be verified either by exact string match,
386 /// or by using a function to parse the string and validate it
387 /// (see Verifier for more info).
388 ///
389 /// # Errors
390 ///
391 /// - [`MacaroonError::TooManyCaveats`] if adding this caveat would exceed
392 /// the [`MAX_CAVEATS`] limit.
393 /// - [`MacaroonError::FieldTooLarge`] if the predicate exceeds
394 /// [`MAX_FIELD_SIZE_BYTES`].
395 ///
396 /// ```
397 /// # use libmacaroon::{Macaroon, MacaroonKey};
398 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
399 /// let key = MacaroonKey::generate(b"k");
400 /// let mut m = Macaroon::create(Some("loc"), &key, "id")?;
401 /// m.add_first_party_caveat("account = 12345")?
402 /// .add_first_party_caveat("user = alice")?;
403 /// # Ok(()) }
404 /// ```
405 pub fn add_first_party_caveat(&mut self, predicate: impl AsRef<[u8]>) -> Result<&mut Self> {
406 let predicate_bytes = predicate.as_ref();
407 check_field_size("predicate", predicate_bytes.len())?;
408 self.check_caveat_capacity()?;
409 let caveat: caveat::Caveat = caveat::new_first_party(ByteString(predicate_bytes.to_vec()));
410 self.signature = caveat.sign(&self.signature);
411 self.caveats.push(caveat);
412 debug!("Macaroon::add_first_party_caveat: {:?}", self);
413 Ok(self)
414 }
415
416 /// Add a third-party caveat to the macaroon
417 ///
418 /// A third-party caveat is a caveat which must be verified by a third party
419 /// using macaroons provided by them (referred to as "discharge macaroons").
420 ///
421 /// # Errors
422 ///
423 /// - [`MacaroonError::TooManyCaveats`] if adding this caveat would exceed
424 /// the [`MAX_CAVEATS`] limit.
425 /// - [`MacaroonError::FieldTooLarge`] if the location or id exceeds
426 /// [`MAX_FIELD_SIZE_BYTES`].
427 /// - [`MacaroonError::RngError`] if the OS RNG fails while generating the
428 /// nonce used to encrypt the caveat key (can happen in WASM environments
429 /// without `crypto.getRandomValues`).
430 pub fn add_third_party_caveat(
431 &mut self,
432 location: &str,
433 key: &MacaroonKey,
434 id: impl AsRef<[u8]>,
435 ) -> Result<&mut Self> {
436 let id_bytes = id.as_ref();
437 check_field_size("caveat id", id_bytes.len())?;
438 check_field_size("caveat location", location.len())?;
439 self.check_caveat_capacity()?;
440 let vid: Vec<u8> = crypto::encrypt_key(&self.signature, key)?;
441 let caveat: caveat::Caveat =
442 caveat::new_third_party(ByteString(id_bytes.to_vec()), ByteString(vid), location);
443 self.signature = caveat.sign(&self.signature);
444 self.caveats.push(caveat);
445 debug!("Macaroon::add_third_party_caveat: {:?}", self);
446 Ok(self)
447 }
448
449 fn check_caveat_capacity(&self) -> Result<()> {
450 if self.caveats.len() >= MAX_CAVEATS {
451 return Err(MacaroonError::TooManyCaveats);
452 }
453 Ok(())
454 }
455
456 /// Bind a discharge macaroon to the original macaroon
457 ///
458 /// When a macaroon with third-party caveats must be authorized, you send off to the various
459 /// locations specified in the caveats, sending the caveat ID and key, and receive a set
460 /// of one or more "discharge macaroons" which are used to verify the caveat. In order to ensure
461 /// that the discharge macaroons aren't re-used in some other context, we bind them to the original
462 /// macaroon so that they can't be used in a different context.
463 pub fn bind(&self, discharge: &mut Macaroon) {
464 let zero_key = MacaroonKey::from([0; 32]);
465 discharge.signature = crypto::hmac2(&zero_key, &self.signature, &discharge.signature);
466 debug!(
467 "Macaroon::bind: original: {:?}, discharge: {:?}",
468 self, discharge
469 );
470 }
471
472 /// Serialize the macaroon using the serialization [Format] provided
473 ///
474 /// For V1 and V2, the binary format will be encoded as URL-safe base64 with padding
475 /// (`base64::URL_SAFE`). For V2JSON, the output will be JSON.
476 pub fn serialize(&self, format: serialization::Format) -> Result<String> {
477 match format {
478 serialization::Format::V1 => serialization::v1::serialize(self),
479 serialization::Format::V2 => serialization::v2::serialize(self),
480 #[cfg(feature = "v2json")]
481 serialization::Format::V2JSON => serialization::v2json::serialize(self),
482 }
483 }
484
485 /// Deserialize an encoded macaroon token, inferring the [Format].
486 ///
487 /// For V1 and V2 tokens, this assumes base64 encoding, in either "standard" or URL-safe
488 /// encoding, with or without padding.
489 ///
490 /// For V2JSON tokens, the token must begin with the `{` character with no preceeding whitespace.
491 ///
492 /// ## Usage
493 ///
494 /// ```rust
495 /// use libmacaroon::Macaroon;
496 /// # use std::error::Error;
497 /// # fn main() -> Result<(), Box<dyn Error>> {
498 ///
499 /// // '&str' gets automatically de-referenced to bytes ('&[u8]').
500 /// // 'b"byte-string"' or slice of 'u8' would also work.
501 /// let mac = Macaroon::deserialize("MDAxY2xvY2F0aW9uIGh0dHA6Ly9teWJhbmsvCjAwMjZpZGVudGlmaWVyIHdlIHVzZWQgb3VyIHNlY3JldCBrZXkKMDAxNmNpZCB0ZXN0ID0gY2F2ZWF0CjAwMmZzaWduYXR1cmUgGXusegRK8zMyhluSZuJtSTvdZopmDkTYjOGpmMI9vWcK")?;
502 ///
503 /// # #[cfg(feature = "v2json")] {
504 /// let mac_v2json = Macaroon::deserialize(r#"{"v":2,"l":"http://example.org/","i":"keyid", "c":[{"i":"account = 3735928559"},{"i":"user = alice"}],"s64": "S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw"}"#)?;
505 ///
506 /// // expect this to fail; leading whitespace is not allowed
507 /// Macaroon::deserialize(r#" {"v":2,"l":"http://example.org/","i":"keyid", "c":[{"i":"account = 3735928559"},{"i":"user = alice"}],"s64": "S-lnzR6gxrJrr2pKlO6bBbFYhtoLqF6MQqk8jQ4SXvw"}"#).unwrap_err();
508 /// # }
509 /// # Ok(()) }
510 /// ```
511 pub fn deserialize<T: AsRef<[u8]>>(token: T) -> Result<Macaroon> {
512 if token.as_ref().is_empty() {
513 return Err(MacaroonError::DeserializationError(
514 "empty token provided".to_string(),
515 ));
516 }
517 let mac: Macaroon = match token.as_ref()[0] as char {
518 #[cfg(feature = "v2json")]
519 '{' => serialization::v2json::deserialize(token.as_ref())?,
520 #[cfg(not(feature = "v2json"))]
521 '{' => {
522 return Err(MacaroonError::DeserializationError(
523 "V2JSON support is disabled (the `v2json` feature is not enabled)".to_string(),
524 ))
525 }
526 _ => {
527 let binary = base64_decode_flexible(token.as_ref())?;
528 Macaroon::deserialize_binary(&binary)?
529 }
530 };
531 mac.validate()
532 }
533
534 /// Deserialize a binary macaroon token in binary, inferring the [Format]
535 ///
536 /// This works with V1 and V2 tokens, with no base64 encoding. It does not make sense to use
537 /// this with V2JSON tokens.
538 pub fn deserialize_binary(token: &[u8]) -> Result<Macaroon> {
539 if token.is_empty() {
540 return Err(MacaroonError::DeserializationError(
541 "empty macaroon token".to_string(),
542 ));
543 }
544 // V2 binary starts with the literal version byte 0x02. V1 binary
545 // starts with a four-hex-digit packet length, so the first byte must
546 // be a hex digit (the previous `'A'..='Z'` range accidentally
547 // accepted non-hex letters, which the parser would then reject with
548 // a less informative error).
549 let mac: Macaroon = match token[0] as char {
550 '\x02' => serialization::v2::deserialize(token)?,
551 '0'..='9' | 'a'..='f' | 'A'..='F' => serialization::v1::deserialize(token)?,
552 _ => {
553 return Err(MacaroonError::DeserializationError(
554 "unknown macaroon serialization format".to_string(),
555 ))
556 }
557 };
558 mac.validate()
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use crate::{Caveat, Macaroon, MacaroonError, MacaroonKey, Result, Verifier};
565
566 #[test]
567 fn create_macaroon() {
568 let signature: MacaroonKey = [
569 20, 248, 23, 46, 70, 227, 253, 33, 123, 35, 116, 236, 130, 131, 211, 16, 41, 184, 51,
570 65, 213, 46, 109, 76, 49, 201, 186, 92, 114, 163, 214, 231,
571 ]
572 .into();
573 // NOTE: using byte string directly, not generating with HMAC
574 let key = MacaroonKey::from(b"this is a super duper secret key");
575 let macaroon_res = Macaroon::create(Some("location"), &key, "identifier");
576 assert!(macaroon_res.is_ok());
577 let macaroon = macaroon_res.unwrap();
578 assert!(macaroon.location.is_some());
579 assert_eq!("location", macaroon.location.as_deref().unwrap());
580 assert_eq!(macaroon.identifier(), b"identifier");
581 assert_eq!(signature, macaroon.signature);
582 assert_eq!(0, macaroon.caveats.len());
583 }
584
585 #[test]
586 fn create_invalid_macaroon() {
587 // NOTE: using byte string directly, not generating with HMAC
588 let key = MacaroonKey::from(b"this is a super duper secret key");
589 let macaroon_res: Result<Macaroon> = Macaroon::create(Some("location"), &key, "");
590 assert!(macaroon_res.is_err());
591 assert!(matches!(
592 macaroon_res,
593 Err(MacaroonError::IncompleteMacaroon(_))
594 ));
595 println!("{}", macaroon_res.unwrap_err());
596 }
597
598 #[test]
599 fn create_macaroon_errors() {
600 let deser_err = Macaroon::deserialize(b"\0");
601 assert!(matches!(
602 deser_err,
603 Err(MacaroonError::DeserializationError(_))
604 ));
605 println!("{}", deser_err.unwrap_err());
606
607 let key = MacaroonKey::generate(b"this is a super duper secret key");
608 let mut mac = Macaroon::create(Some("http://mybank"), &key, "identifier").unwrap();
609
610 let mut ver = Verifier::default();
611 let wrong_key = MacaroonKey::generate(b"not what was expected");
612 let sig_err = ver.verify(&mac, &wrong_key, &[]);
613 assert!(matches!(sig_err, Err(MacaroonError::InvalidSignature)));
614 println!("{}", sig_err.unwrap_err());
615 assert!(ver.verify(&mac, &key, &[]).is_ok());
616
617 mac.add_first_party_caveat("account = 3735928559").unwrap();
618 let cav_err = ver.verify(&mac, &key, &[]);
619 assert!(matches!(cav_err, Err(MacaroonError::CaveatNotSatisfied(_))));
620 println!("{}", cav_err.unwrap_err());
621 ver.satisfy_exact("account = 3735928559");
622 assert!(ver.verify(&mac, &key, &[]).is_ok());
623
624 let mut mac2 = mac.clone();
625 let cav_key = MacaroonKey::generate(b"My key");
626 mac2.add_third_party_caveat("other location", &cav_key, "other ident")
627 .unwrap();
628 let cav_err = ver.verify(&mac2, &key, &[]);
629 assert!(matches!(cav_err, Err(MacaroonError::CaveatNotSatisfied(_))));
630 println!("{}", cav_err.unwrap_err());
631
632 let discharge =
633 Macaroon::create(Some("http://auth.mybank/"), &cav_key, "other keyid").unwrap();
634 let disch_err = ver.verify(&mac, &key, &[discharge]);
635 assert!(matches!(disch_err, Err(MacaroonError::DischargeNotUsed)));
636 println!("{}", disch_err.unwrap_err());
637 }
638
639 #[test]
640 fn create_macaroon_with_first_party_caveat() {
641 let signature: MacaroonKey = [
642 14, 23, 21, 148, 48, 224, 4, 143, 81, 137, 60, 25, 201, 198, 245, 250, 249, 62, 233,
643 94, 93, 65, 247, 88, 25, 39, 170, 203, 8, 4, 167, 187,
644 ]
645 .into();
646 // NOTE: using byte string directly, not generating with HMAC
647 let key = MacaroonKey::from(b"this is a super duper secret key");
648 let mut macaroon = Macaroon::create(Some("location"), &key, "identifier").unwrap();
649 macaroon.add_first_party_caveat("predicate").unwrap();
650 assert_eq!(1, macaroon.caveats.len());
651 let predicate = match &macaroon.caveats[0] {
652 Caveat::FirstParty(fp) => fp.predicate().to_vec(),
653 _ => Vec::new(),
654 };
655 assert_eq!(b"predicate".to_vec(), predicate);
656 assert_eq!(signature, macaroon.signature);
657 assert_eq!(&macaroon.caveats[0], macaroon.first_party_caveats()[0]);
658 }
659
660 #[test]
661 fn create_macaroon_with_third_party_caveat() {
662 // NOTE: using byte string directly, not generating with HMAC
663 let key = MacaroonKey::from(b"this is a super duper secret key");
664 let mut macaroon = Macaroon::create(Some("location"), &key, "identifier").unwrap();
665 let location = "https://auth.mybank.com";
666 let cav_key = MacaroonKey::generate(b"My key");
667 let id = "My Caveat";
668 macaroon
669 .add_third_party_caveat(location, &cav_key, id)
670 .unwrap();
671 assert_eq!(1, macaroon.caveats.len());
672 let cav_id = match &macaroon.caveats[0] {
673 Caveat::ThirdParty(tp) => tp.id().to_vec(),
674 _ => Vec::new(),
675 };
676 let cav_location = match &macaroon.caveats[0] {
677 Caveat::ThirdParty(tp) => tp.location().to_string(),
678 _ => String::default(),
679 };
680 assert_eq!(location, cav_location);
681 assert_eq!(id.as_bytes().to_vec(), cav_id);
682 assert_eq!(&macaroon.caveats[0], macaroon.third_party_caveats()[0]);
683 }
684
685 #[test]
686 fn test_deserialize_bad_data() {
687 // these are all expected to fail... but not panic!
688 assert!(Macaroon::deserialize(b"").is_err());
689 assert!(Macaroon::deserialize(b"12345").is_err());
690 assert!(Macaroon::deserialize(b"\0").is_err());
691 assert!(Macaroon::deserialize(b"NDhJe_A==").is_err());
692
693 // examples that fail from fuzzing for the top-level deserialize function
694 assert!(Macaroon::deserialize(vec![10]).is_err());
695 assert!(Macaroon::deserialize(vec![70, 70, 102, 70]).is_err());
696 assert!(Macaroon::deserialize(vec![2, 2, 212, 212, 212, 212]).is_err());
697 }
698}
699
700// This will run rust code in the README as a test. Copied from:
701// https://github.com/rust-lang/cargo/issues/383#issuecomment-720873790
702#[cfg(doctest)]
703mod test_readme {
704 macro_rules! external_doc_test {
705 ($x:expr) => {
706 #[doc = $x]
707 extern "C" {}
708 };
709 }
710
711 external_doc_test!(include_str!("../README.md"));
712}