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.
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.
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.
[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.
| 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.
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>(())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.
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.
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.
MIT