You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Residual timing side-channel surfaced by the #35 red-team and intentionally not closed by the #35
fix (the constant-time rejection floor). Filing it as a first-class issue so it has its own lifecycle,
per PR #43 review — the mitigation guidance currently lives only in the ReceiveRejectionFloor XML doc
and the CHANGELOG, where an operator searching issues won't find it.
Category: timing side-channel / information disclosure (held-recipient-key enumeration) Severity: High (deployment-dependent; fully mitigable with auth / rate-limiting in front of the endpoint)
#35 added a constant-time floor on the HTTP 400-rejection path
(DidCommReceiveOptions.ReceiveRejectionFloor, default 5 ms). A fixed floor can only mask failures that
finish under it. It closes the cheap, universal probe — garbage ciphertext to a guessed kid, where
unheld fast-fails (~180 µs) and held + AEAD-fail (~360 µs) both pad to the floor.
It does not close the held-only path that decrypts and then performs network DID resolution
(authcrypt sender authorization / FR-CONSIST-06 / from_prior), which can run longer than any sane
fixed floor. Network latency is unbounded, so no fixed floor value closes it without being absurd.
Exploit
An unauthenticated attacker who knows candidate recipient public keys (they appear in DID documents):
Builds an authcrypt envelope addressed to a guessed recipient kid, signed as an
attacker-controlled did:webvh (or any method whose resolution the attacker can stall).
Submits it to the receive endpoint and times the response.
Unheld recipient kid: decryption fast-fails before any resolver call → response ≈ floor.
Held recipient kid: decryption succeeds → the unpack pipeline resolves the attacker-controlled
sender DID, which the attacker makes deliberately slow (up to the resolver timeout) → response
visibly ≫ floor, then a uniform 400 (consistency/auth fail).
response ≫ floor ⇒ held. The attacker enumerates which recipient private keys the agent holds, and
amplifies the signal arbitrarily by controlling the resolver delay.
This is reachable today because the held path performs the sender-DID resolution synchronously before
the response is produced.
Why a fixed floor can't fix it
To mask a held path that takes T_resolve, the floor would have to be ≥ T_resolve for every request
(including unheld ones) — but T_resolve is attacker-controlled and unbounded. That is not a viable
default.
Mitigations / candidate fixes (for discussion)
Operational (available now, recommended): put authentication and/or a rate-limiter in front of the
receive endpoint so unauthenticated enumeration probing is not possible / is rate-bounded. This is the
deployment tradeoff Timing side-channel on HTTP receive enables recipient-kid enumeration (residual of #20) #35 calls out and what the ReceiveRejectionFloor doc currently points to.
Bound the receive-path resolver: apply a small, fixed per-request resolution timeout on the receive path and fold it into the floor budget, so the held path cannot be stalled past a known
ceiling. Tradeoff: breaks legitimately-slow resolvers; needs care.
Decouple resolution from the response: acknowledge/reject before the network-dependent consistency
checks, doing resolution-dependent validation out of band. Larger architectural change; changes
semantics (a message that later fails consistency would already have been 202'd).
Warm/constant-time resolver cache with a constant-time miss. Partial; doesn't help a cold cache.
Summary
Residual timing side-channel surfaced by the #35 red-team and intentionally not closed by the #35
fix (the constant-time rejection floor). Filing it as a first-class issue so it has its own lifecycle,
per PR #43 review — the mitigation guidance currently lives only in the
ReceiveRejectionFloorXML docand the CHANGELOG, where an operator searching issues won't find it.
Category: timing side-channel / information disclosure (held-recipient-key enumeration)
Severity: High (deployment-dependent; fully mitigable with auth / rate-limiting in front of the endpoint)
What #35 closed, and what it didn't
#35 added a constant-time floor on the HTTP
400-rejection path(
DidCommReceiveOptions.ReceiveRejectionFloor, default 5 ms). A fixed floor can only mask failures thatfinish under it. It closes the cheap, universal probe — garbage ciphertext to a guessed
kid, whereunheld fast-fails (~180 µs) and held + AEAD-fail (~360 µs) both pad to the floor.
It does not close the held-only path that decrypts and then performs network DID resolution
(authcrypt sender authorization / FR-CONSIST-06 /
from_prior), which can run longer than any sanefixed floor. Network latency is unbounded, so no fixed floor value closes it without being absurd.
Exploit
An unauthenticated attacker who knows candidate recipient public keys (they appear in DID documents):
kid, signed as anattacker-controlled
did:webvh(or any method whose resolution the attacker can stall).kid: decryption fast-fails before any resolver call → response ≈ floor.kid: decryption succeeds → the unpack pipeline resolves the attacker-controlledsender DID, which the attacker makes deliberately slow (up to the resolver timeout) → response
visibly ≫ floor, then a uniform
400(consistency/auth fail).response ≫ floor ⇒ held. The attacker enumerates which recipient private keys the agent holds, andamplifies the signal arbitrarily by controlling the resolver delay.
This is reachable today because the held path performs the sender-DID resolution synchronously before
the response is produced.
Why a fixed floor can't fix it
To mask a held path that takes
T_resolve, the floor would have to be ≥T_resolvefor every request(including unheld ones) — but
T_resolveis attacker-controlled and unbounded. That is not a viabledefault.
Mitigations / candidate fixes (for discussion)
receive endpoint so unauthenticated enumeration probing is not possible / is rate-bounded. This is the
deployment tradeoff Timing side-channel on HTTP receive enables recipient-kid enumeration (residual of #20) #35 calls out and what the
ReceiveRejectionFloordoc currently points to.receive path and fold it into the floor budget, so the held path cannot be stalled past a known
ceiling. Tradeoff: breaks legitimately-slow resolvers; needs care.
checks, doing resolution-dependent validation out of band. Larger architectural change; changes
semantics (a message that later fails consistency would already have been
202'd).References
JweParserconstant-time recipient selection — the local-crypto gap; distinct fromthis network-resolution residual)
ReceiveRejectionFloorand in the CHANGELOG)Filed per PR #43 review (the "Finding 2" residual).