libmacaroon 0.2.0

Macaroons (bearer credentials with contextual caveats) in pure Rust, with first-party and third-party caveats, WASM support, and cross-language interop.
Documentation

libmacaroon

Rust implementation of macaroons.

Crates.io Build Status Docs.rs

This is a maintained fork of the macaroon crate (macaroon-rs/macaroon, last released Oct 2022). The fork carries forward the paper-faithful macaroon semantics and wire-compatibility with libmacaroons / pymacaroons, and adds a pre-release hardening pass: constant-time signature comparison, key zeroization on drop, DoS caps, WASM support via pure-Rust crypto_secretbox, interop and property tests, and a fuzz harness. See ChangeLog.md for the full list.

Status

Pre-1.0; no external formal security audit. The API is expected to break between 0.x minor versions until we've shaken it out against real-world use.

What has been done:

  • Pure-Rust crypto stack: HMAC-SHA256 via the RustCrypto hmac/sha2 crates, XSalsa20-Poly1305 via crypto_secretbox. No FFI, no C dependencies, no unsafe.
  • Constant-time signature comparison (subtle::ConstantTimeEq), key zeroization on drop (ZeroizeOnDrop), redacted Debug, no Deref into key bytes — MacaroonKey is hard to accidentally leak.
  • DoS caps on every input dimension: caveat count (MAX_CAVEATS = 1000), field size (MAX_FIELD_SIZE_BYTES = 65535), verification recursion depth (32). Applied symmetrically on construction and deserialization.
  • Fallible RNG path: WASM environments without crypto.getRandomValues surface MacaroonError::RngError instead of aborting the module.
  • Interop test vectors from libmacaroons, pymacaroons, and the Go macarooncompat project. First- and third-party signatures match byte-for-byte where the compared implementations permit deterministic nonces.
  • Property tests over the full serialization matrix (V1 / V2 / V2JSON round-trip, verification, wrong-key rejection) plus fuzz harness under fuzz/ for both deserialization entry points.
  • cargo audit clean; CI runs fmt, clippy -D warnings, test, MSRV, WASM target, and audit on every PR.

What has not been done:

  • External formal audit. If you're considering this crate for a security boundary, please budget for one.

What are Macaroons?

Macaroons are bearer tokens (similar to cookies) which encode within them criteria within which the authorization is allowed to take place (referred to as "caveats"). For instance, authorization could be restricted to a particular user, account, time of day, really anything. These criteria can be either evaluated locally (a "first-party caveat"), or using special macaroons ("discharge macaroons") generated by a third party (a "third-party caveat").

A first-party caveat consists simply of a predicate which, when evaluated as true, authorizes the caveat. The predicate is a string which is either evaluated using strict string comparison (satisfy_exact), or interpreted using a provided function (satisfy_general).

A third-party caveat consists of a location string, an identifier, and a specially-generated signing key to authenticate the generated discharge macaroons. The key and identifier is passed to the third-party who generates the discharge macaroons. The receiver then binds each discharge macaroon to the original macaroon.

During verification of a third-party caveat, a discharge macaroon is found from those received whose identifier matches that of the caveat. The binding signature is verified, and the discharge macaroon's caveats are verified using the same process as the original macaroon.

The macaroon is considered authorized only if all its caveats are authorized by the above process.

Functionality Implemented

  • Creating macaroons, and adding first- and third-party caveats
  • Serialization - versions 1, 2 & 2J are supported
  • Validation (mostly for validating deserialized macaroons)
  • Creation of discharge macaroons
  • Verification of both first- and third-party caveats (the latter using discharge macaroons)

Examples

# fn main() -> Result<(), Box<dyn std::error::Error>> {
use libmacaroon::{Macaroon, Verifier, MacaroonKey};

// Create a key. `generate` derives via HMAC from any byte string; for a
// fresh random key use `MacaroonKey::generate_random()?`.
let key = MacaroonKey::generate(b"key");

// Create a macaroon. Location is optional; identifier accepts any
// `AsRef<[u8]>`.
let mut macaroon = Macaroon::create(Some("location"), &key, "id")?;

// Add a first-party caveat — an opaque predicate that the verifier will
// match. Predicates accept any `AsRef<[u8]>`. Add as many as you want.
macaroon.add_first_party_caveat("account = 12345678")?;

// Build a verifier with the predicates we're willing to accept, then verify.
// Returns `Ok(())` when every caveat is satisfied and the signature chain
// matches. Discharges are passed as a borrowed slice.
let mut verifier = Verifier::default();
verifier.satisfy_exact("account = 12345678");
verifier.verify(&macaroon, &key, &[])?;

// Add a third-party caveat. This binds the macaroon to an external
// authorization that will be represented by a discharge macaroon issued
// under `other_key`.
let other_key = MacaroonKey::generate(b"different key");
macaroon.add_third_party_caveat("https://auth.mybank", &other_key, "caveat id")?;

// The third party issues the discharge using the same caveat id and the
// caveat key. They can also attach further caveats to the discharge.
let mut discharge = Macaroon::create(
    Some("http://auth.mybank/"),
    &other_key,
    "caveat id",
)?;
discharge.add_first_party_caveat("account = 12345678")?;

// Bind the discharge to the original macaroon so it cannot be reused
// against a different authorizing macaroon.
macaroon.bind(&mut discharge);

// Same verifier, now with the discharge supplied.
verifier.verify(&macaroon, &key, &[discharge])?;
# Ok(())
# }

API changes in libmacaroon 0.2.0

  • Macaroon::create takes Option<&str> for location instead of Option<String>. Replace Some("x".to_string()) / Some("x".into()) with Some("x").

  • add_first_party_caveat and add_third_party_caveat now return Result<&mut Self> instead of Result<()>, enabling ?-chained builds:

    let mut m = Macaroon::create(Some("loc"), &key, "id")?;
    m.add_first_party_caveat("account = 12345")?
        .add_first_party_caveat("user = alice")?;
    

    Existing macaroon.add_first_party_caveat("x")?; code continues to compile (the returned &mut Self is silently discarded).

API changes from macaroon 0.3.0

The libmacaroon 0.1.0 API carries forward the macaroon-rs 0.3.0 surface with these changes. Migrating from macaroon is mostly a crate-name swap plus a handful of signature tweaks:

  • initialize() removed — The library no longer requires explicit initialization. The cryptographic backend is now pure Rust and thread-safe by default.
  • ByteString removed from public API — All public methods now use &[u8], &str, or impl AsRef<[u8]> instead of ByteString.
  • MacaroonKey — Now uses ZeroizeOnDrop to clear key material on drop, constant-time equality (ConstantTimeEq), and a redacted Debug impl. No longer implements Copy, DerefMut, or Deref (the Deref<[u8]> impl was removed to keep Debug redaction effective — use .as_ref() to get a byte slice). to_vec() was removed (it leaked un-zeroized Vec<u8>). Use .clone() for explicit copies.
  • MacaroonKey::generate_random() now returns Result<MacaroonKey> and can fail with MacaroonError::RngError when the OS RNG (or the browser's crypto.getRandomValues in WASM) is unavailable, instead of panicking.
  • Macaroon accessor changes:
    • identifier() returns &[u8] (was ByteString)
    • location() returns Option<&str> (was Option<String>)
    • signature() returns &MacaroonKey (was MacaroonKey)
    • caveats() returns &[Caveat] (was Vec<Caveat>)
    • first_party_caveats() / third_party_caveats() return Vec<&Caveat> (was Vec<Caveat>)
  • Macaroon::create() — Identifier parameter now accepts impl AsRef<[u8]> (was ByteString). Now validates that the identifier and (optional) location are within MAX_FIELD_SIZE_BYTES, returning MacaroonError::FieldTooLarge if not.
  • add_first_party_caveat() — Predicate now accepts impl AsRef<[u8]> (was ByteString). Returns Result<()> and can fail with MacaroonError::TooManyCaveats (per-macaroon cap of 1000) or MacaroonError::FieldTooLarge (per-field cap of 65535 bytes).
  • add_third_party_caveat() — ID now accepts impl AsRef<[u8]> (was ByteString). Returns Result<()> and can fail with MacaroonError::TooManyCaveats, MacaroonError::FieldTooLarge, or MacaroonError::RngError (the nonce used to encrypt the caveat key requires the OS RNG).
  • Verifier::verify() — Discharges parameter is now &[Macaroon] (was Vec<Macaroon>).
  • Verifier::satisfy_exact() — Now accepts impl AsRef<[u8]> (was ByteString).
  • Verifier::satisfy_general() — Now accepts closures (Fn(&[u8]) -> bool + Send + Sync + 'static) instead of only function pointers. The Send + Sync bound lets &Verifier be shared across threads (typical HTTP server pattern). VerifyFunc type alias removed.
  • Caveat sub-types exportedFirstParty and ThirdParty are now public. Their accessors return &[u8] and &str instead of owned types.
  • New public constantsMAX_CAVEATS (1000) and MAX_FIELD_SIZE_BYTES (65535) are now re-exported so downstream callers can reason about the same limits the crate enforces.
  • New error variantsMacaroonError::FieldTooLarge { field, size } and MacaroonError::RngError(&'static str).
  • MacaroonError::InitializationError removed — No longer applicable.
  • V2JSON base64 interopi64, v64, l64, s64 fields are now emitted as URL-safe base64 without padding (matching libmacaroons / pymacaroons wire format) and the decoder accepts both URL-safe and standard alphabets, padded or unpadded.
  • Crypto backend — migrated from the unmaintained xsalsa20poly1305 crate to crypto_secretbox (the RustCrypto-recommended successor). Same XSalsa20-Poly1305 algorithm and wire format, so first-party and third-party signatures are unchanged.

Backwards compatibility

Pre-1.0; breaking changes may land in any 0.x.y minor bump, with changelog entries and migration notes per release. Once the API has shaken out against real-world use, a 1.0 will commit to semver — no breaking changes within the 1.x line.

WebAssembly (WASM) Support

This crate now supports WebAssembly! Use the wasm feature to enable WASM compatibility:

[dependencies]
libmacaroon = { version = "0.1", default-features = false, features = ["wasm"] }

The wasm feature enables browser-compatible random number generation. The cryptographic backend is always pure Rust (RustCrypto), so WASM compilation works out of the box — the feature flag only configures getrandom for JavaScript environments.

Building for WASM

# Build for WASM target
cargo build --target wasm32-unknown-unknown --features wasm

Cryptographic Backend

This crate uses pure Rust implementations from the RustCrypto ecosystem (HMAC-SHA256, XSalsa20-Poly1305). No C dependencies are required, and the same backend works for both native and WASM targets.

Feature Flags

  • wasm - Enable WASM-compatible random number generation via getrandom/js

Minimum Supported Rust Version

This crate targets Rust Language 2021 Edition and builds on stable Rust 1.71 (Jul 2023) and later. The library build (cargo build) is the contract; cargo test pulls in dev-dep transitives that require newer editions and isn't part of the MSRV guarantee.

MSRV bumps are not considered breaking for 0.x, but we only bump when needed, and only to land bug fixes or to keep transitive deps on supported versions.

Requires std.

Contributing

We :heart: any contributions. Any fixes to make things simpler or more idiomatic are also more than welcome. Please open a pull request if you have something you want to contribute. As the project matures, we will add a more detailed contributors guide