Skip to content

Releases: luisgf/openvc

v1.21.0

Choose a tag to compare

@luisgf luisgf released this 19 Jul 10:23

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
    module openvc.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 the x5c primitives 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_certificate binds the WRPRC's sub to 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), and check_request_within_registration
    requires every DCQL credential query to match a registered format whose meta
    covers the requested one, with every requested claim path inside 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-listed alg, the x5c chain,
    a crit that 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[]); exp is optional (Table 10), so the 12-month ceiling
    (GEN-5.2.4-08) binds only when it is present and require_expiry=True is 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) vs intermediary.name (Annex C), and intermediary.sub vs act.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.py pins 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 took claim before claims on both sides, which is
    correct for a registration (the spec's spelling) but on a request let a relying
    party hide a narrow decoy in claim and the real, broader ask in claims — openvc
    authorized the decoy while the wallet, following DCQL, answers claims, 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 bare OverflowError escaping the OpenvcError family on a bignum
    iat/exp/nbf (math.isfinite casts to float, and json.loads yields 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-object meta being
    coerced to {}, which turned a malformed constraint into no constraint and
    widened the entry to every credential of that format; meta matching conflating
    True with 1; and an entitlement floor that accepted any non-empty string while
    ENTITLEMENT_URI_PREFIX sat 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

Choose a tag to compare

@luisgf luisgf released this 18 Jul 20:39

Part of the Q3–Q4 2026 — eIDAS deadline & ecosystem refresh milestone.

Security

  • Unknown JWS crit extensions 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 public verify_status_list_token entry point and the error-precedence ordering included. An adversarial review (parser tricks incl. duplicate / unicode-escaped crit keys, lane completeness across every public entry point, precedence, hostile shapes, verify_many isolation, 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 in docs/audit/assurance.md §5: the PyJWK / PyJWKClient / HMAC-confusion classes are structurally unreachable (allow-list before crypto, no PyJWK(Client), SSRF-guarded JWKS fetch); the crit (CVE-2026-32597) and b64=false (CVE-2026-48525) classes were reachable pre-2.13 on the VC-JWT lane and are closed by the floor plus the openvc-side crit rejection above. cryptography 49.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

Choose a tag to compare

@luisgf luisgf released this 18 Jul 09:55

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.md now 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 stays pyld>=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-byte vc-di-eddsa golden and the ecdsa-sd intermediates included — passes identically on 2.0.4 and 3.1.0, so the permissive floor is deliberately kept and CI now runs a pyld==2.0.4 floor leg alongside the latest-resolving matrix. The bundled-context loader stays openvc's own rather than pyld 3's FrozenDocumentLoader / 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

Choose a tag to compare

@luisgf luisgf released this 16 Jul 08:13

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 enveloped URI="" reference resolving to the trust-list root (plus optional SignedProperties / co-signed ds:KeyInfo). Anchoring on URI="" — 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-Id relocation attack), now pinned by a regression test.
  • Bitstring Status List encodedList is multibase-conformant on both sides (#115). The W3C v1.0 REC mandates a multibase (u-prefixed) encodedList, but the codec used bare base64url: decode_bitstring could not consume a spec-conformant (or any real third-party) list, and encode_bitstring issued non-conformant ones. decode_bitstring now tolerates the u prefix (legacy prefix-less lists still decode) and encode_bitstring emits 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 Bitstring encodedList decode 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

Choose a tag to compare

@luisgf luisgf released this 14 Jul 06:09

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 _sd digest, a VC-JWT
    header/payload, or an enveloped VC's data: payload — made json.loads (or the
    SD-JWT _unpack recursion) raise RecursionError (a RuntimeError, not an
    OpenvcError). On the untrusted peek/unwrap path that escaped verify_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 _unpack now caps recursion at depth 100 (parity with cbor=64 /
    _jcs=100), the did:webvh genesis SCID walk (_deep_replace_scid) is depth-bounded
    too, and every attacker-facing json.loads — SD-JWT, VC-JWT peek/verify, _jws,
    the enveloped unwrap, jwe, and the did:jwk / did:webvh / fetch / status-resolver
    paths — now maps RecursionError to a typed error, so hostile input fails closed and
    verify_many isolates it across all formats. Resolves the R1 residual risk from
    the audit pack. (#117)

v1.20.0

Choose a tag to compare

@luisgf luisgf released this 10 Jul 13:56

[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).
    VerificationPolicy gains require_status_issuer_binding — bind the resolved status
    list's issuer to the credential's issuer — and status_issuer_allowlist for 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 bundled credentials/v2 context 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 injected extra_contexts can'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.resolvers documents openvc.cache.cached_resolve /
    CachingDidResolver for high-volume verifiers (verify_many already 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 no credentialType restriction (an
    unrestricted delegation) was accepted by openvc_ebsi.verify's single-level check but
    rejected at the leaf of the recursive verify_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 MSO validityInfo
    must now carry signed, and signed must 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 unsigned TrustServiceProvider / 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

Choose a tag to compare

@luisgf luisgf released this 10 Jul 13:00

[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 or vp_token whose 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 bare AttributeError, which escaped the
    OpenvcError family and — through verify_many — aborted the entire batch, breaking
    both the fail-closed typed-error contract and the documented per-item isolation.
    peek_issuer / peek_claims and the SD-JWT decoder now reject a non-object
    header/payload with a typed MalformedToken. The same pass closes sibling untyped
    escapes on attacker-controlled input: ecdsa_sd.verify (a hostile proofValue or an
    unknown @context now raise ProofValueMalformed / ProofMalformed instead of a raw
    pyld error), a malformed Ed25519 JWK (ProofMalformed), a lone surrogate in JCS
    (JcsError), and a non-JSON-object EBSI registry 200 (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, which json.loads accepts 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
    (an ES256 header can no longer verify against a P-384 key), matching
    keys.verify_signature; verify_vp_token's jwt_vc_json lane forwards
    require_status=False like the dc+sd-jwt and ldp_vc lanes, so an embedded VC that
    carries a credentialStatus is no longer wrongly rejected with StatusUnavailable;
    did:web now requires the resolved document's id to equal the requested DID (a
    missing id no longer skips the binding); and a malformed (non-string)
    credentialSchema.digestSRI fails closed instead of silently dropping the integrity
    pin. (#101)

Security

  • did:webvh witness-policy refusal is no longer bypassable via a non-integer
    threshold.
    A log declaring a witness policy with a float or string threshold, or a
    witnesses list with no threshold at all, slipped past the integer-only fail-closed
    gate — so a single compromised updateKey could 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 (a threshold of any type that is not an
    explicit 0/false, or a non-empty witnesses list) is now refused.
    (#100)
  • SD-JWT key binding is no longer accepted without a verifier nonce/aud to bind it.
    When a verifier set require_key_binding=True but passed neither nonce nor
    audience, the KB-JWT's signature and sd_hash were 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-null nonce and audience,
    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 signature alg only from the protected header (RFC 9052 §3.1 — an
    alg in the unsigned unprotected header is no longer honoured; the x5chain unprotected
    fallback is unchanged) and rejects a crit header 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 general did:web fetch
    gained the same wall-clock deadline (chunked read1); jwe.decrypt_compact bounds the
    token size before decoding; multibase caps the base58 input length (its decode is
    O(n²)) and the multicodec varint length; and the EBSI Retry-After header now honours
    the HTTP-date form as well as delta-seconds.
    (#103)

v1.19.1

Choose a tag to compare

@luisgf luisgf released this 10 Jul 10:23

Added

  • DID 1.1 / CID 1.0 document tolerance, pinned. parse_did_document is context-agnostic
    (it reads the document shape, not @context), so DID 1.1 documents — rebased on CID 1.0,
    using the https://www.w3.org/ns/did/v1.1 context, with Multikey verification 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

Choose a tag to compare

@luisgf luisgf released this 10 Jul 10:03

Added

  • ML-DSA (RFC 9964) post-quantum signing + verification — experimental opt-in. New
    openvc.keys.MLDSASigningKey (parameter sets ML-DSA-44 / ML-DSA-65 / ML-DSA-87) behind
    the same SigningKey protocol — seed-only private keys, the RFC 9964 AKP JWK 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 with allow_pq=True; it is
    never in the default allow-list — the default suites reject ML-DSA-* before any crypto, and
    opting in adds only the three names, never the classic weak algs. Verification routes through the
    dependency-light keys.verify_signature (not PyJWT); did:jwk carries an AKP key unchanged.
    Gated behind the new [pq] extra (cryptography>=48 built 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 reject ML-DSA-* before any crypto) — which also tightened two
    fail-closed contract gaps it found: a malformed AKP JWK now raises a typed ProofError
    (not a bare InvalidKey), and non-finite exp / nbf are rejected on both the ML-DSA and
    the PyJWT paths. (#72)

v1.18.0

Choose a tag to compare

@luisgf luisgf released this 10 Jul 08:41

Added

  • SD-JWT VC issuance can emit an x5c header. 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 callverify_credential(sd_jwt, x5c_trust_anchors=[…]) chains the
    leaf to those anchors and binds it to iss (previously the anchoring needed a separate
    resolve_x5c_key step). The leaf's key must be the signing key and iss must be in its SAN, or
    verification fails closed. The examples/11_spanish_university_credential.py walkthrough now
    verifies the FNMT-anchored diploma in a single call.
    (#94)