v1.23.0
Two additive features, and the scope decision behind them.
OpenID4VCI 1.0 — wallet key-proof verification (#141)
A new openvc.openid4vci verifies the openid4vci-proof+jwt a wallet sends to your
Credential Endpoint and returns the public key it demonstrated possession of — the
value SdJwtVcProofSuite.issue(holder_jwk=…) binds a credential to. The last mile
already existed; this supplies the first.
Checks run in a fixed order, structure and allow-lists before any crypto, and any
failure rejects the whole request — there is no partial issuance: the typ pin (so a
KB-JWT, VP-JWT or status-list token cannot be replayed as a key proof), the algorithm
allow-list, unknown crit, exactly one of the jwk/kid/x5c/trust_chain
header key parameters, the key↔alg (kty, crv) binding, the signature, aud pinned to
the Credential Issuer Identifier with a multi-valued aud rejected, and iat
freshness in both directions. Across a batch: one shared nonce, no two proofs on the
same key.
Two of those are the ones implementations get wrong. iat future-dated — without
it a wallet signs once with iat = now + 10y and holds a proof that never goes stale
(check_jwt_temporal reads exp/nbf and never iat, so this is new logic).
Exactly one key parameter — two present lets an attacker pair a kid naming an
honest key with a jwk they control, and anything that silently prefers one accepts it.
Nonce single-use is the caller's, injected as a required callable rather than
documented in prose: a plain expected_nonce string cannot express "consume once,
atomically", and offering it would make the fail-open path the ergonomic one. It fires
exactly once per request and only after every signature has verified, so an
unauthenticated attacker cannot burn nonces. Replay surfaces as a distinct
ProofReplayed so an endpoint can answer invalid_nonce and hand out a fresh one.
Stateless and transport-free: no endpoint, no Authorization Server, no state store, no
response/offer/metadata builders. The claim this supports is "OpenID4VCI 1.0
key-proof verification" — not "issuance", and not HAIP, which additionally requires
DPoP, key attestations and client authentication, all downstream.
What you get, and what you owe
The public surface: verify_credential_request_proofs, parse_credential_request,
CredentialRequest, VerifiedProof, ConsumeNonce, ResolveProofKey,
OpenID4VCIError, CredentialRequestMalformed, UnsupportedProofType,
ProofReplayed, PROOF_TYPE_JWT, PROOF_TYP, DEFAULT_PROOF_MAX_AGE_S,
MAX_PROOF_BYTES.
Two obligations travel back the other way, both load-bearing (ADR-0007):
check_noncemust be atomic — a RedisSET … NX, a SQLDELETE … RETURNING.
Never a read-then-write: under one, two concurrent requests both observe the nonce as
unused, and the replay window openvc cannot close on your behalf re-opens.
openvc.cache.TtlCacheis explicitly not suitable — it documents its own lack of
single-flight, which is benign for a read cache and fatal for a single-use token.- Codes and identifiers are yours to mint — openvc generates no pre-authorized code,
transaction_idornotification_id. They are opaque values with no bytes for a
library to get right, so it does not pretend to own them.
RFC 7638 JWK Thumbprint (#140)
openvc.keys grows jwk_thumbprint (base64url) and jwk_thumbprint_bytes (raw),
covering EC, OKP, RSA and oct. The canonical form is built from an explicit
allow-list of the required members rather than by filtering a deny-list of private
ones — so kid/use/alg and every private member are excluded by construction, a
private key hashes identically to its public half, and a member nobody anticipated can
never leak into a digest. Pinned by both published golden vectors (RFC 7638 §3.1,
RFC 8037 §2), whose examples carry alg/kid and so double as the exclusion test.
ADR-0007 — the scope decision (#139)
Records which half of OpenID4VCI openvc owns, and why the other half is not a
future release but someone else's job. The rule: attacker-controlled bytes that must
be verified or parsed fail-closed are openvc's; anything with a lifetime, a socket, or
a deployment policy is the consumer's.
The charter does not move — verifying attacker-controlled bytes and failing closed
is the same posture as the OpenID4VP verifier, one protocol over, and no out-of-scope
entry is retracted. What is corrected is an inaccurate summary: "read/verify-only" had
been untrue since 1.0, since sign, issue and the status-list builders all write.
The security property likewise generalises rather than doubles: no wrong-accept
now reads "credential or key proof", and the invariant catalog gains I17–I18.
Verified
flake8 · mypy (61 source files) · 1553 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) · published to PyPI via
Trusted Publishing.
No new runtime dependency — the core is still cryptography + pyjwt.
Full detail: CHANGELOG ·
guide: Issuing with OpenID4VCI