Skip to content

SEC-2275: keybox-bypass defense -- per-(fp, aud) rate limit + blocklist#163

Open
NnnOooPppEee wants to merge 2 commits into
SEC-2276/integrity-confidence-and-telemetryfrom
SEC-2275/keybox-defense-rate-limit-blocklist
Open

SEC-2275: keybox-bypass defense -- per-(fp, aud) rate limit + blocklist#163
NnnOooPppEee wants to merge 2 commits into
SEC-2276/integrity-confidence-and-telemetryfrom
SEC-2275/keybox-defense-rate-limit-blocklist

Conversation

@NnnOooPppEee
Copy link
Copy Markdown
Contributor

Summary

Stateful enforcement layer on top of the stateless integrity signals from #162. Closes the keybox-bypass surface where a single leaked batch certificate can be reused across many devices: any abuse spike on a (SHA256(intermediate_cert_DER), aud) pair gets capped, and known-bad fingerprints can be blocklisted out of band.

Stacks on top of #162 (which stacks on #161).

Changes

  • New android::keybox_defense module:
    • KeyboxDefense::fingerprint(der) -- lowercase hex SHA-256 of the intermediate (batch) cert.
    • KeyboxDefense::evaluate(redis, fp, aud) -- checks keybox:block:{fp} for an explicit blocklist entry, then increments a sliding-window counter at keybox:count:{fp}:{aud} with EXPIRE = window_secs. Maps the count to RiskLevel::{Low, Medium, High, Blocked}.
    • KeyboxDefense::blocklist_add / blocklist_remove for ops use.
  • AndroidAttestationOutput.batch_cert_fingerprint: SHA-256 hex of the intermediate cert, computed once per attestation from the validated chain (uses AndroidCertChain::intermediate_cert_der() introduced in SEC-2314: enforce registered roots as the chain trust anchor #161).
  • routes/a.rs: after the nonce is consumed and we know the audience, call evaluate(fp, &aud) and reject when verdict.should_reject().

Operational notes

  • Shadow mode by default. KeyboxDefenseConfig::from_env() reads KEYBOX_DEFENSE_ENFORCE; only when set to `1` does verdict.should_reject() ever return `true`. Until we flip the switch, the gateway computes verdicts, emits metrics, and logs but never blocks on rate limits.
  • Thresholds are tunable via KEYBOX_WARN_THRESHOLD / KEYBOX_BLOCK_THRESHOLD / KEYBOX_WINDOW_SECS; defaults are 500 / 5000 per hour per (fp, aud).
  • Redis errors are logged and ignored: an outage of the defense layer must never take down attestation. Blocklist hits are still logged even in shadow mode.

Linear

SEC-2275 -- Implement integrity token/key revocation mechanism (CRL-like)

Test plan

  • CI: cargo check / clippy / fmt
  • Send 600 attestations from the same intermediate-batch cert against the same audience on staging:
    • confirm attestation_gateway.keybox_defense{risk_level=Medium} increments
    • confirm requests are still served (shadow mode)
  • Set KEYBOX_DEFENSE_ENFORCE=1 on staging, push past KEYBOX_BLOCK_THRESHOLD, confirm 400 rate limit exceeded
  • redis-cli SET keybox:block:<fp> 1 and confirm next attestation gets 400 certificate blocklisted
  • Stop Redis temporarily, confirm attestations still succeed (defense is non-blocking on Redis errors)

Adds a stateful enforcement layer on top of the stateless integrity
signals from SEC-2276. Closes the keybox-bypass surface where a single
leaked batch certificate can be reused across many devices: any abuse
spike on a `(SHA256(intermediate_cert_DER), aud)` pair gets capped, and
known-bad fingerprints can be blocklisted out of band.

What it does:

* New `android::keybox_defense` module:
  * `KeyboxDefense::fingerprint(der)` -- lowercase hex SHA-256 of the
    intermediate (batch) cert.
  * `KeyboxDefense::evaluate(redis, fp, aud)` -- checks
    `keybox:block:{fp}` for an explicit blocklist entry, then
    increments a sliding-window counter at
    `keybox:count:{fp}:{aud}` with `EXPIRE = window_secs`. Maps the
    count to `RiskLevel::{Low, Medium, High, Blocked}`.
  * `KeyboxDefense::blocklist_add` / `blocklist_remove` for ops use.
* `AndroidAttestationOutput.batch_cert_fingerprint`: SHA-256 hex of the
  intermediate cert, computed once per attestation from the validated
  chain (uses `AndroidCertChain::intermediate_cert_der()` introduced
  in SEC-2314).
* `routes/a.rs`: after the nonce is consumed and we know the audience,
  call `evaluate(fp, &aud)` and reject when `verdict.should_reject()`.

Operational notes:

* **Shadow mode by default.** `KeyboxDefenseConfig::from_env()` reads
  `KEYBOX_DEFENSE_ENFORCE`; only when set to `1` does
  `verdict.should_reject()` ever return `true`. Until we flip the
  switch, the gateway computes verdicts, emits metrics, and logs but
  never blocks on rate limits.
* Thresholds are tunable via `KEYBOX_WARN_THRESHOLD` /
  `KEYBOX_BLOCK_THRESHOLD` / `KEYBOX_WINDOW_SECS`; defaults are
  500/5000 per hour per `(fp, aud)`.
* Redis errors are logged and ignored: an outage of the defense layer
  must never take down attestation. Blocklist hits are still logged
  even in shadow mode.

Maps to https://linear.app/worldcoin/issue/SEC-2275
@NnnOooPppEee NnnOooPppEee force-pushed the SEC-2275/keybox-defense-rate-limit-blocklist branch from 5fb7e52 to eabfd6a Compare April 17, 2026 02:06
Rate limiting and blocklisting in the keybox-defense layer target legacy
batch attestation keys, which are the only keys that can be extracted in
a keybox leak. Chains rooted in a Remote-Key-Provisioning (RKP) root are
issued by Google per-device and cannot originate from a leaked keybox,
so running them through the rate limiter and blocklist adds no
defensive value.

Set `batch_cert_fingerprint = None` when `is_rkp` is true. The existing
`if let Some(ref fp)` guard in `routes/a.rs` naturally short-circuits
both the rate limit increment and the blocklist lookup, so no changes
are needed on the handler side.

This also prevents legitimate RKP traffic from ever consuming rate-limit
budget, keeping the Redis counters focused on the surface area they were
designed for.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant