libmacaroon
Rust implementation of macaroons.
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/sha2crates, XSalsa20-Poly1305 viacrypto_secretbox. No FFI, no C dependencies, nounsafe. - Constant-time signature comparison (
subtle::ConstantTimeEq), key zeroization on drop (ZeroizeOnDrop), redactedDebug, noDerefinto key bytes —MacaroonKeyis 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.getRandomValuessurfaceMacaroonError::RngErrorinstead of aborting the module. - Interop test vectors from libmacaroons, pymacaroons, and the Go
macarooncompatproject. 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 auditclean; 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
#
API changes in libmacaroon 0.2.0
-
Macaroon::createtakesOption<&str>for location instead ofOption<String>. ReplaceSome("x".to_string())/Some("x".into())withSome("x"). -
add_first_party_caveatandadd_third_party_caveatnow returnResult<&mut Self>instead ofResult<()>, enabling?-chained builds:let mut m = create?; m.add_first_party_caveat? .add_first_party_caveat?;Existing
macaroon.add_first_party_caveat("x")?;code continues to compile (the returned&mut Selfis 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.ByteStringremoved from public API — All public methods now use&[u8],&str, orimpl AsRef<[u8]>instead ofByteString.MacaroonKey— Now usesZeroizeOnDropto clear key material on drop, constant-time equality (ConstantTimeEq), and a redactedDebugimpl. No longer implementsCopy,DerefMut, orDeref(theDeref<[u8]>impl was removed to keepDebugredaction effective — use.as_ref()to get a byte slice).to_vec()was removed (it leaked un-zeroizedVec<u8>). Use.clone()for explicit copies.MacaroonKey::generate_random()now returnsResult<MacaroonKey>and can fail withMacaroonError::RngErrorwhen the OS RNG (or the browser'scrypto.getRandomValuesin WASM) is unavailable, instead of panicking.Macaroonaccessor changes:identifier()returns&[u8](wasByteString)location()returnsOption<&str>(wasOption<String>)signature()returns&MacaroonKey(wasMacaroonKey)caveats()returns&[Caveat](wasVec<Caveat>)first_party_caveats()/third_party_caveats()returnVec<&Caveat>(wasVec<Caveat>)
Macaroon::create()— Identifier parameter now acceptsimpl AsRef<[u8]>(wasByteString). Now validates that the identifier and (optional) location are withinMAX_FIELD_SIZE_BYTES, returningMacaroonError::FieldTooLargeif not.add_first_party_caveat()— Predicate now acceptsimpl AsRef<[u8]>(wasByteString). ReturnsResult<()>and can fail withMacaroonError::TooManyCaveats(per-macaroon cap of 1000) orMacaroonError::FieldTooLarge(per-field cap of 65535 bytes).add_third_party_caveat()— ID now acceptsimpl AsRef<[u8]>(wasByteString). ReturnsResult<()>and can fail withMacaroonError::TooManyCaveats,MacaroonError::FieldTooLarge, orMacaroonError::RngError(the nonce used to encrypt the caveat key requires the OS RNG).Verifier::verify()— Discharges parameter is now&[Macaroon](wasVec<Macaroon>).Verifier::satisfy_exact()— Now acceptsimpl AsRef<[u8]>(wasByteString).Verifier::satisfy_general()— Now accepts closures (Fn(&[u8]) -> bool + Send + Sync + 'static) instead of only function pointers. TheSend + Syncbound lets&Verifierbe shared across threads (typical HTTP server pattern).VerifyFunctype alias removed.Caveatsub-types exported —FirstPartyandThirdPartyare now public. Their accessors return&[u8]and&strinstead of owned types.- New public constants —
MAX_CAVEATS(1000) andMAX_FIELD_SIZE_BYTES(65535) are now re-exported so downstream callers can reason about the same limits the crate enforces. - New error variants —
MacaroonError::FieldTooLarge { field, size }andMacaroonError::RngError(&'static str). MacaroonError::InitializationErrorremoved — No longer applicable.- V2JSON base64 interop —
i64,v64,l64,s64fields 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
xsalsa20poly1305crate tocrypto_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:
[]
= { = "0.1", = false, = ["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
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 viagetrandom/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