Status lists
Two encodings behind one interface — W3C Bitstring and IETF Token Status List — plus the issuer-side builders.
Checking
openvc.status.status_list
openvc.status.status_list — check a credential's credentialStatus against a
Bitstring Status List credential.
Transport- and proof-agnostic by design: the caller injects
resolve_status_list — a function that fetches the status-list credential URL
and returns the verified status-list VC as a dict (its signature is the
caller's concern, since the proof format and the SSRF policy for that host are
too). This module then parses the entry, decodes the bitstring, and reads the
bit. So it stays pure stdlib and reusable by any VC profile (OB 3.0, EBSI, EUDI).
Supports BitstringStatusListEntry and the older StatusList2021Entry —
same field names, same bit encoding.
CredentialRevoked
Bases: OpenvcError
Raised by verifiers that treat a set revocation bit as a hard failure.
Source code in src/openvc/status/status_list.py
34 35 | |
CredentialSuspended
Bases: OpenvcError
Raised by verifiers that treat a set suspension bit as a hard failure (a suspended credential is temporarily invalid — not currently usable).
Source code in src/openvc/status/status_list.py
38 39 40 | |
parse_status_entries(credential)
Parse the credential's credentialStatus into typed entries. Entries
whose type is not a recognised status-list entry are skipped (a credential
may carry unrelated status types).
Source code in src/openvc/status/status_list.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
check_credential_status(credential, *, resolve_status_list)
Resolve each status-list credential the credential references and read its
bit. Returns a :class:StatusResult; never raises on a set bit (that is a
verifier policy decision) — only on malformed data or a resolve failure.
Source code in src/openvc/status/status_list.py
144 145 146 147 148 149 150 151 152 153 | |
check_credential_status_async(credential, *, resolve_status_list)
async
Async :func:check_credential_status — awaits an async resolve_status_list;
identical decoding, tally, and fail-closed semantics (the bit-reading is the same
pure code).
Source code in src/openvc/status/status_list.py
156 157 158 159 160 161 162 163 164 165 | |
openvc.status.token_status_list
openvc.status.token_status_list — IETF Token Status List
(draft-ietf-oauth-status-list-21; IESG-approved and in the RFC Editor queue as of
2026-07, intended Proposed Standard — no RFC number yet). The codec is pinned
byte-for-byte to the draft §4.1 examples in tests/test_conformance_status_list.
The second status-list encoding, alongside the W3C Bitstring list in
openvc.status.bitstring. The differences that matter:
- Multi-bit statuses. Each referenced token gets
bitsin {1, 2, 4, 8}, so a status is a value (0-255, constrained bybits), not just a set/clear bit.0x00= VALID,0x01= INVALID (revoked),0x02= SUSPENDED; the rest are application-specific. - LSB-first packing. Index 0 occupies the lowest
bitsbits of byte 0 (the W3C list is MSB-first); index 1 the nextbitsup, and so on. - DEFLATE/zlib compression (the W3C list uses gzip).
A referenced token points at a status list with a status claim
{"status_list": {"idx": N, "uri": "..."}}; the status list token at that URI
carries {"status_list": {"bits": B, "lst": "<base64url zlib>"}}. As with the
W3C flow, resolving and verifying that token (its proof and the SSRF policy for
its host) is the caller's concern — injected as resolve_status_list_token —
so this module stays pure stdlib and proof-agnostic.
decode_status_list(lst)
base64url-decode then zlib-inflate a lst value into the packed bytes.
Source code in src/openvc/status/token_status_list.py
53 54 55 56 57 58 59 60 61 62 63 64 | |
encode_status_list(data)
zlib-deflate then base64url (unpadded) — the inverse of
:func:decode_status_list, for issuers and tests. zlib output is
deterministic for a fixed level, so no mtime dance is needed.
Source code in src/openvc/status/token_status_list.py
67 68 69 70 71 | |
new_status_list(size, *, bits=1)
A zeroed (all-VALID) packed list holding at least size statuses of bits each.
Source code in src/openvc/status/token_status_list.py
74 75 76 77 78 79 80 81 | |
get_status(data, index, *, bits=1)
Return the status value at index for a list packed bits per status (LSB-first). Raises for a bad bits or an out-of-range index.
Source code in src/openvc/status/token_status_list.py
84 85 86 87 88 89 90 91 92 93 94 95 96 | |
set_status(data, index, value, *, bits=1)
Set the status value at index in-place (LSB-first). For issuers and tests. Raises if value does not fit in bits.
Source code in src/openvc/status/token_status_list.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | |
parse_token_status_ref(claims)
Parse a referenced token's status.status_list reference, or None if
the token carries no status-list reference. Raises on a malformed reference.
Source code in src/openvc/status/token_status_list.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
check_token_status(claims, *, resolve_status_list_token)
Resolve the status list token a referenced token points at and read its status.
Returns None if the token carries no status-list reference. Never raises
on a non-VALID status — turning INVALID/SUSPENDED into a hard failure is the
verifier's policy — only on malformed data or a resolve failure.
Source code in src/openvc/status/token_status_list.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | |
check_token_status_async(claims, *, resolve_status_list_token)
async
Async :func:check_token_status — awaits an async resolve_status_list_token;
identical decoding and fail-closed semantics (the status-reading is the same
pure code).
Source code in src/openvc/status/token_status_list.py
211 212 213 214 215 216 217 218 219 220 221 222 | |
Issuing
openvc.status.issue
openvc.status.issue — issuer-side status-list construction.
The check side (:func:~openvc.status.status_list.check_credential_status,
:func:~openvc.status.token_status_list.check_token_status) reads a status
list; this is the other half — building the artifacts an issuer publishes and
updates in order to revoke:
- :func:
build_status_list_credential— a W3C BitstringStatusListCredential (the VC that wraps anencodedList), returned UNSIGNED so the caller secures it with whichever proof suite it already uses (VcJwtProofSuite.signorDataIntegrityProofSuite.add_proof): the status list is an ordinary VC. - :func:
build_status_list_token/ :func:verify_status_list_token— an IETF status-list token (typ: statuslist+jwt), signed and verified through the shared JOSE path (allow-listed{ES256, ES384, EdDSA, Ed25519}). - :func:
build_status_list_entry/ :func:build_token_status_reference— the tiny pointer an issuer embeds in each issued credential/token so a verifier knows which list bit to read.
A revocation flow: allocate a list (:func:~openvc.status.bitstring.new_bitstring
or :func:~openvc.status.token_status_list.new_status_list), set the revoked
indices (set_status_bit / set_status), build + sign the artifact, serve it
at the URL the credentials point at, and re-issue it when a revocation changes.
This is the only :mod:openvc.status module that touches :mod:openvc.proof (to
sign/verify the token); the codecs and the check side stay pure stdlib.
build_status_list_credential(*, id, issuer, bitstring, status_purpose='revocation', valid_from=None, valid_until=None, subject_id=None, ttl=None, contexts=None)
Build an UNSIGNED W3C BitstringStatusListCredential wrapping bitstring.
id is the URL a credential's statusListCredential points at; bitstring
is the raw bits (from :func:~openvc.status.bitstring.new_bitstring +
set_status_bit). status_purpose must match the entries that reference this
list. The result is a plain VC — sign it with any suite before publishing.
The inputs are not mutated.
Source code in src/openvc/status/issue.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
build_status_list_entry(*, status_list_credential, index, status_purpose='revocation', id=None)
Build a BitstringStatusListEntry for a credential's credentialStatus.
Add the return value to each credential you issue; toggling the same index in
the published list (:func:build_status_list_credential) then flips that
credential's status. status_purpose must match the list's.
Source code in src/openvc/status/issue.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
build_status_list_token(*, signing_key, uri, status_list, bits=1, issued_at=None, expires=None, ttl=None, issuer=None)
Build and sign an IETF status-list token (typ: statuslist+jwt).
uri (the sub claim, the list's own URL) is what a referenced token's
status.status_list.uri points at; status_list is the packed multi-bit
list (from :func:~openvc.status.token_status_list.new_status_list +
set_status). Signed via the allow-listed {ES256, ES384, EdDSA, Ed25519} path, so an
HSM/Vault key works. issued_at / expires accept a datetime or epoch int.
Source code in src/openvc/status/issue.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
verify_status_list_token(token, *, public_key_jwk, expected_uri=None, leeway_s=60, now=None)
Verify a status-list token and return its claims.
Allow-lists the algorithm, requires typ: statuslist+jwt, verifies the
signature, checks exp (within leeway_s), and — when expected_uri is
given — that sub equals the URL it was fetched from (the IETF anti-swap
check). The returned dict carries the status_list member, so it can be
handed straight to :func:~openvc.status.token_status_list.check_token_status
as the resolver's result.
Source code in src/openvc/status/issue.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
build_token_status_reference(*, uri, index)
Build the status claim a referenced token carries to point at an IETF
status-list token — merge it into the token's claims when you issue it.
Source code in src/openvc/status/issue.py
203 204 205 206 207 208 | |
Resolver factories
The batteries-included status-list resolvers the pipeline accepts (and their async twins), which fetch and verify a referenced status list before reading its bits.
openvc.resolvers
openvc.resolvers — blessed, SSRF-guarded default resolvers for the status and schema fetch paths.
:func:~openvc.verify.verify_credential's resolve_status_list,
resolve_status_list_token and resolve_credential_schema are caller-injected:
the transport, the SSRF policy for each issuer-named host, and — for status — the
proof verification of the fetched list are the caller's concern, so
openvc.fetch's guard protects these URLs only if you pass a guarded fetch. That
makes the secure path the opt-in path.
These factories are the safe drop-in that makes the secure path the easy path:
they fetch through the SSRF-guarded https fetch (openvc.fetch: https-only,
private/loopback/link-local blocked, redirects refused, connection pinned to the
validated IP) and — for status — verify the fetched status list through the
pipeline before trusting it. Pass one of these to verify_credential; a custom
resolver deliberately opts out of the guard.
from openvc import verify_credential
from openvc.resolvers import (default_status_list_resolver,
default_credential_schema_resolver)
verify_credential(
cred, resolver=reg,
resolve_status_list=default_status_list_resolver(resolver=reg),
resolve_credential_schema=default_credential_schema_resolver())
Caching for high-volume verification. A default status resolver fetches and
re-verifies the status list on every call — a batch of N credentials from one issuer
would otherwise download and verify one shared list N times. :func:openvc.verify.verify_many
(and its async twin) already deduplicate this within a call via
:func:openvc.cache.batch_resolvers (each distinct issuer / status list resolved once). For
repeated single verify_credential calls in a long-lived verifier, wrap your resolvers
with :func:openvc.cache.cached_resolve (a short-TTL memo) and your DID resolver with
:class:openvc.cache.CachingDidResolver, so a hot status list / DID document is not refetched
per credential.
default_credential_schema_resolver(*, fetch=https_bytes_fetch)
A resolve_credential_schema that fetches the raw schema bytes over the
SSRF-guarded https fetch. Returning bytes lets the pipeline verify a
credentialSchema.digestSRI over the exact response before parsing.
Source code in src/openvc/resolvers.py
59 60 61 62 63 64 65 66 67 | |
default_type_metadata_resolver(*, fetch=https_bytes_fetch)
A resolve for :func:openvc.type_metadata.validate_type_metadata that
fetches the raw Type Metadata bytes from a vct / extends URL over the
SSRF-guarded https fetch. Returning the exact bytes lets vct#integrity /
extends#integrity be verified over the response before parsing.
Source code in src/openvc/resolvers.py
70 71 72 73 74 75 76 77 78 79 | |
default_status_list_resolver(*, resolver=None, jwt_vc_issuer_fetch=None, leeway_s=60, extra_contexts=None, fetch=https_text_fetch)
A resolve_status_list (W3C Bitstring) that fetches the status-list
credential over the SSRF-guarded https fetch and verifies it through the
pipeline before returning it — a fetched-but-unverified status list would let a
forged one clear revocation. Status-of-status recursion is turned off
(require_status=False); resolver resolves the status issuer's key.
Source code in src/openvc/resolvers.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
default_status_list_token_resolver(*, resolver=None, jwt_vc_issuer_fetch=None, leeway_s=60, fetch=https_text_fetch)
A resolve_status_list_token (IETF) that fetches the statuslist+jwt
token over the SSRF-guarded https fetch, resolves the issuer key, verifies the
token (typ + signature + exp + sub == the fetched URI, the IETF
anti-swap check), and returns its claims.
Source code in src/openvc/resolvers.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
default_credential_schema_resolver_async(*, fetch=https_bytes_fetch_async)
Async :func:default_credential_schema_resolver — fetches the raw schema
bytes over the SSRF-guarded async https fetch.
Source code in src/openvc/resolvers.py
143 144 145 146 147 148 149 150 | |
default_status_list_resolver_async(*, resolver=None, jwt_vc_issuer_fetch=None, leeway_s=60, extra_contexts=None, fetch=https_text_fetch_async)
Async :func:default_status_list_resolver — fetches the status-list credential
over the SSRF-guarded async fetch and verifies it through the async pipeline
before returning it. resolver is an :class:AsyncDidResolver.
Source code in src/openvc/resolvers.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
default_status_list_token_resolver_async(*, resolver=None, jwt_vc_issuer_fetch=None, leeway_s=60, fetch=https_text_fetch_async)
Async :func:default_status_list_token_resolver — fetches the statuslist+jwt
over the SSRF-guarded async fetch, resolves the issuer key via the async pipeline,
and verifies the token (typ + signature + exp + sub == the fetched URI).
Source code in src/openvc/resolvers.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |