Skip to content

luisgf/openvc

openvc

PyPI Python versions CI License: LGPL-3.0-or-later

A dependency-light, HSM-friendly Verifiable Credentials core for Python: sign and verify W3C VCs in the three mainstream proof formats, resolve issuer keys, check revocation, and verify wallet presentations — fail-closed by default, with private keys that never have to enter the process.

Capability What is covered Spec
Sign & verify VC-JWT (ES256 / ES384 / EdDSA) VC-JOSE-COSE
SD-JWT VC — selective disclosure, Key Binding, Type Metadata SD-JWT VC
Data Integrity — eddsa-rdfc-2022, ecdsa-rdfc-2019, eddsa-jcs-2022 / ecdsa-jcs-2019 (stdlib JCS, no pyld), and selective-disclosure ecdsa-sd-2023 vc-di-eddsa / vc-di-ecdsa
Verify presentations VP-JWT, Data Integrity challenge/domain, and stateless OpenID4VP 1.0 vp_token — incl. HAIP direct_post.jwt JWE-encrypted responses and the W3C Digital Credentials API (origin-bound) OpenID4VP / HAIP / DC API
EUDI relying-party access certificate (WRPAC) — the X.509 identity of the requester, validated to ACA anchors ETSI TS 119 411-8
EUDI relying-party registration certificate (WRPRC) — the signed JWT/CWT carrying the requester's registered entitlements, cross-checked against its WRPAC and against what the request actually asks for ETSI TS 119 475
Resolve issuer keys did:key, did:jwk, did:web, did:webvh (verifiable-history log) (+ did:ebsi via plugin), /.well-known/jwt-vc-issuer, X.509 x5c chains with SAN issuer binding DID
Revocation Bitstring Status List and Token Status List — check and issue W3C / IETF
Trust anchors Caller-pinned X.509 anchors, EU Trusted Lists (LOTL → national TL), EBSI Trusted Issuers Registry (read-only plugin) ETSI TS 119 612 / EBSI
Keys The SigningKey protocol — an HSM / KMS / Vault backend is a drop-in; ES256 signatures are raw JOSE R‖S, never DER

Install

The PyPI distribution is openvc-core (the import package stays openvc):

pip install openvc-core

The core needs only cryptography and pyjwt. Everything heavier is an extra:

Extra Adds Pulls in
openvc-core[data-integrity] RDF-canonicalized suites (eddsa-rdfc-2022, ecdsa-rdfc-2019, ecdsa-sd-2023) pyld
openvc-core[ebsi] the EBSI registry client httpx
openvc-core[schema] credentialSchema (W3C VC JSON Schema) validation jsonschema
openvc-core[trustlist] XAdES signature verification for EU Trusted Lists signxml
openvc-core[all] everything above + the dev tools

Quick start

Issue a VC-JWT and verify it with the one-call pipeline. verify_credential detects the format (VC-JWT / SD-JWT VC / Data Integrity / enveloped), resolves the issuer key, verifies the proof, and applies policy — types, audience, and fail-closed status:

from cryptography.hazmat.primitives.asymmetric import ed25519

from openvc import VerificationPolicy, verify_credential
from openvc.keys import Ed25519SigningKey
from openvc.multibase import encode_multibase
from openvc.proof.vc_jwt import VcJwtProofSuite

# An issuer key addressed by did:key, so the whole flow runs offline.
private_key = ed25519.Ed25519PrivateKey.generate()
public_raw = Ed25519SigningKey(private_key, kid="_").public_key_raw()
mb = encode_multibase(bytes([0xED, 0x01]) + public_raw)   # multicodec ed25519-pub
issuer = Ed25519SigningKey(private_key, kid=f"did:key:{mb}#{mb}")

token = VcJwtProofSuite().sign({
    "@context": ["https://www.w3.org/ns/credentials/v2"],
    "id": "urn:uuid:2f3a-example",
    "type": ["VerifiableCredential", "ExampleCredential"],
    "issuer": f"did:key:{mb}",
    "credentialSubject": {"id": "did:example:alice", "name": "Ada Lovelace"},
}, signing_key=issuer)

result = verify_credential(
    token, policy=VerificationPolicy(expected_types=["ExampleCredential"]))
print(result.format, result.issuer, result.subject)

Selective disclosure with SD-JWT VC — issue, present with a Key Binding JWT, verify; the holder proves possession of the cnf key and the verifier sees only what was disclosed:

from openvc.keys import Ed25519SigningKey
from openvc.proof.sd_jwt import SdJwtVcProofSuite

issuer = Ed25519SigningKey.generate(kid="https://issuer.example#key-1")
holder = Ed25519SigningKey.generate(kid="holder-key-1")
suite = SdJwtVcProofSuite()

sd_jwt = suite.issue(
    {"iss": "https://issuer.example", "given_name": "Ada", "age": 36},
    signing_key=issuer, disclosable=["given_name", "age"],
    holder_jwk=holder.public_jwk(), vct="https://credentials.example/identity")

presentation = suite.create_presentation(
    sd_jwt, holder_key=holder, audience="https://verifier.example", nonce="n-123")

result = suite.verify(
    presentation, public_key_jwk=issuer.public_jwk(),
    audience="https://verifier.example", nonce="n-123", require_key_binding=True,
    expected_vct="https://credentials.example/identity")
print(result.claims["given_name"], result.key_bound)

Every flow — Data Integrity proofs, VP-JWT and OpenID4VP presentations, status lists, remote HSM signing, EU Trusted Lists, EBSI — has a guide in the wiki and a runnable script in examples/.

Why openvc

  • HSM-first. Signing goes through the SigningKey protocol (alg / kid / sign), so a PKCS#11, AWS KMS, or Vault Transit backend drops in and the private key never enters the process. ES256 signatures are the correct raw JOSE R‖S form — the classic reason a locally-produced token fails elsewhere.
  • Fail-closed by construction. The {ES256, ES384, EdDSA, Ed25519} allow-list runs before any crypto (alg:none, RS*, HS* never reach a verifier); a declared credential status without a resolver rejects; an unparseable timestamp rejects; the JWT envelope is reconciled with the embedded credential.
  • Post-quantum ready (experimental). ML-DSA (RFC 9964, ML-DSA-44/65/87) signs and verifies VC-JWT / SD-JWT VC behind an explicit opt-in (allow_pq=True) and the [pq] extra — first-mover space; no maintained Python VC library signs ML-DSA today. Never a default trust path; the allow-list above is unchanged unless you opt in.
  • SSRF-guarded network. Every issuer-named URL (did:web, well-known, status lists, schemas) goes through an https-only fetch that blocks private/loopback/link-local ranges, refuses redirects, and pins the connection to the validated IP (no DNS rebinding).
  • Dependency-light. The core imports cryptography and pyjwt, nothing else; JSON canonicalization (RFC 8785) and the ecdsa-sd-2023 CBOR codec are hand-rolled on the stdlib, and pyld / httpx stay behind extras.
  • Conformance pinned by real vectors. eddsa-rdfc-2022 reproduces the official W3C test vector byte-for-byte; ecdsa-rdfc-2019 / ecdsa-sd-2023 verify the official vc-di-ecdsa vectors and match their intermediates; ISO 18013-5 mso_mdoc verifies the Annex D reference DeviceResponse; the EBSI client is verified against recorded pilot responses. Golden fixtures are the drift alarm. Beyond them, a test-only VC-API shim runs openvc through the official W3C suites (vc-data-model-2.0, vc-di-eddsa, vc-di-ecdsa, bitstring-status-list) for third-party conformance reports.

Documentation

  • Manual (wiki) — installation, a guide per proof format, presentations & OpenID4VP, issuer-key resolution, status lists, trust (EU Trusted Lists, EBSI), HSM integration, the security model, and the versioning contract.
  • API reference — generated from the docstrings, per module.
  • examples/ — ten runnable, offline scripts covering every flow (they run in CI, so they cannot rot).

Scope

openvc is the generic VC machinery a badge issuer, an EBSI verifier, or a EUDI wallet backend builds on — intentionally not an Open Badges library, a wallet, or a node operator. EBSI support is read-only (resolve did:ebsi, read the trust registries); onboarding/writing is out of scope. The openvc_ebsi plugin depends on openvc, never the reverse.

Project

pip install -e ".[all]"       # from a checkout
pytest                        # offline: deterministic, no network
OPENVC_EBSI_LIVE=1 pytest     # + the opt-in live EBSI smoke test

License

LGPL-3.0-or-later. Copyright © 2026 Luis González Fernández. See COPYING.LESSER and COPYING.

About

Generic, HSM-friendly Verifiable Credentials core — VC-JWT and Data Integrity proofs, DID resolution (key/web/ebsi), status lists — with an optional read-only EBSI plugin.

Topics

Resources

License

LGPL-3.0, GPL-3.0 licenses found

Licenses found

LGPL-3.0
COPYING.LESSER
GPL-3.0
COPYING

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages