Skip to content

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
class CredentialRevoked(OpenvcError):
    """Raised by verifiers that treat a set revocation bit as a hard failure."""

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
class CredentialSuspended(OpenvcError):
    """Raised by verifiers that treat a set suspension bit as a hard failure
    (a suspended credential is temporarily invalid — not currently usable)."""

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
def parse_status_entries(credential: dict[str, Any]) -> list[StatusEntry]:
    """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)."""
    from .bitstring import StatusListError
    entries: list[StatusEntry] = []
    for raw in _as_entry_list(credential.get("credentialStatus")):
        types = raw.get("type")
        if isinstance(types, str):
            types = [types]
        elif not isinstance(types, (list, tuple)):
            types = []
        # Only string members can name a status-entry type; filtering them keeps a hostile
        # `type` (a non-iterable, or a list carrying an unhashable/dict member) from crashing
        # the set intersection with a bare TypeError — it is skipped like any unrelated type.
        matched = _ENTRY_TYPES.intersection(t for t in types if isinstance(t, str))
        if not matched:
            continue
        url = raw.get("statusListCredential")
        index = raw.get("statusListIndex")
        if not url or index is None:
            raise StatusListError(
                "status entry needs statusListCredential and statusListIndex")
        try:
            index_int = int(index)         # the spec encodes the index as a string
        except (TypeError, ValueError) as exc:
            raise StatusListError(f"invalid statusListIndex {index!r}") from exc
        entries.append(StatusEntry(
            status_list_credential=str(url),
            index=index_int,
            purpose=raw.get("statusPurpose", PURPOSE_REVOCATION),
            entry_type=next(iter(matched)),
        ))
    return entries

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
def check_credential_status(
    credential: dict[str, Any], *, resolve_status_list: ResolveStatusList
) -> StatusResult:
    """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."""
    with span("openvc.status"):
        results = [_read_status_bit(resolve_status_list(e.status_list_credential), e)
                   for e in parse_status_entries(credential)]
    return _tally_status(results)

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
async def check_credential_status_async(
    credential: dict[str, Any], *, resolve_status_list: AsyncResolveStatusList
) -> StatusResult:
    """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)."""
    with span("openvc.status"):
        results = [_read_status_bit(await resolve_status_list(e.status_list_credential), e)
                   for e in parse_status_entries(credential)]
    return _tally_status(results)

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 bits in {1, 2, 4, 8}, so a status is a value (0-255, constrained by bits), 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 bits bits of byte 0 (the W3C list is MSB-first); index 1 the next bits up, 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
def decode_status_list(lst: str) -> bytes:
    """base64url-decode then zlib-inflate a ``lst`` value into the packed bytes."""
    try:
        compressed = base64.urlsafe_b64decode(lst + "=" * (-len(lst) % 4))
    except (ValueError, TypeError) as exc:
        raise StatusListError(f"lst is not valid base64url: {exc}") from exc
    try:
        return inflate_bounded(compressed)
    except zlib.error as exc:
        raise StatusListError(f"lst is not valid zlib/DEFLATE: {exc}") from exc
    except DecompressionBomb as exc:
        raise StatusListError(f"lst decompresses too large: {exc}") from exc

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
def encode_status_list(data: bytes) -> str:
    """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."""
    return base64.urlsafe_b64encode(zlib.compress(data, 9)).rstrip(b"=").decode("ascii")

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
def new_status_list(size: int, *, bits: int = 1) -> bytearray:
    """A zeroed (all-VALID) packed list holding at least *size* statuses of
    *bits* each."""
    _check_bits(bits)
    if size < 0:
        raise StatusListError(f"size must be non-negative, got {size}")
    per_byte = 8 // bits
    return bytearray((size + per_byte - 1) // per_byte)

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
def get_status(data: bytes, index: int, *, bits: int = 1) -> int:
    """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."""
    _check_bits(bits)
    if index < 0:
        raise StatusListError(f"status index must be non-negative, got {index}")
    per_byte = 8 // bits
    byte_index, pos = divmod(index, per_byte)
    if byte_index >= len(data):
        raise StatusListError(
            f"status index {index} out of range for a {len(data) * per_byte}-status list")
    mask = (1 << bits) - 1
    return (data[byte_index] >> (pos * bits)) & mask

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
def set_status(data: bytearray, index: int, value: int, *, bits: int = 1) -> None:
    """Set the status value at *index* in-place (LSB-first). For issuers and
    tests. Raises if *value* does not fit in *bits*."""
    _check_bits(bits)
    if index < 0:
        raise StatusListError(f"status index must be non-negative, got {index}")
    mask = (1 << bits) - 1
    if not 0 <= value <= mask:
        raise StatusListError(f"status value {value} does not fit in {bits} bit(s)")
    per_byte = 8 // bits
    byte_index, pos = divmod(index, per_byte)
    if byte_index >= len(data):
        raise StatusListError(
            f"status index {index} out of range for a {len(data) * per_byte}-status list")
    shift = pos * bits
    data[byte_index] = (data[byte_index] & ~(mask << shift)) | (value << shift)

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
def parse_token_status_ref(claims: dict[str, Any]) -> TokenStatusRef | None:
    """Parse a referenced token's ``status.status_list`` reference, or ``None`` if
    the token carries no status-list reference. Raises on a malformed reference."""
    status = claims.get("status")
    if not isinstance(status, dict):
        return None
    ref = status.get("status_list")
    if not isinstance(ref, dict):
        return None
    uri = ref.get("uri")
    idx = ref.get("idx")
    if not uri or idx is None:
        raise StatusListError("status_list reference needs both uri and idx")
    if not isinstance(idx, int) or isinstance(idx, bool) or idx < 0:
        raise StatusListError(
            f"status_list idx must be a non-negative integer, got {idx!r}")
    return TokenStatusRef(uri=str(uri), index=idx)

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
def check_token_status(
    claims: dict[str, Any], *, resolve_status_list_token: ResolveStatusListToken
) -> TokenStatusResult | None:
    """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.
    """
    ref = parse_token_status_ref(claims)
    if ref is None:
        return None
    with span("openvc.status"):
        token_claims = resolve_status_list_token(ref.uri)
        return _token_status_result(ref, token_claims)

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
async def check_token_status_async(
    claims: dict[str, Any], *, resolve_status_list_token: AsyncResolveStatusListToken
) -> TokenStatusResult | None:
    """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)."""
    ref = parse_token_status_ref(claims)
    if ref is None:
        return None
    with span("openvc.status"):
        token_claims = await resolve_status_list_token(ref.uri)
        return _token_status_result(ref, token_claims)

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 an encodedList), returned UNSIGNED so the caller secures it with whichever proof suite it already uses (VcJwtProofSuite.sign or DataIntegrityProofSuite.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
def build_status_list_credential(
    *,
    id: str,
    issuer: str | dict[str, Any],
    bitstring: bytes | bytearray,
    status_purpose: str = "revocation",
    valid_from: datetime | None = None,
    valid_until: datetime | None = None,
    subject_id: str | None = None,
    ttl: int | None = None,
    contexts: list[str] | None = None,
) -> dict[str, Any]:
    """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.
    """
    subject: dict[str, Any] = {
        "id": subject_id or f"{id}#list",
        "type": BITSTRING_SUBJECT_TYPE,
        "statusPurpose": status_purpose,
        "encodedList": encode_bitstring(bytes(bitstring)),
    }
    if ttl is not None:
        subject["ttl"] = ttl
    credential: dict[str, Any] = {
        "@context": list(contexts) if contexts else [_VC2_CONTEXT],
        "id": id,
        "type": ["VerifiableCredential", BITSTRING_CREDENTIAL_TYPE],
        "issuer": issuer,
        "credentialSubject": subject,
    }
    if valid_from is not None:
        credential["validFrom"] = _iso(valid_from)
    if valid_until is not None:
        credential["validUntil"] = _iso(valid_until)
    return credential

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
def build_status_list_entry(
    *,
    status_list_credential: str,
    index: int,
    status_purpose: str = "revocation",
    id: str | None = None,
) -> dict[str, Any]:
    """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."""
    if index < 0:
        raise StatusListError(f"status index must be non-negative, got {index}")
    entry: dict[str, Any] = {
        "type": BITSTRING_ENTRY_TYPE,
        "statusPurpose": status_purpose,
        "statusListIndex": str(index),          # the spec encodes the index as a string
        "statusListCredential": status_list_credential,
    }
    if id is not None:
        entry["id"] = id
    return entry

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
def build_status_list_token(
    *,
    signing_key: SigningKey,
    uri: str,
    status_list: bytes | bytearray,
    bits: int = 1,
    issued_at: datetime | int | None = None,
    expires: datetime | int | None = None,
    ttl: int | None = None,
    issuer: str | None = None,
) -> str:
    """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."""
    _check_bits(bits)
    payload: dict[str, Any] = {
        "sub": uri,
        "iat": _epoch(issued_at) if issued_at is not None else int(time.time()),
        "status_list": {"bits": bits, "lst": encode_status_list(bytes(status_list))},
    }
    if issuer is not None:
        payload["iss"] = issuer
    if expires is not None:
        payload["exp"] = _epoch(expires)
    if ttl is not None:
        payload["ttl"] = ttl
    header = {"typ": STATUS_LIST_JWT_TYP, "alg": signing_key.alg, "kid": signing_key.kid}
    return sign_compact(header, payload, signing_key=signing_key)

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
def verify_status_list_token(
    token: str,
    *,
    public_key_jwk: dict[str, Any],
    expected_uri: str | None = None,
    leeway_s: int = 60,
    now: datetime | None = None,
) -> dict[str, Any]:
    """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."""
    header, claims = verify_compact(token, public_key_jwk=public_key_jwk)
    if header.get("typ") != STATUS_LIST_JWT_TYP:
        raise StatusListError(
            f"unexpected token typ {header.get('typ')!r}, want {STATUS_LIST_JWT_TYP!r}")
    if expected_uri is not None and claims.get("sub") != expected_uri:
        raise StatusListError(
            f"status list token sub {claims.get('sub')!r} != expected {expected_uri!r}")
    exp = claims.get("exp")
    if exp is not None:
        # exp is a NumericDate (RFC 7519); a present-but-non-numeric exp fails
        # CLOSED — skipping it would let a stale/superseded list be accepted (the
        # same fail-open bug openvc.proof._verify_common is written to avoid).
        if isinstance(exp, bool) or not isinstance(exp, (int, float)):
            raise StatusListError(
                f"status list token exp must be a numeric timestamp, got {exp!r}")
        instant = _epoch(now) if now is not None else int(time.time())
        if instant - max(0, leeway_s) > exp:
            raise StatusListError("status list token has expired")
    return claims

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
def build_token_status_reference(*, uri: str, index: int) -> dict[str, Any]:
    """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."""
    if index < 0:
        raise StatusListError(f"status index must be non-negative, got {index}")
    return {"status": {"status_list": {"idx": index, "uri": uri}}}

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
def default_credential_schema_resolver(
    *, fetch: Any = https_bytes_fetch,
) -> ResolveCredentialSchema:
    """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."""
    def resolve(url: str) -> bytes:
        return fetch(url)
    return resolve

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
def default_type_metadata_resolver(
    *, fetch: Any = https_bytes_fetch,
) -> ResolveTypeMetadata:
    """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."""
    def resolve(url: str) -> bytes:
        return fetch(url)
    return resolve

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
def default_status_list_resolver(
    *, resolver: Any = None, jwt_vc_issuer_fetch: Any = None,
    leeway_s: int = 60, extra_contexts: Any = None, fetch: Any = https_text_fetch,
) -> ResolveStatusList:
    """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."""
    def resolve(url: str) -> dict:
        from .verify import VerificationPolicy, verify_credential
        credential = _as_credential(fetch(url))
        result = verify_credential(
            credential, resolver=resolver, jwt_vc_issuer_fetch=jwt_vc_issuer_fetch,
            policy=VerificationPolicy(require_status=False, leeway_s=leeway_s),
            extra_contexts=extra_contexts)
        return result.credential
    return resolve

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
def default_status_list_token_resolver(
    *, resolver: Any = None, jwt_vc_issuer_fetch: Any = None,
    leeway_s: int = 60, fetch: Any = https_text_fetch,
) -> ResolveStatusListToken:
    """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."""
    def resolve(uri: str) -> dict:
        from .proof._jws import parse_compact
        from .status import StatusListError, verify_status_list_token
        from .verify import _resolve_jose_key, default_resolver
        token = fetch(uri).strip()
        header, payload, _, _ = parse_compact(token)
        iss, kid = payload.get("iss"), header.get("kid")
        if not isinstance(iss, str) or not iss:
            raise StatusListError(
                "status list token has no string `iss` to resolve its key")
        reg = resolver if resolver is not None else default_resolver()
        jwk = _resolve_jose_key(reg, iss, kid, jwt_vc_issuer_fetch)
        return verify_status_list_token(
            token, public_key_jwk=jwk, expected_uri=uri, leeway_s=leeway_s)
    return resolve

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
def default_credential_schema_resolver_async(
    *, fetch: Any = https_bytes_fetch_async,
) -> AsyncResolveCredentialSchema:
    """Async :func:`default_credential_schema_resolver` — fetches the raw schema
    bytes over the SSRF-guarded async https fetch."""
    async def resolve(url: str) -> bytes:
        return await fetch(url)
    return resolve

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
def default_status_list_resolver_async(
    *, resolver: Any = None, jwt_vc_issuer_fetch: Any = None,
    leeway_s: int = 60, extra_contexts: Any = None, fetch: Any = https_text_fetch_async,
) -> AsyncResolveStatusList:
    """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`."""
    async def resolve(url: str) -> dict:
        from .aio import verify_credential_async
        from .verify import VerificationPolicy
        credential = _as_credential(await fetch(url))
        result = await verify_credential_async(
            credential, resolver=resolver, jwt_vc_issuer_fetch=jwt_vc_issuer_fetch,
            policy=VerificationPolicy(require_status=False, leeway_s=leeway_s),
            extra_contexts=extra_contexts)
        return result.credential
    return resolve

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
def default_status_list_token_resolver_async(
    *, resolver: Any = None, jwt_vc_issuer_fetch: Any = None,
    leeway_s: int = 60, fetch: Any = https_text_fetch_async,
) -> AsyncResolveStatusListToken:
    """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)."""
    async def resolve(uri: str) -> dict:
        from .aio import _resolve_jose_key_async, default_async_resolver
        from .proof._jws import parse_compact
        from .status import StatusListError, verify_status_list_token
        token = (await fetch(uri)).strip()
        header, payload, _, _ = parse_compact(token)
        iss, kid = payload.get("iss"), header.get("kid")
        if not isinstance(iss, str) or not iss:
            raise StatusListError(
                "status list token has no string `iss` to resolve its key")
        reg = resolver if resolver is not None else default_async_resolver()
        jwk = await _resolve_jose_key_async(reg, iss, kid, jwt_vc_issuer_fetch)
        return verify_status_list_token(
            token, public_key_jwk=jwk, expected_uri=uri, leeway_s=leeway_s)
    return resolve