Skip to content

Upstream: section 5.2.2's constant-digest requirement is unsatisfiable #16

Description

@OR13

Found by implementing the credential half of Appendix A against a real RFC 9578 implementation (privacypass). It does not surface any other way.

The requirement

draft-jms-mole-protocols-00 §5.2.2, on the Token carried in PresentationAndUpdate:

The token field carries a Token as defined in {{PRIVACYPASS-AUTH}}. Its challenge_digest field is fixed when the token is issued, one exchange before it is presented, and MUST equal the Moderator's constant challenge_digest ({{challenge-binding}}).

That cross-reference is §3.3 — MoLE's challenge binding, SHA-256(challenge octets). And §5.2.2 opens with:

Challenge is empty for this type. The challenge octets, and therefore challenge_digest, are constant for a given Moderator configuration.

So read literally: the token's challenge_digest field MUST equal SHA-256("").

Why that cannot be done

The token's challenge_digest is not a field an implementation gets to choose. It is computed by the RFC 9578 issuance protocol as SHA-256(TokenChallenge), where TokenChallenge is the RFC 9577 structure:

struct {
    uint16_t token_type;
    opaque issuer_name<1..2^16-1>;
    opaque redemption_context<0..32>;
    opaque origin_info<0..2^16-1>;
} TokenChallenge;

token_type alone makes the serialization at least two bytes, and issuer_name is 1.. — non-empty by construction. So SHA-256(TokenChallenge) can never equal SHA-256(""). Confirmed against privacypass, where TokenChallenge::digest() is literally Sha256::digest(self.serialize()?).

No configuration closes this. It would need a SHA-256 preimage.

Two different fields share one name

This is the root of it. There are two distinct challenge_digest values in play and §5.2.2 conflates them:

Defined by Computed over
MoLE's challenge_digest protocols §3.3 MoLE's Challenge structure
the token's challenge_digest RFC 9577/9578 an RFC 9577 TokenChallenge

For endorsement types these coincide in purpose, so the shared name is harmless. For credential type 0x0002 they are unrelated quantities and the MUST tries to equate them.

The workable reading

The intent is clear from the very next sentence:

This binds the token to the Moderator, not to the exchange that presents it.

That property only needs the digest to be constant per Moderator — which it is, automatically, if the Moderator issues under one fixed TokenChallenge. So the requirement should be something like:

Its challenge_digest field MUST equal the digest of the TokenChallenge named in the Moderator's configuration, and MUST be the same for every token that Moderator issues.

and the {{challenge-binding}} cross-reference should be dropped, because §3.3 is not what governs this field.

What this crate does

interop/tests/appendix_a_end_to_end.rs takes the workable reading: the Moderator holds one TokenChallenge and every token it issues carries that digest, which the Moderator then checks on presentation. That works end to end against privacypass.

Note this does not weaken anything. §5.2.2 is explicit that anti-replay does not come from challenge binding here — it comes from the nonce being single-use, which moderator::NonceStore enforces and which the test exercises in both directions.

Also added credential::reverse_flow::Token, a parser for the RFC 9578 Token structure, because a Moderator cannot enforce either of §5.2.2's two requirements — constant digest, unseen nonce — without reading those fields. It is structure only, no cryptography, and keeps the crate no_std with two dependencies.

Action

Report upstream together with #7, #8, #9 and #11. Of the set this one is the cheapest to fix — a cross-reference removal and a sentence rewritten — and the most clearly a defect rather than an open design question.

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity-relevantupstreamNeeds resolution in the IETF drafts, not here

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions