v1.23.1
A patch on 1.23.0: one fix, and the third-party vectors that found it.
The fix — di_vp is unsupported, not malformed (#147)
OpenID4VCI 1.0 §8.2 lets each proof type define its own element type: a jwt proof
is a string, a di_vp proof is a JSON object (App. F.2). openvc's shape check demanded
strings for every type and ran before the proof type was ever consulted, so a
spec-valid di_vp Credential Request came back as CredentialRequestMalformed —
contradicting the module's own documented contract, and telling a Credential Endpoint
the wallet had sent garbage when it had sent a proof of a type openvc declines. It is
now UnsupportedProofType.
No security change: both paths fail closed, and no request that was rejected before
is accepted now. What changes is the signal an endpoint maps to invalid_proof — and
whether an integrator can tell "unsupported" from "broken" in their telemetry.
The vectors that found it (#147, ADR-0007 D10)
The key-proof verifier shipped in 1.23.0 pinned entirely by proofs this repo minted
itself — openvc agreeing with openvc. tests/fixtures/openid4vci/ now adds material
written by others, and the bug above surfaced the moment the first one was added.
The spec's own examples. App. F.1's jwt proof turns out to be a complete, genuinely
signed ES256 token rather than an illustration. It verifies end-to-end with the clock
frozen to its own iat, so one third-party artifact holds the typ pin, the algorithm
allow-list, the key↔alg binding, the signing-input assembly and the aud/iat/nonce
checks. Two negatives ride along — the same bytes an hour later are stale, and under a
different Credential Issuer the aud binding rejects them — in both cases without
burning the nonce. Plus the three §8.2 Credential Request shapes.
Recorded from the EU reference issuer. Issuer Metadata and two Credential Offers
captured from https://issuer.eudiw.dev (eudi-srv-web-issuing-eudiw-py) on 2026-07-27,
the offers in the deep-link form a wallet receives rather than pre-decoded. 91 KB of
metadata: 27 credential configurations across mdoc and SD-JWT VC,
batch_credential_issuance.batch_size 100, a nonce_endpoint, request and response
encryption blocks, per-config proof_types_supported carrying key_attestations_required.
None of it is what we would have invented, which is the point.
Provenance is enforced, not described: a test re-checks every recorded artifact
against the sha256 its README documents.
Still self-made, and said so. No key proof here came from a shipping wallet — that
capture needs a live Credential Endpoint openvc does not ship (ADR-0007 D1), so
#147 stays open and
tests/fixtures/openid4vci/README.md states the gap instead of papering over it.
Documentation
The 1.23.0 release notes and changelog now name the full public surface of
openvc.openid4vci — all fourteen symbols, not five — and carry the two obligations
ADR-0007 places on the consumer, because a release note is where an integrator actually
reads them: check_nonce must be atomic (a SET … NX, a DELETE … RETURNING, never
a read-then-write, and explicitly not openvc.cache.TtlCache), and codes and
identifiers are yours to mint (#146).
The release gate also caught two stale statements: threat-model I17/I18 cited
openid4vci.py lines the fix shifted by eight, and docs/audit/assurance.md had no
OpenID4VCI row at all — the verifier shipped in 1.23.0 without one. Both fixed.
Verified
flake8 · mypy (61 source files) · 1565 tests passing, 25 skipped ·
python -m build + twine check on sdist and wheel · gitlint · the CI matrix
(3.10–3.14 plus 3.15 pre-release, and the pyld 2.0.4 leg).
No new runtime dependency — the core is still cryptography + pyjwt.
Full detail: CHANGELOG ·
guide: Issuing with OpenID4VCI