libmacaroon 0.2.1

Macaroons (bearer credentials with contextual caveats) in pure Rust, with first-party and third-party caveats, WASM support, and cross-language interop.
Documentation
//! Property-based tests for the macaroon serialization formats and the
//! verification flow.
//!
//! The goal is to catch any macaroon shape the hand-written tests miss —
//! weird byte sequences in identifiers, binary predicates, the full range
//! of permitted field sizes, interactions between first- and third-party
//! caveats, and so on. Everything that can serialize should round-trip
//! identically through all three formats, and verification should accept
//! the correct key and reject a tampered one.
//!
//! Field size bounds are kept well under `MAX_FIELD_SIZE_BYTES` and caveat
//! counts under `MAX_CAVEATS` so this runs in a reasonable time under the
//! default proptest configuration.
//!
//! Skipped on wasm32 — proptest's transitive deps don't support that
//! target (see Cargo.toml for the dev-dependency scope).

#![cfg(not(target_arch = "wasm32"))]

use libmacaroon::{Format, Macaroon, MacaroonKey, Verifier};
use proptest::collection::vec;
use proptest::prelude::*;

/// Locations must be valid UTF-8 strings per the public API (`&str`).
/// Keep them small and printable to keep the corpus interesting without
/// exercising unicode normalization edge cases that aren't the crate's
/// concern.
fn location_strategy() -> impl Strategy<Value = String> {
    "[ -~]{0,64}".prop_map(|s| s)
}

/// Identifiers, predicates, and caveat ids are byte strings. Exercise both
/// printable ASCII and binary bytes (with NULs, control characters, high
/// bits).
fn bytes_strategy(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
    vec(any::<u8>(), 1..=max_len)
}

#[derive(Debug, Clone)]
enum CaveatSpec {
    First(Vec<u8>),
    Third {
        location: String,
        key_seed: Vec<u8>,
        id: Vec<u8>,
    },
}

fn caveat_spec_strategy() -> impl Strategy<Value = CaveatSpec> {
    prop_oneof![
        4 => bytes_strategy(128).prop_map(CaveatSpec::First),
        1 => (location_strategy(), bytes_strategy(32), bytes_strategy(64)).prop_map(
            |(location, key_seed, id)| CaveatSpec::Third { location, key_seed, id },
        ),
    ]
}

#[derive(Debug, Clone)]
struct MacaroonSpec {
    location: Option<String>,
    key_seed: Vec<u8>,
    identifier: Vec<u8>,
    caveats: Vec<CaveatSpec>,
}

fn macaroon_spec_strategy() -> impl Strategy<Value = MacaroonSpec> {
    (
        proptest::option::of(location_strategy()),
        bytes_strategy(32),
        bytes_strategy(64),
        vec(caveat_spec_strategy(), 0..=8),
    )
        .prop_map(|(location, key_seed, identifier, caveats)| MacaroonSpec {
            location,
            key_seed,
            identifier,
            caveats,
        })
}

fn build(spec: &MacaroonSpec) -> (MacaroonKey, Macaroon) {
    let key = MacaroonKey::generate(&spec.key_seed);
    let mut mac = Macaroon::create(spec.location.as_deref(), &key, spec.identifier.as_slice())
        .expect("valid inputs");
    for c in &spec.caveats {
        match c {
            CaveatSpec::First(predicate) => {
                mac.add_first_party_caveat(predicate.as_slice())
                    .expect("valid first-party caveat");
            }
            CaveatSpec::Third {
                location,
                key_seed,
                id,
            } => {
                let cav_key = MacaroonKey::generate(key_seed);
                mac.add_third_party_caveat(location, &cav_key, id.as_slice())
                    .expect("valid third-party caveat");
            }
        }
    }
    (key, mac)
}

proptest! {
    // Keep test times reasonable — 64 cases per property gives good coverage
    // without making `cargo test` noticeably slower.
    #![proptest_config(ProptestConfig::with_cases(64))]

    /// Every macaroon this crate builds serializes to every format and
    /// round-trips back to the same `Macaroon` value.
    #[test]
    fn roundtrip_all_formats(spec in macaroon_spec_strategy()) {
        let (_key, mac) = build(&spec);

        #[cfg(feature = "v2json")]
        let formats = [Format::V1, Format::V2, Format::V2JSON];
        #[cfg(not(feature = "v2json"))]
        let formats = [Format::V1, Format::V2];
        for format in formats {
            let encoded = mac.serialize(format).expect("serialize");
            let decoded = Macaroon::deserialize(&encoded).expect("deserialize");
            prop_assert_eq!(&decoded, &mac, "round-trip mismatch in one of the formats");
        }
    }

    /// First-party caveats produced by `add_first_party_caveat` are
    /// verifiable when the verifier is configured with the exact-match
    /// predicate for each one. Third-party caveats are excluded from this
    /// property — discharges would need to be generated and bound, which
    /// is tested separately below.
    #[test]
    fn verify_succeeds_when_all_predicates_exact_matched(spec in macaroon_spec_strategy()) {
        // Skip specs that include third-party caveats: those need discharges
        // that we don't generate in this particular property.
        prop_assume!(spec.caveats.iter().all(|c| matches!(c, CaveatSpec::First(_))));

        let (key, mac) = build(&spec);
        let mut verifier = Verifier::default();
        for c in &spec.caveats {
            if let CaveatSpec::First(pred) = c {
                verifier.satisfy_exact(pred.as_slice());
            }
        }
        prop_assert!(verifier.verify(&mac, &key, &[]).is_ok());
    }

    /// Verification rejects a macaroon verified with the wrong key.
    #[test]
    fn verify_fails_with_wrong_key(spec in macaroon_spec_strategy(), wrong_seed in bytes_strategy(32)) {
        // Skip third-party caveats for the same reason as above.
        prop_assume!(spec.caveats.iter().all(|c| matches!(c, CaveatSpec::First(_))));
        prop_assume!(wrong_seed != spec.key_seed);

        let (_key, mac) = build(&spec);
        let wrong_key = MacaroonKey::generate(&wrong_seed);
        let mut verifier = Verifier::default();
        for c in &spec.caveats {
            if let CaveatSpec::First(pred) = c {
                verifier.satisfy_exact(pred.as_slice());
            }
        }
        prop_assert!(verifier.verify(&mac, &wrong_key, &[]).is_err());
    }
}

// Feeding arbitrary bytes into `Macaroon::deserialize` must never panic —
// either outcome (parse or error) is fine. 512 cases because the surface
// being fuzzed is pure deserialization and each iteration is cheap.
proptest! {
    #![proptest_config(ProptestConfig::with_cases(512))]

    #[test]
    fn deserialize_does_not_panic(input in vec(any::<u8>(), 0..=256)) {
        let _ = Macaroon::deserialize(input.as_slice());
        let _ = Macaroon::deserialize_binary(input.as_slice());
    }
}