Releases: luisgf/openvc
Release list
v1.21.0
Added
-
EUDI relying-party registration certificates (WRPRC, ETSI TS 119 475 V1.2.1
clause 5.2) — the other half of #67's
WRPAC. Where the access certificate authenticates who is asking, the registration
certificate answers "were they registered to ask for this?": it carries the
relying party's entitlements and the credentials/attributes it may request. New
moduleopenvc.rp_registration, with the library's usual trusted/untrusted split —
parse_rp_registration_certificate(UNTRUSTED, header-profile only) and
verify_rp_registration_certificate, which anchors the signer's chain in
caller-provided registrar roots. Both profiled forms are read: the JWT
(rc-wrp+jwt) over the JOSE lane, the CWT (rc-wrp+cwt) over the
dependency-free CBOR/COSE codec, sharing the{ES256, ES384, EdDSA, Ed25519}
allow-list applied before any crypto and thex5cprimitives now exported from
openvc.x5c(load_x5c_chain,load_der_chain,leaf_public_jwk).Verification alone only proves a registrar signed something, so two cross-checks
carry the authorization decision, both fail-closed:
check_matches_access_certificatebinds the WRPRC'ssubto the WRPAC's
entity_identifier(GEN-5.1.1-04 — without it an attacker pairs their own valid
WRPAC with someone else's valid WRPRC and inherits that scope; an identifier absent
on either side is a failure, never a match), andcheck_request_within_registration
requires every DCQL credential query to match a registeredformatwhosemeta
covers the requested one, with every requested claimpathinside the registered
paths. A registered container covers its members; a request naming no claims asks for
everything and is refused against an enumerated registration.Only a verify subset of the JAdES baseline B-B profile GEN-5.2.1-04 mandates is
implemented — the signed-header profile (typ, allow-listedalg, thex5cchain,
acritthat fails closed on any parameter this verifier does not process) plus the
chain validation. No signature-policy processing, timestamps, or augmentation.Three properties of the profile are easy to get wrong and are handled explicitly:
one WRPRC carries exactly one intended use (clause 5.2.4 flattens TS5's nested
intendedUse[]);expis optional (Table 10), so the 12-month ceiling
(GEN-5.2.4-08) binds only when it is present andrequire_expiry=Trueis opt-in; and
the CWT form has no claim-key mapping in TS 119 475 — the envelope is fully
specified (RFC 9052/9360) and implemented, while the claims map is read accepting both
the RFC 8392 integer keys and text keys, which is the only reading available to an
issuer today. That lane is provisional until a real artifact exists to pin. Two
published spec defects are absorbed rather than inherited:intermediary.sname
(Table 10) vsintermediary.name(Annex C), andintermediary.subvsact.sub
(GEN-5.2.4-09) — both spellings are read. The German BMI Architekturkonzept
certificate (rc-rp+jwt) is a different profile and is refused by name rather than
half-parsed under the wrong claim semantics.No official signed vectors exist (the deliverable ships one informative, unsigned
Annex C example; the 2026 EAA Plugtests covered TS 119 472-1), so
tests/test_rp_registration.pypins the Annex C payload verbatim and otherwise
builds both forms over the library's own machinery — 106 tests, negative paths first.
Recording a real third-party artifact stays a gated follow-up.An adversarial review of the verify and authorization paths found six issues, all
fixed here with a regression test named for the attack. The one that mattered:
the claim-path reader tookclaimbeforeclaimson both sides, which is
correct for a registration (the spec's spelling) but on a request let a relying
party hide a narrow decoy inclaimand the real, broader ask inclaims— openvc
authorized the decoy while the wallet, following DCQL, answersclaims, and unknown
query members are ignored downstream so the escalating query stayed valid end to end.
The request side now takes the union of both spellings; the registration side
keeps precedence, since only there does a second spelling risk widening a grant.
Also closed: a bareOverflowErrorescaping theOpenvcErrorfamily on a bignum
iat/exp/nbf(math.isfinitecasts to float, andjson.loadsyields such ints
from the wire); an empty registered{"path": []}acting as a blanket grant over the
whole credential (an empty tuple is a prefix of everything); a non-objectmetabeing
coerced to{}, which turned a malformed constraint into no constraint and
widened the entry to every credential of that format;metamatching conflating
Truewith1; and an entitlement floor that accepted any non-empty string while
ENTITLEMENT_URI_PREFIXsat exported-but-unused — it now checks the clause-A.2
namespace GEN-5.2.4-03 actually requires. The binding check, the algorithm allow-list,
the chain handling and the CWT parser were probed and held.
(#89)
Fixed
- Documentation drift: the WRPAC was attributed to ETSI TS 119 475 (it is
TS 119 411-8; 475 is the WRPRC) in the wiki module map and the API reference,
where it was also described as carrying "registered entitlements" — the WRPRC's job,
not the WRPAC's.
v1.20.4
Part of the Q3–Q4 2026 — eIDAS deadline & ecosystem refresh milestone.
Security
- Unknown JWS
critextensions are now rejected on every JOSE verify lane (RFC 7515 §4.1.11). openvc processes no JWS extension header parameters, so a token marking any as critical must fail closed — but the hand-rolled JWS lanes (the SD-JWT issuer JWT and KB-JWT, and the IETF status-list token) accepted them regardless of PyJWT version, and the VC-JWT lane inherited pre-2.13 PyJWT behaviour (CVE-2026-32597). All lanes — VC-JWT (the ML-DSA one included), SD-JWT, KB-JWT, status-list token — now reject through one shared check (reject_unknown_crit), matching the stance the COSE and JWE paths already took. Regression tests per lane, the publicverify_status_list_tokenentry point and the error-precedence ordering included. An adversarial review (parser tricks incl. duplicate / unicode-escapedcritkeys, lane completeness across every public entry point, precedence, hostile shapes,verify_manyisolation, global state) found no bypass; its coverage recommendations are these tests. (#125) - PyJWT floor raised to
>=2.13— 2.13.0 (2026-05-21) is a security release. The advisory-by-advisory reachability audit through openvc's usage is recorded indocs/audit/assurance.md§5: thePyJWK/PyJWKClient/ HMAC-confusion classes are structurally unreachable (allow-list before crypto, noPyJWK(Client), SSRF-guarded JWKS fetch); thecrit(CVE-2026-32597) andb64=false(CVE-2026-48525) classes were reachable pre-2.13 on the VC-JWT lane and are closed by the floor plus the openvc-sidecritrejection above.cryptography49.0.0 compatibility confirmed. (#125)
Verification for this release: flake8 + mypy clean; pytest 1217 passed / 20 skipped (coverage gate met); adversarial review clean (no bypass; findings were coverage recommendations, all addressed); sdist + wheel built and twine check passed; full CI matrix green (py3.10–3.14 + the pyld 2.0.4 floor leg); published to PyPI via Trusted Publishing with digital attestations.
v1.20.3
Part of the Q3–Q4 2026 — eIDAS deadline & ecosystem refresh milestone — the first two slices of the 2026-07-17 standards-review wave.
Changed
- Documentation truth pass (2026-07 roadmap refresh).
docs/ROADMAP.mdnow points at the live milestones — Q3–Q4 2026 — eIDAS deadline & ecosystem refresh, Conformance & production readiness and Long term — PQ, BBS & 2.0 — instead of the closed short/medium pair, and the long-term summary no longer lists shipped work (ML-DSA, DID 1.1 tolerance) as future. No code change. (#128) - pyld 3.x verified; the
[data-integrity]floor stayspyld>=2.0.4, now tested at both edges. PyLD came back to life (3.0.0 / 3.1.0, 2026-06-19, after two dormant years) with JSON-LD 1.1 conformance fixes — exactly the class of change that could silently shift RDF canonicalization and break Data Integrity signatures, and fresh installs already resolve 3.x. The full suite — the byte-for-bytevc-di-eddsagolden and theecdsa-sdintermediates included — passes identically on 2.0.4 and 3.1.0, so the permissive floor is deliberately kept and CI now runs apyld==2.0.4floor leg alongside the latest-resolving matrix. The bundled-context loader stays openvc's own rather than pyld 3'sFrozenDocumentLoader/BUNDLED_CONTEXTS, so the exact context bytes feeding canonicalization ship vendored in this package (openvc.proof.contexts). (#124)
Also restores the ## [1.20.2] — 2026-07-16 CHANGELOG heading accidentally dropped in #129 (1.20.2's notes had been left dangling under the unreleased block).
Verification for this release: flake8 + mypy clean; pytest 1208 passed / 20 skipped (coverage 90%); sdist + wheel built and twine check passed; full CI matrix green (py3.10–3.14 plus the new pyld 2.0.4 floor leg); published to PyPI via Trusted Publishing with digital attestations.
v1.20.2
Two fail-closed conformance fixes, each surfaced by holding the code to real third-party artifacts instead of self-recorded round-trips.
Fixed
- XAdES trust-list verifier now accepts real EU trusted lists (#114). The v1.20.0 1-reference pin rejected every genuine XAdES-BASELINE signature — the EU LOTL and national TLs sign the enveloped document plus their own
SignedProperties(two references). Coverage is now anchored on the envelopedURI=""reference resolving to the trust-list root (plus optionalSignedProperties/ co-signedds:KeyInfo). Anchoring onURI=""— not on the resolved element's tag — is what defeats XML-Signature-Wrapping; an adversarial review of the initial fix caught a tag-equality gap (a by-Idrelocation attack), now pinned by a regression test. - Bitstring Status List
encodedListis multibase-conformant on both sides (#115). The W3C v1.0 REC mandates a multibase (u-prefixed)encodedList, but the codec used bare base64url:decode_bitstringcould not consume a spec-conformant (or any real third-party) list, andencode_bitstringissued non-conformant ones.decode_bitstringnow tolerates theuprefix (legacy prefix-less lists still decode) andencode_bitstringemits it.
Added
- Real Commission-signed golden fixtures — the EU LOTL (seq 388) and Spanish national TL (seq 187), verified end to end through the XAdES verifier (#114).
- Third-party interop vectors — an SD-JWT VC from RFC 9901 Appendix A.3 (verified against the published A.5 key) and a real EUDI reference PID (ES256,
x5c), plus W3C BitstringencodedListdecode vectors (the REC's Example 3 + a Digital Bazaar list) (#115).
Verified: full test matrix (py3.10–3.14), flake8, mypy, and twine check all green on the tag; published to PyPI via Trusted Publishing.
PyPI: pip install openvc-core==1.20.2
v1.20.1
Added
- External-audit pack (
docs/audit/) — audit-readiness groundwork for the
funded external review: a code-cited
threat-model annex
(per-suite and per-parser attack-surface tables, the fail-closed invariants
catalog I1–I15, and a residual-risk register R1–R8), an
assurance report
(property-based fuzz coverage, the harden-next gap map, and the
adversarial-review history), and a
reviewer index
with the suggested review scope and EU funding routes. No code change — the
external audit itself stays gated on funding.
(#75)
Security
- Fail closed on hostile deeply-nested JSON across the verify pipeline. A
deeply-nested (but valid) JSON credential — an SD-JWT header/payload/disclosure, a
chain of SD-JWT disclosures each carrying the next_sddigest, a VC-JWT
header/payload, or an enveloped VC'sdata:payload — madejson.loads(or the
SD-JWT_unpackrecursion) raiseRecursionError(aRuntimeError, not an
OpenvcError). On the untrusted peek/unwrap path that escapedverify_many's
per-credential isolation and aborted the whole batch (denial-of-service; not a
wrong-accept) — reachable unauthenticated and, on CPython 3.10–3.13, with ~1 KB of
input. SD-JWT_unpacknow caps recursion at depth 100 (parity withcbor=64 /
_jcs=100), the did:webvh genesis SCID walk (_deep_replace_scid) is depth-bounded
too, and every attacker-facingjson.loads— SD-JWT, VC-JWT peek/verify,_jws,
the enveloped unwrap,jwe, and thedid:jwk/did:webvh/ fetch / status-resolver
paths — now mapsRecursionErrorto a typed error, so hostile input fails closed and
verify_manyisolates it across all formats. Resolves the R1 residual risk from
the audit pack. (#117)
v1.20.0
[1.20.0] — 2026-07-10
Part of the Depth — mdoc, status trust & parity
milestone — the second wave from the 2026-07-10 internal audit.
Added
- Opt-in status-list issuer binding (ADR-0006).
VerificationPolicygainsrequire_status_issuer_binding— bind the resolved status
list's issuer to the credential's issuer — andstatus_issuer_allowlistfor trusted
delegates. A status list is authenticated but its issuer was otherwise unconstrained, so
a compromised status host serving a list signed by an attacker key could silently
"un-revoke" a credential; enabling the binding closes that. Off by default (delegation is
spec-legal — no behaviour change); a mismatch raises the new typed
StatusListIssuerUntrusted. Enforced in both the sync and async pipelines.
(#106)
Changed
- Performance: the bundled JSON-LD context is parsed once, not per proof. RDF Data
Integrity canonicalization re-read and re-parsed the bundledcredentials/v2context from
disk on every sign/verify; it is now parsed once and shared read-only (still a fresh
top-level dict per call, so an injectedextra_contextscan't pollute the cache). The
three whole-document DI suites (eddsa-rdfc-2022,ecdsa-rdfc-2019, JCS) now share one
proof-preamble helper (_verify_common.prepare_di_proof), so the fail-closed proof
validation cannot drift between them — behaviour is byte-for-byte identical (pinned by the
golden fixtures).openvc.resolversdocumentsopenvc.cache.cached_resolve/
CachingDidResolverfor high-volume verifiers (verify_manyalready deduplicates status /
DID resolution within a call). (#110)
Fixed
- EBSI: the recursive trust-chain walk now accepts a wildcard accreditation, matching the
single-level policy. An accreditation with nocredentialTyperestriction (an
unrestricted delegation) was accepted byopenvc_ebsi.verify's single-level check but
rejected at the leaf of the recursiveverify_trust_chain; the two now agree — a wildcard
delegates the credential's types. (#109)
Security
- mdoc: the document-signer certificate profile is now enforced.
resolve_mdoc_signer_key
requires the leaf to carry the ISO 18013-5 Annex B document-signer ExtendedKeyUsage
(1.0.18013.5.1.2) — previously any certificate that chained to a trusted IACA anchor
(a TLS leaf, a DS for another purpose) was accepted as an MSO signer. The MSOvalidityInfo
must now carrysigned, andsignedmust fall within the document-signer certificate's own
validity window (ISO 18013-5 §9.3.1). Verified against the real Annex D reference certificate,
which carries the EKU. (#105) - XAdES trusted-list verification hardened. A Trusted List's XAdES / XML-DSig signature
is now verified with a pinned algorithm profile (RSA / ECDSA — including RSA-PSS — over
SHA-256/384/512; HMAC, DSA, SHA-1 and SHA-224 rejected) and exactly one Reference, and
the verified subtree must be the document root — an XML-Signature-Wrapping guard, so a
signature valid over a fragment cannot leave unsignedTrustServiceProvider/ certificate
nodes outside the signed scope for the parser to harvest. The TL parser also enforces a
max element count (ADR-0003 D4) alongside the existing byte cap.
(#107)
v1.19.3
[1.19.3] — 2026-07-10
Part of the Correctness & fail-closed hardening
milestone — the 2026-07-10 internal-audit hardening wave.
Fixed
- Typed-error boundary on hostile input — a non-object JOSE header/payload no longer
crashes untyped or aborts a batch. A credential orvp_tokenwhose JOSE header or
payload is valid JSON but not an object (e.g. a bare array[0]) reached the
untrusted peek path and raised a bareAttributeError, which escaped the
OpenvcErrorfamily and — throughverify_many— aborted the entire batch, breaking
both the fail-closed typed-error contract and the documented per-item isolation.
peek_issuer/peek_claimsand the SD-JWT decoder now reject a non-object
header/payload with a typedMalformedToken. The same pass closes sibling untyped
escapes on attacker-controlled input:ecdsa_sd.verify(a hostileproofValueor an
unknown@contextnow raiseProofValueMalformed/ProofMalformedinstead of a raw
pyld error), a malformed Ed25519 JWK (ProofMalformed), a lone surrogate in JCS
(JcsError), and a non-JSON-object EBSI registry200(MalformedRegistryResponse).
(#99) - Verify-path consistency & fail-closed defaults across formats. Several checks were
hardened on one proof family but not its siblings: the JOSE temporal check is now
single-sourced (_verify_common.check_jwt_temporal) and rejects a non-finite
exp/nbf(NaN/Infinity, whichjson.loadsaccepts and which never expires) on
the SD-JWT VC and VP-JWT paths too, not only VC-JWT; VC-JWT now pins the EC curve to
the alg (anES256header can no longer verify against a P-384 key), matching
keys.verify_signature;verify_vp_token'sjwt_vc_jsonlane forwards
require_status=Falselike thedc+sd-jwtandldp_vclanes, so an embedded VC that
carries acredentialStatusis no longer wrongly rejected withStatusUnavailable;
did:webnow requires the resolved document'sidto equal the requested DID (a
missingidno longer skips the binding); and a malformed (non-string)
credentialSchema.digestSRIfails closed instead of silently dropping the integrity
pin. (#101)
Security
did:webvhwitness-policy refusal is no longer bypassable via a non-integer
threshold. A log declaring a witness policy with a float or stringthreshold, or a
witnesseslist with nothresholdat all, slipped past the integer-only fail-closed
gate — so a single compromisedupdateKeycould forge an entry and silently downgrade
a witness-protected DID to the un-witnessed trust model. openvc still cannot verify
witness co-signatures, so any active policy (athresholdof any type that is not an
explicit0/false, or a non-emptywitnesseslist) is now refused.
(#100)- SD-JWT key binding is no longer accepted without a verifier nonce/aud to bind it.
When a verifier setrequire_key_binding=Truebut passed neithernoncenor
audience, the KB-JWT's signature andsd_hashwere checked but not its binding to a
challenge/verifier — so a presentation built for verifier A satisfied verifier B
(replay). Requiring key binding now also requires a non-nullnonceandaudience,
matching VP-JWT's "no unbound mode". (#101) - Codec strictness pass on attacker-controlled bytes. Three hand-rolled decoders were
tightened to their RFC/ISO contracts: the CBOR decoder now rejects duplicate map keys
(RFC 8949 §5.6 / COSE + ISO 18013-5 deterministic encoding) instead of keeping the last;
COSE reads the signaturealgonly from the protected header (RFC 9052 §3.1 — an
algin the unsigned unprotected header is no longer honoured; thex5chainunprotected
fallback is unchanged) and rejects acritheader listing any label it does not
process; and the JCS canonicalizer serialises integers beyond ±2^53 as IEEE-754
doubles (RFC 8785 §3.2.2.3) so a JCS credential with a large integer canonicalizes
identically to other implementations. (#102) - Uniform resource limits on the network and codec surface. The EBSI HTTP client now
streams responses under a size cap and a total wall-clock deadline (it previously
had only a per-socket timeout and no size bound, so a large or slow-drip body from an
allow-listed host could exhaust memory or pin the client); the generaldid:webfetch
gained the same wall-clock deadline (chunkedread1);jwe.decrypt_compactbounds the
token size before decoding;multibasecaps the base58 input length (its decode is
O(n²)) and the multicodec varint length; and the EBSIRetry-Afterheader now honours
the HTTP-date form as well as delta-seconds.
(#103)
v1.19.1
Added
- DID 1.1 / CID 1.0 document tolerance, pinned.
parse_did_documentis context-agnostic
(it reads the document shape, not@context), so DID 1.1 documents — rebased on CID 1.0,
using thehttps://www.w3.org/ns/did/v1.1context, withMultikeyverification methods —
already resolve unchanged. Conformance tests now pin that tolerance (parse + end-to-end
verification through the pipeline) so a future change cannot silently start rejecting DID 1.1
the day issuers emit it. No behaviour change; the relationship-semantics diff is revisited when
DID 1.1 reaches Proposed Recommendation. (#76)
v1.19.0
Added
- ML-DSA (RFC 9964) post-quantum signing + verification — experimental opt-in. New
openvc.keys.MLDSASigningKey(parameter setsML-DSA-44/ML-DSA-65/ML-DSA-87) behind
the sameSigningKeyprotocol — seed-only private keys, the RFC 9964AKPJWK key type,
and external-mu (sign_mu) so an HSM keeps the private-key-never-in-process posture. VC-JWT and
SD-JWT VC issue/verify ML-DSA when a suite is constructed withallow_pq=True; it is
never in the default allow-list — the default suites rejectML-DSA-*before any crypto, and
opting in adds only the three names, never the classic weak algs. Verification routes through the
dependency-lightkeys.verify_signature(not PyJWT);did:jwkcarries anAKPkey unchanged.
Gated behind the new[pq]extra (cryptography>=48built against OpenSSL ≥ 3.5; check
openvc.mldsa_available()) — the core install is unchanged (cryptography>=45+pyjwt).
Experimental: no golden-fixture conformance claim (no stable third-party ML-DSA VC vectors
yet), Data Integrity PQ cryptosuites stay out (W3C FPWD), JOSE-only. Implements
ADR-0004.
Hardened by an adversarial review — no forgery, opt-in bypass or downgrade was achievable
(the default suites rejectML-DSA-*before any crypto) — which also tightened two
fail-closed contract gaps it found: a malformedAKPJWK now raises a typedProofError
(not a bareInvalidKey), and non-finiteexp/nbfare rejected on both the ML-DSA and
the PyJWT paths. (#72)
v1.18.0
Added
- SD-JWT VC issuance can emit an
x5cheader.SdJwtVcProofSuite.issue(..., x5c=[…])places
an X.509 certificate chain (base64 DER, leaf first) in the issuer JWT header, closing the loop
with the existing verify-side support: an issuer anchored on a trusted list (eIDAS / EUDI) can
now be verified in one call —verify_credential(sd_jwt, x5c_trust_anchors=[…])chains the
leaf to those anchors and binds it toiss(previously the anchoring needed a separate
resolve_x5c_keystep). The leaf's key must be the signing key andissmust be in its SAN, or
verification fails closed. Theexamples/11_spanish_university_credential.pywalkthrough now
verifies the FNMT-anchored diploma in a single call.
(#94)