Skip to content

Commit 4649409

Browse files
authored
spec: revocation protocol fixes — naming, security, correctness
spec: revocation protocol fixes — naming, security, correctness
2 parents 7664c1f + 57c2d80 commit 4649409

4 files changed

Lines changed: 91 additions & 39 deletions

File tree

COMPARISON.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ COVID system found painful in practice.
4747

4848
MTA-QR supports short, medium, and long credential lifetimes. TTL-based expiry
4949
handles short-lived tokens. For medium and long-lived credentials, the protocol
50-
provides explicit revocation modeled on CRLite: the issuer constructs a signed
51-
Bloom filter cascade encoding revoked entry indices and serves it at `GET /revoked`
52-
on the same charge-cycle schedule as checkpoint updates. Verifiers cache the artifact
53-
locally and query it at scan time — no network access required. Because the issuer is
54-
the aggregator (unlike WebPKI CRLite, which needs a third-party aggregator), no
55-
additional trust infrastructure is needed beyond the existing trust configuration. For credentials that must
50+
provides explicit revocation using a filter cascade approach inspired by CRLite:
51+
the issuer constructs a signed Bloom filter cascade encoding revoked entry indices
52+
and serves it at `GET /revoked` on the charge-cycle schedule. Verifiers cache the
53+
artifact locally and query it at scan time — no network access required. Because
54+
the issuer is also the aggregator (unlike WebPKI CRLite, which requires a
55+
third-party aggregator), no additional trust infrastructure is needed beyond the
56+
existing trust configuration. The MTA-QR revocation format is not wire-compatible
57+
with CRLite artifacts. For credentials that must
5658
remain verifiable over years, the post-quantum signing posture also matters:
5759
a credential issued today under a classical algorithm may need to be verifiable
5860
long after quantum computers make that algorithm breakable.
@@ -172,10 +174,10 @@ marked as MTA-QR advantages depend on features that are defined in the spec but
172174
not yet fully implemented in this reference SDK:
173175

174176
- **Auditable revocation** is now fully specified in SPEC.md §Revocation.
175-
The format is a signed Bloom filter cascade (modeled on CRLite) distributed
176-
at `GET /revoked` on the charge-cycle schedule. The reference implementations
177-
still emit a "not implemented" stub — the cascade construction is the
178-
primary remaining SDK task before production use.
177+
The format is a signed Bloom filter cascade (inspired by CRLite, not wire-compatible
178+
with it) distributed at `GET /revoked` on the charge-cycle schedule. The reference
179+
implementations still emit a "not implemented" stub — the cascade construction is
180+
the primary remaining SDK task before production use.
179181
- **Mode 0 (fully offline)** is not yet implemented in the server-side verifiers.
180182
The browser demo and SDK implement Mode 0 verification.
181183
- **Post-quantum interoperability with the witness network** requires C2SP note

IMPLEMENTERS_GUIDE.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,15 @@ bit is 0, return current interpretation immediately.
566566
- Inserting elements in non-ascending order
567567
- LSB-first bit encoding instead of MSB-first
568568
- Inverted query alternation at odd levels
569+
- Not rejecting `k != 1` (using the k value to determine hash count produces
570+
silently wrong query results — always reject if k != 1)
569571
- Verifying signature on every query instead of at load time
570-
- Missing the staleness check (serving a stale artifact silently)
572+
- Missing the staleness check; remember it is entry-count-based and must be
573+
calibrated to your issuance rate — 32 entries means very different things
574+
at 10k/day vs 100/month
571575
- Not checking `origin` in the artifact body against the expected origin
576+
- Accepting an artifact signed with the wrong algorithm (must use sig_alg from
577+
trust config, same binding rule as for payload signatures)
572578

573579
**Rejection cases your test suite MUST cover:** See SPEC.md §Test Vectors —
574580
Revocation Vectors, cases R-REJ-1 through R-REJ-9.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ These are genuine gaps that should be addressed before production use. They are
222222

223223
**Mode 2 tile server not implemented.** The SDK verifier accepts Mode 2 payloads without verifying Merkle inclusion. A complete Mode 2 deployment requires a tile server serving proof tiles at `GET /tile/{level}/{index}` and a scanner-side tile-fetching verifier that calls it. The protocol for tile format and addressing is not yet defined in `SPEC.md`.
224224

225-
**Revocation not implemented.** The verifier emits a `revocation check` step with a fixed "not implemented — no revocation list defined yet" message in all four implementations. The revocation protocol is now fully specified in SPEC.md §Revocation (Bloom filter cascade over revoked/valid index sets, signed with the issuer checkpoint key, distributed at `GET /revoked` on the charge-cycle schedule, modeled on CRLite). Implementation is the primary remaining SDK task before production use.
225+
**Revocation not implemented.** The verifier emits a `revocation check` step with a fixed "not implemented — no revocation list defined yet" message in all four implementations. The revocation protocol is now fully specified in SPEC.md §Revocation (Bloom filter cascade over revoked/valid index sets, signed with the issuer checkpoint key, distributed at `GET /revoked` on the charge-cycle schedule, inspired by CRLite). Implementation is the primary remaining SDK task before production use.
226226

227227
**Mode 0 (embedded checkpoint) not implemented.** The payload codec defines `mode=0` where the cosigned checkpoint (root hash + issuer signature + witness cosignatures) is embedded directly in the payload. This eliminates the checkpoint fetch at scan time but still requires a pre-loaded trust configuration — the embedded signatures are verified against the issuer and witness public keys in that config. The issuer and verifier in all four SDKs handle only modes 1 and 2.
228228

SPEC.md

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,10 +1206,13 @@ The filter cascade encodes two disjoint sets derived from the log:
12061206
The universe is indices 1 through `tree_size - 1`. Index 0 (null entry) is
12071207
always excluded from both sets.
12081208

1209-
A Bloom filter cascade over (R, S) produces a structure with zero false negatives
1210-
and a configurable false positive rate. The false positive direction is deliberately
1211-
conservative: a false positive causes the verifier to incorrectly reject a valid
1212-
assertion, but never to incorrectly accept a revoked one.
1209+
A Bloom filter cascade over (R, S) has the following properties as a data structure:
1210+
zero false negatives (an element in R is always reported as revoked) and a configurable
1211+
false positive rate (an element in S may be reported as revoked). The false positive
1212+
direction is deliberately conservative. Note that "zero false negatives" is a property
1213+
of the cascade given a correctly populated R — it is not an end-to-end system guarantee.
1214+
An issuer that omits an entry from R produces a validly signed artifact that correctly
1215+
reports that entry as not revoked. See §Security Model — What the Protocol Does Not Guarantee.
12131216

12141217
### Security Model
12151218

@@ -1361,10 +1364,16 @@ if artifact.tree_size < verifier.latest_checkpoint_tree_size(origin):
13611364
reject artifact as stale
13621365
```
13631366

1364-
`REVOCATION_STALE_THRESHOLD` is a deployment parameter. Recommended value:
1365-
`2 * BATCH_SIZE` (32 for the reference implementation). This allows the issuer
1366-
one full batch publication cycle of slack before the artifact is considered
1367-
dangerously stale.
1367+
`REVOCATION_STALE_THRESHOLD` is a deployment parameter. The recommended
1368+
default is `2 * BATCH_SIZE` (32 for the reference implementation).
1369+
1370+
**Important:** this threshold is entry-count-based, not time-based. Its
1371+
meaning depends on the issuer's issuance rate. A transit system issuing
1372+
10,000 credentials per day has a 32-entry window representing minutes; a
1373+
low-volume issuer issuing 100 credentials per month has a 32-entry window
1374+
representing weeks. Deployments MUST calibrate this parameter to their
1375+
issuance rate. A future version may add a timestamp to the artifact body
1376+
to enable time-based staleness checks independent of issuance rate.
13681377

13691378
Issuers MUST publish a fresh revocation artifact every time they publish a new
13701379
checkpoint. An issuer that publishes checkpoints without updating the revocation
@@ -1409,6 +1418,16 @@ unsigned integer for hashing. No other encoding is permitted.
14091418
**Hash function.** SHA-256. Given an element `x` (8-byte big-endian uint64) and
14101419
hash function index `i` (0-indexed), the j-th bit position in a filter of `m` bits is:
14111420

1421+
*(Note: conventional Bloom filter implementations use non-cryptographic hash
1422+
functions such as MurmurHash3 for performance. SHA-256 is used here because it
1423+
is universally available across all target platforms without additional
1424+
dependencies, is already present in every MTA-QR implementation for Merkle tree
1425+
and checkpoint operations, and at MTA-QR deployment scales the performance
1426+
difference is negligible — building a cascade over 1,000 revoked entries requires
1427+
fewer than 10,000 SHA-256 calls, completing in under 1ms on any modern hardware.
1428+
Interoperability is the primary concern; a non-cryptographic hash that varies
1429+
between implementations would silently break cross-language cascade compatibility.)*
1430+
14121431
```
14131432
bit_position(x, i) = (big_endian_uint64(SHA-256(x || uint8(i))[0:8])) mod m
14141433
```
@@ -1458,7 +1477,11 @@ function BuildCascade(R, S):
14581477
if bits[bit] == 1:
14591478
new_fp_set.add(x)
14601479
1461-
// Next level: encode the false positives to eliminate them
1480+
// Next level: encode the false positives to eliminate them.
1481+
// Invariant: include_set is the set this level encodes.
1482+
// current_fp_set is the set whose false positives define the next level.
1483+
// Both are new_fp_set because Level N+1 encodes exactly the false
1484+
// positives of Level N — elements of S that made it through Level N.
14621485
include_set = new_fp_set
14631486
current_fp_set = new_fp_set
14641487
level_index += 1
@@ -1504,10 +1527,12 @@ The cascade is serialized as follows. All integer fields are big-endian.
15041527
uint8 num_levels (number of levels, 0..255)
15051528
for each level (index 0..num_levels-1):
15061529
uint32 bit_count (number of bits in this level's filter)
1507-
uint8 k (number of hash functions; always 1 in this spec)
1530+
uint8 k (number of hash functions; MUST be 1; reject if not 1)
15081531
ceil(bit_count/8) bytes bit_array (MSB of first byte = bit 0)
15091532
```
15101533

1534+
Verifiers MUST reject any cascade where `k != 1` at any level. A parser that uses the `k` value to determine how many hash functions to apply will compute different bit positions than intended, silently producing wrong query results without any parse error. The field is included in the format to preserve extensibility, but `k = 1` is the only valid value in this version.
1535+
15111536
Bit indexing: bit `i` of the filter is stored in byte `i/8` at bit position
15121537
`7 - (i mod 8)` (MSB-first within each byte). Bit 0 is the most significant
15131538
bit of byte 0.
@@ -1536,20 +1561,21 @@ as a checkpoint. The body is what the issuer signature covers.
15361561
```
15371562
<origin>\n
15381563
<tree_size decimal>\n
1539-
crlite-v1\n
1564+
mta-qr-revocation-v1\n
15401565
<base64(cascade_bytes)>\n
15411566
```
15421567

1543-
The `crlite-v1` literal is the artifact type identifier. Verifiers MUST reject
1544-
artifacts with unrecognized type identifiers. `cascade_bytes` is the binary
1568+
The `mta-qr-revocation-v1` literal is the artifact type identifier, specific to
1569+
MTA-QR. This format is not backward-compatible with CRLite artifacts. Verifiers
1570+
MUST reject artifacts with unrecognized type identifiers. `cascade_bytes` is the binary
15451571
encoding defined above, base64-encoded per RFC 4648 §4 (standard alphabet,
15461572
`=` padding, no line breaks).
15471573

15481574
**Full signed note:**
15491575
```
15501576
<origin>
15511577
<tree_size decimal>
1552-
crlite-v1
1578+
mta-qr-revocation-v1
15531579
<base64(cascade_bytes)>
15541580
15551581
— <issuer_key_name> <base64(4_byte_key_hash || issuer_signature)>
@@ -1563,6 +1589,14 @@ convention, identical to how the checkpoint issuer signature is computed.
15631589
Verifiers MUST verify this signature before trusting the artifact. An artifact
15641590
with an invalid or missing signature MUST be discarded entirely.
15651591

1592+
**Algorithm binding.** The revocation artifact signature MUST be verified using
1593+
the `sig_alg` from the trust configuration for the origin — the same algorithm
1594+
binding requirement that applies to payload signature verification (§Trust Model).
1595+
Verifiers MUST NOT infer the signing algorithm from the key name, key length, or
1596+
signature length. An implementation that dispatches by signature length would
1597+
accept an Ed25519 signature where ECDSA P-256 is required, or vice versa, since
1598+
both produce 64-byte signatures.
1599+
15661600
### Trust Configuration
15671601

15681602
The trust configuration MUST include a `revocation_url` field alongside
@@ -1589,10 +1623,12 @@ updates. The endpoint returns the full signed note as `text/plain; charset=utf-8
15891623
**Full artifact (`GET /revoked`):** A complete cascade covering all issued
15901624
non-expired entries. This is the only format defined for v1.
15911625

1592-
**Delta updates** are deferred to v2. The `crlite-v1-delta` artifact type
1593-
identifier is reserved but its format is not defined in this version. Issuers
1594-
that need bandwidth-efficient updates in high-volume deployments SHOULD track
1595-
the Clubcard construction (Schanck 2025) for future reference.
1626+
**Delta updates** are deferred to v2. The `mta-qr-revocation-v1-delta` artifact
1627+
type identifier is reserved but its format is not defined in this version.
1628+
Implementations MUST NOT implement `mta-qr-revocation-v1-delta` until the
1629+
format is specified — doing so before the spec exists guarantees incompatibility.
1630+
Issuers needing bandwidth-efficient updates at high volume SHOULD track the
1631+
Clubcard construction (Schanck 2025) for future reference.
15961632

15971633
### Verifier Behavior
15981634

@@ -1622,19 +1658,25 @@ and fetch failed or network unavailable), the verifier declines to verify and
16221658
reports the revocation check as failed. A missing artifact cannot be
16231659
distinguished from an attacker serving an empty revocation list.
16241660

1625-
**Fail-open posture (non-default).** Verifiers in deployments where revocation
1626-
is best-effort MAY configure fail-open: if no artifact is available, the
1627-
revocation check passes with a warning in the verification trace. This MUST be
1628-
an explicit deployment configuration. The verification trace MUST clearly
1629-
indicate that the revocation check was skipped.
1661+
**Fail-open posture (non-default, discouraged).** Verifiers in deployments
1662+
where revocation is genuinely best-effort MAY configure fail-open: if no
1663+
artifact is available, the revocation check passes. This MUST be an explicit
1664+
deployment configuration — never a default. Implementations MUST:
1665+
- Log a prominent warning at startup when fail-open is configured
1666+
- Include "revocation check skipped (fail-open)" in every verification trace
1667+
- Make fail-open harder to enable than fail-closed in the API or configuration
1668+
(e.g., require a named constant, not just `failOpen: true`)
1669+
1670+
Fail-open silently converts a network-level revocation suppression attack into
1671+
an accepted verification. Most deployments should not use it.
16301672

16311673
**Artifact load procedure.** When a revocation artifact is fetched (either
16321674
on first load or cache refresh), before storing it:
16331675
1. Parse the four-line body and the signature line.
16341676
2. Verify the issuer signature over the four-line body. If verification fails,
16351677
discard the artifact entirely. Do not cache it.
16361678
3. Check that `origin` in the body matches the expected origin.
1637-
4. Check that `artifact_type` is `crlite-v1`. Reject unrecognized types.
1679+
4. Check that `artifact_type` is `mta-qr-revocation-v1`. Reject unrecognized types.
16381680
5. Decode the base64 cascade bytes. If decoding fails, discard.
16391681
6. Apply the staleness check: if `tree_size` is more than
16401682
`REVOCATION_STALE_THRESHOLD` entries behind the verifier's latest
@@ -1683,9 +1725,11 @@ approximately `1.44 × n / 8` bytes plus a small constant per level.
16831725
- Schanck. "Clubcards for the WebPKI: smaller certificate revocation tests in
16841726
theory and practice." IEEE S&P 2025.
16851727
https://research.mozilla.org/files/2025/04/clubcards_for_the_webpki.pdf
1686-
- Mozilla CRLite reference implementation (MPL-2.0):
1728+
- Mozilla CRLite reference implementation — filter cascade construction reference
1729+
(MPL-2.0, binary format not compatible with MTA-QR):
16871730
https://github.com/mozilla/crlite
1688-
- Mozilla rust-cascade library (MPL-2.0):
1731+
- Mozilla rust-cascade library — cascade algorithm reference
1732+
(MPL-2.0, binary format not compatible with MTA-QR):
16891733
https://github.com/mozilla/rust-cascade
16901734

16911735
---
@@ -1873,7 +1917,7 @@ Expected:
18731917
| R-REJ-2 | Valid signature, `bit_count = 0` at any level | Discard: malformed artifact |
18741918
| R-REJ-3 | Signature over correct body, but signed by wrong key | Discard: signature verification failure |
18751919
| R-REJ-4 | Artifact body `origin` does not match expected origin | Discard: origin mismatch |
1876-
| R-REJ-5 | Artifact body `artifact_type` is `crlite-v2` (unrecognized) | Discard: unknown type |
1920+
| R-REJ-5 | Artifact body `artifact_type` is `mta-qr-revocation-v2` (unrecognized) | Discard: unknown type |
18771921
| R-REJ-6 | Artifact `tree_size` is 0 | Discard: malformed (tree_size must be ≥ 1) |
18781922
| R-REJ-7 | Valid artifact, but `artifact.tree_size` is more than `REVOCATION_STALE_THRESHOLD` behind verifier's checkpoint `tree_size` | Discard: stale artifact |
18791923
| R-REJ-8 | Artifact body has only 3 lines (missing `filter_bytes_base64` line) | Discard: parse error |

0 commit comments

Comments
 (0)