Async verification
openvc.aio is the async counterpart of the sync
verification pipeline, for asyncio servers (FastAPI /
Starlette): a handler awaits verify_credential_async instead of offloading the
whole call to a thread pool, and verify_many_async verifies a presentation
cascade concurrently instead of serialising N blocking fetches.
It reuses every proof suite, status/schema codec and binding check of the sync path
unchanged — only the I/O sequencing is re-expressed with await, so there is no
second implementation of any signature check to drift. The batteries-included async
fetch (openvc.fetch.https_json_fetch_async) runs the exact same SSRF /
DNS-rebind guard as the sync fetch under asyncio.to_thread; a caller may inject an
httpx.AsyncClient-backed fetch instead. See
ADR-0002 for the design and its trade-offs.
import asyncio
from openvc import verify_credential_async
from openvc.aio import default_async_resolver
async def main():
result = await verify_credential_async(token, resolver=default_async_resolver())
print(result.issuer)
asyncio.run(main())
openvc.aio
openvc.aio — an additive async verification surface over the sync pipeline.
:func:verify_credential_async mirrors :func:openvc.verify.verify_credential
exactly — same formats, same policy, same fail-closed guarantees — but await\s
its I/O (DID resolution, jwt-vc-issuer discovery, status-list and
credentialSchema fetches) instead of blocking. It reuses every pure/CPU
helper of the sync path unchanged (the proof suites, the status/schema codecs, the
issuer binding and type checks); only the sequencing around the I/O boundaries is
re-expressed with await. There is no second implementation of any signature
check to drift — see docs/adr/ADR-0002-async-verification.md.
Injection mirrors the sync path with async counterparts: resolver is an
:class:~openvc.did.base.AsyncDidResolver; resolve_status_list /
resolve_status_list_token / resolve_credential_schema / jwt_vc_issuer_fetch
each return an awaitable. The caller supplies the transport — an
httpx.AsyncClient-backed fetch, or the batteries-included
:func:openvc.fetch.https_json_fetch_async (the exact same SSRF/DNS-rebind guard
as the sync fetch, run off the event loop). :func:default_async_resolver wires
the offline did:key / did:jwk resolvers (via :func:~openvc.did.base.as_async_resolver)
and the SSRF-guarded async did:web resolver.
:func:verify_many_async verifies a batch concurrently (asyncio.gather),
each item independently fail-closed — the fix for a presentation cascade that would
otherwise serialise N blocking fetches.
Data Integrity note: the embedded-proof suites resolve the proof's
verificationMethod synchronously; the async path resolves that DID first
(awaiting the injected resolver) and hands the suite a one-shot in-memory resolver,
so the suite's own resolution does no I/O. The canonicalisation + signature check
then run inline on the event loop (bounded CPU).
default_async_resolver()
The async counterpart of :func:openvc.verify.default_resolver: the offline
did:key / did:jwk resolvers (adapted via :func:as_async_resolver) plus
the SSRF-guarded async did:web and did:webvh resolvers.
Source code in src/openvc/aio.py
87 88 89 90 91 92 93 94 95 96 97 98 99 | |
verify_credential_async(credential, *, policy=None, resolver=None, resolve_status_list=None, resolve_status_list_token=None, resolve_credential_schema=None, jwt_vc_issuer_fetch=None, x5c_trust_anchors=None, extra_contexts=None, _depth=0)
async
Async :func:openvc.verify.verify_credential. Every keyword means what it does
there, with async counterparts for the injected I/O: resolver is an
:class:~openvc.did.base.AsyncDidResolver (default :func:default_async_resolver),
and the status/schema/issuer fetches return awaitables. Same formats, same policy,
same fail-closed behaviour and same :class:VerificationResult.
Source code in src/openvc/aio.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
verify_many_async(credentials, *, policy=None, resolver=None, resolve_status_list=None, resolve_status_list_token=None, resolve_credential_schema=None, jwt_vc_issuer_fetch=None, x5c_trust_anchors=None, extra_contexts=None)
async
Verify many credentials concurrently (asyncio.gather), each independently
fail-closed: one failure becomes that item's error and never aborts the others.
Returns one :class:BatchResult per input, in input order. Unlike the sync
:func:openvc.verify.verify_many, this does not de-duplicate shared resolutions
across credentials (that cache is not concurrency-safe — ADR-0002 D4); overlapping
the I/O is the win here.
Source code in src/openvc/aio.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
Async resolvers & fetch
The async DID-resolution and fetch primitives the pipeline awaits.
openvc.did.base.AsyncDidResolver
Bases: Protocol
The async counterpart of :class:DidResolver: supports stays a plain
predicate (no I/O), resolve is awaitable so a backend can await a non-blocking
fetch. A sync resolver is adapted to this shape by :func:as_async_resolver.
Source code in src/openvc/did/base.py
231 232 233 234 235 236 237 238 239 | |
supports(did)
Whether this resolver handles did (its DID method).
Source code in src/openvc/did/base.py
236 237 | |
resolve(did)
Resolve did to a :class:DidDocument, or raise :class:DidResolutionError.
Source code in src/openvc/did/base.py
238 239 | |
openvc.did.base.AsyncDidResolverRegistry
The async counterpart of :class:DidResolverRegistry: dispatches an awaitable
resolve to the first :class:AsyncDidResolver that supports the DID.
Source code in src/openvc/did/base.py
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
register(resolver)
Add a resolver (consulted in registration order).
Source code in src/openvc/did/base.py
270 271 272 | |
resolve(did)
async
Resolve did via the first matching resolver, else raise UnsupportedDidMethod.
Source code in src/openvc/did/base.py
274 275 276 277 278 279 | |
openvc.did.base.as_async_resolver(resolver)
Wrap a sync :class:DidResolver as an :class:AsyncDidResolver (its
resolve awaits nothing). Lets an offline did:key / did:jwk resolver — or any
sync resolver whose blocking is acceptable — drop into an async registry.
Source code in src/openvc/did/base.py
256 257 258 259 260 | |
openvc.did.did_web.AsyncDidWebResolver
The async counterpart of :class:DidWebResolver: same URL mapping and id
integrity check, but awaits an injected async fetch (pass
:func:openvc.fetch.https_json_fetch_async for the SSRF-guarded one).
Source code in src/openvc/did/did_web.py
67 68 69 70 71 72 73 74 75 76 77 78 | |
openvc.fetch.https_json_fetch_async(url, *, timeout_s=DEFAULT_TIMEOUT_S, max_bytes=MAX_RESPONSE_BYTES)
async
Async :func:https_json_fetch — same SSRF guards, run off the event loop.
Source code in src/openvc/fetch.py
233 234 235 236 237 | |
openvc.fetch.https_text_fetch_async(url, *, timeout_s=DEFAULT_TIMEOUT_S, max_bytes=MAX_RESPONSE_BYTES)
async
Async :func:https_text_fetch — same SSRF guards, run off the event loop.
Source code in src/openvc/fetch.py
240 241 242 243 244 | |
openvc.fetch.https_bytes_fetch_async(url, *, timeout_s=DEFAULT_TIMEOUT_S, max_bytes=MAX_RESPONSE_BYTES)
async
Async :func:https_bytes_fetch — same SSRF guards, run off the event loop.
Source code in src/openvc/fetch.py
247 248 249 250 251 | |
openvc.fetch.default_async_did_web_resolver()
An :class:~openvc.did.did_web.AsyncDidWebResolver wired to the SSRF-guarded
async fetch — the async counterpart of :func:default_did_web_resolver.
Source code in src/openvc/fetch.py
254 255 256 257 | |