Skip to content

Timing residual: held-path network DID resolution exceeds the #35 rejection floor (response ≫ floor ⇒ held) #44

Description

@moisesja

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 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)

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 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):

  1. 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).
  2. 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).
  3. 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.

References

Filed per PR #43 review (the "Finding 2" residual).

Metadata

Metadata

Assignees

Labels

dosDenial of service / resource exhaustionsecuritySecurity-relevant issueseverity:highHigh severityvulnerabilityExploitable security vulnerability

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions