Skip to content

Repository files navigation

mole-exp

An experimental Rust implementation of MoLE — Moderation of unLinkable Endorsements — covering the parts that the IETF drafts actually specify, plus an IHAT profile that cross-verifies against the reference implementation the MoLE authors publish.

Not published to crates.io, and publish = false in the manifest. The -exp suffix is load-bearing.

CI

MoLE lets a party performing access control (a Moderator) bootstrap trust in a client from a third party (an Anchor) that already has a relationship with that client, then adjust that trust over time — dynamic rate limiting, for example — without being able to identify the client or link its sessions. It is the three-party successor to Privacy Pass (RFC 9576/9577/9578), developed by authors from Google, Mozilla, and Cloudflare.

⚠️ Read this before you use it

Nothing here is deployable. The default build contains no cryptography, and the one cryptographic feature implements a construction that has not been audited.

The protocols draft specifies IHAT's message structures and defers every cryptographic operation to a companion document:

The cryptographic operations, and the contents of every message body, are defined in [CRYPTO]. Until that document is complete, bodies in this section are opaque byte strings produced and consumed by the functions named below.

§4.1, draft-jms-mole-protocols-00

[CRYPTO] has since been written, but it is not on the IETF Datatracker and is not an interoperable specification: it names no group directly, gives no domain separation tag, and publishes no test vectors. The MoLE org does publish a reference implementation, ihat-rs, by one of the draft authors, and that settles all of it.

So the default build is everything the drafts do pin down — the wire format, the HTTP carriage, challenge binding, the registries and greasing rules, and the Moderator's replay state machine — with the cryptography expressed as the trait boundary in src/crypto.rs. Two dependencies, #![no_std], no invented schemes.

The off-by-default exp-ihat feature implements IHAT over P-256, aligned byte-for-byte with ihat-rs and cross-verified against it in both directions. Blindness, issuer hiding, public verifiability and unlinkability are real. It is the more dangerous half of this crate precisely because it works: ihat-rs states it has not been audited, and interoperability is agreement, not correctness.

One thing cannot be reconciled: ihat-rs has no challenge_digest anywhere, so protocols §4.1.3's two MUSTs about binding it are satisfied by no implementation. The profile exposes ChallengeBinding with a mode for each side of that contradiction. See EXP-IHAT-PROFILE.md.

The drafts also note that "IHAT is a placeholder name", that the two grant exchanges still need a correlation mechanism, and that the protocols "are not final. Some may be removed, others added." Registry values are candidates, not assignments.

What is implemented

Area Status
<V> variable-length integers, minimum-size encoding enforced
optional<T> presence octet, invalid octets rejected
All endorsement + credential message structures
All transport structures and their example refinements
challenge_digest = SHA-256(challenge) over octets, never ASCII
Mole HTTP auth scheme; multi-challenge parsing; Mole-Credential
Type registries, reserved 0x0000 rejection, testing ranges
Greasing: value selection, and grease-as-unknown on the Moderator
Epoch nullifier store, single-use enforcement, token nonce replay
200/401/403 challenge semantics
IHAT cryptography (endorsement 0x0002) exp-ihat, over P-256, cross-verified against ihat-rs
Privacy Pass Reverse Flow (credential 0x0002) ✅ carried opaquely per §5.2.1; reverse_flow::Token parses what a Moderator must read; run end to end against privacypass

MoLE specifies several endorsement and credential protocols to demonstrate agility; a deployment picks one of each. This crate implements IHAT and Privacy Pass Reverse Flow. The wire structures, registries and dispatch for the others are present and tested — only their cryptography is out of scope, and Longfellow's and ACT's underlying drafts are not in a state to build against.

SPEC-COVERAGE.md maps every draft section to what this crate does with it, including the drafts' own open questions.

Usage

Not on crates.io. Take it by path or git if you want to experiment:

[dependencies]
mole-exp = { git = "https://github.com/OR13/mole" }

The library is still mole to import. Two dependencies in the default build, sha2 and base64; exp-ihat adds p256. #![no_std] with alloc throughout, including the profile; forbid(unsafe_code).

use mole::binding::ChallengeDigest;
use mole::codec::Message;
use mole::credential::budget;
use mole::http::{parse_www_authenticate, MoleChallenge};
use mole::registry::CredentialType;
use mole::transport::CredentialChallenge;

// A Moderator offers Budget Privacy Pass, charging one unit.
let inner = budget::Challenge { amount: 1 }.to_wire()?;
let challenge = CredentialChallenge {
    credential_type: CredentialType::BUDGET_PRIVACY_PASS,
    challenge: inner,
}.to_wire()?;

let header = MoleChallenge::new(&challenge, Some("moderator")).to_header_value();
// WWW-Authenticate: Mole challenge="AAMIAAAAAAAAAAE", realm="moderator"

// A Client decodes, then binds to the resulting octets.
let parsed = parse_www_authenticate(&header);
let digest = ChallengeDigest::new(&parsed[0].challenge_octets()?);
# Ok::<(), mole::Error>(())

Details worth knowing

A few places where the drafts are easy to misread, and which this crate tests explicitly:

Varints are not RFC 9000 varints. The encoding is the QUIC one, but draft-jms-mole-http-transport-00 §3.2 "requires a minimum-size encoding." 0 encoded in two bytes is legal QUIC and malformed MoLE. Reader::varint rejects it.

The digest is over octets, never the ASCII. §3.3: "When a challenge arrives base64url encoded in an HTTP header, the Client first decodes it, then hashes the resulting octets. The digest is never computed over the ASCII form." The only base64-aware constructor, ChallengeDigest::from_base64url, decodes first; there is deliberately no constructor that hashes a &str.

A Moderator must not enumerate greased values. §3.2: "a Moderator that enumerates greased values defeats their purpose and will still receive unknown types it did not enumerate." Moderator::dispatch is a membership test over the types it supports, so it is structurally unable to treat grease specially — a test asserts grease and an arbitrary unassigned value take identical paths.

An absent update is a signal, not a missing field. §6.3.3: the Moderator "MUST send Mole-Credential with the update parameter after a presentation… It marks the update as absent to intentionally consume the Credential." OptionalCredentialUpdate::absent() encodes to one zero octet, distinct from omitting the header.

Reverse-flow anti-replay is not challenge binding. §5.2.2: the token's digest is the Moderator's constant value, so "anti-replay… comes from the token being single-use. The Moderator MUST reject a token whose nonce it has already seen." That is moderator::NonceStore, kept separate from the epoch NullifierStore.

Tests

cargo test --all-features                        # 111 tests
cargo test --manifest-path interop/Cargo.toml     # 7 interop + end-to-end tests
cargo clippy --all-features --all-targets
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps
cargo build --no-default-features                # no_std

cargo doc is part of the check, not an afterthought: the docs quote the drafts constantly, and a bracketed citation like [ACT] is a broken intra-doc link that only rustdoc will catch.

tests/wire_vectors.rs holds byte-exact hex vectors — the drafts publish none, so these are this crate's, offered for cross-implementation checking. tests/appendix_a_flow.rs drives the complete exchange from Appendix A of the protocols draft on real cryptography — the profile's actual grant messages carried inside the actual EndorsementRequest/EndorsementResponse envelopes, redeemed through a real OR proof against a real accepted set. tests/exp_ihat_profile.rs exercises the profile in isolation.

And interop/tests/appendix_a_end_to_end.rs runs the same exchange with no mocks at all — real IHAT endorsements plus real RFC 9578 Privacy Pass credentials via privacypass, including presentation and reissue. Every piece of cryptography in the reachable protocol surface is now real.

interop/ is a detached package for tests that need dependencies the library refuses to take. It differential-tests the profile against ihat-rs in both directions — our presentations verify under theirs, theirs verify under ours, and re-encoding is byte-identical. Since neither the drafts nor ihat-rs publish test vectors, that cross-check stands in for them, and it is stronger: it exercises both roles rather than fixed byte strings. It also holds the no-mocks end-to-end run, which needs privacypass — std-only, with an async issuer API. Keeping both out there is what lets the library stay no_std with two dependencies while still being demonstrably complete.

Drafts tracked

All revision -00, published 6 July 2026, by Samuel Schlesinger (Google), Dennis Jackson (Mozilla), and Thibault Meunier (Cloudflare):

For the exp-ihat profile the normative artifact is ihat-rs (pinned by revision in interop/Cargo.toml), not the prose. Where the two disagree — the issuance transcript, the Pedersen generator, point compression, the OR proof, the existence of m — the profile follows the code, because the code is executable and the draft is not.

Also relevant, draft-authors-mole-crypto.md — which has no Datatracker entry and no revision number. The profile does not follow it directly; see below.

exp-ihat also relies on RFC 9380 for hash-to-curve, and RFC 9578 governs the Privacy Pass tokens the credential layer carries.

Upstream source: Moderation-of-unLinkable-Endorsements/internet-drafts.

This is an independent implementation. It is not affiliated with or endorsed by the draft authors, their employers, or the IETF.

License

MIT

About

Rust implementation of MoLE (Moderation of unLinkable Endorsements) — wire format, HTTP transport, and Moderator state machine per draft-jms-mole-*-00. No cryptography: the drafts defer it to an unpublished [CRYPTO] document.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages