Dependency-light, HSM-friendly Verifiable Credentials core — VC-JWT, SD-JWT VC and Data Integrity proofs, DID resolution, status lists, OpenID4VP — with an optional read-only EBSI plugin.
Project description
openvc
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 | |
| 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
SigningKeyprotocol (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 JOSER‖Sform — 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
cryptographyandpyjwt, nothing else; JSON canonicalization (RFC 8785) and theecdsa-sd-2023CBOR codec are hand-rolled on the stdlib, andpyld/httpxstay behind extras. - Conformance pinned by real vectors.
eddsa-rdfc-2022reproduces the official W3C test vector byte-for-byte;ecdsa-rdfc-2019/ecdsa-sd-2023verify the official vc-di-ecdsa vectors and match their intermediates; ISO 18013-5mso_mdocverifies the Annex D referenceDeviceResponse; 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
- Changelog — every release, with the spec/security reasoning
- Roadmap
- Versioning & deprecation policy
- Contributing — dev setup, checks, commit convention
- Security policy and the threat model
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openvc_core-1.20.3.tar.gz.
File metadata
- Download URL: openvc_core-1.20.3.tar.gz
- Upload date:
- Size: 332.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b20c6371027261e88d5f55eaa840687a94655488955de8728bc00cbd243d1305
|
|
| MD5 |
1af8263b60d96be13291eedc9aa64bc8
|
|
| BLAKE2b-256 |
4c997786df24511e3327ee2af7bb6e87c4a73d67aced43194156ec542f854aea
|
Provenance
The following attestation bundles were made for openvc_core-1.20.3.tar.gz:
Publisher:
ci.yml on luisgf/openvc
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openvc_core-1.20.3.tar.gz -
Subject digest:
b20c6371027261e88d5f55eaa840687a94655488955de8728bc00cbd243d1305 - Sigstore transparency entry: 2194303475
- Sigstore integration time:
-
Permalink:
luisgf/openvc@1add5aa415933a812352b4634c6b64318f015a9d -
Branch / Tag:
refs/tags/v1.20.3 - Owner: https://github.com/luisgf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@1add5aa415933a812352b4634c6b64318f015a9d -
Trigger Event:
push
-
Statement type:
File details
Details for the file openvc_core-1.20.3-py3-none-any.whl.
File metadata
- Download URL: openvc_core-1.20.3-py3-none-any.whl
- Upload date:
- Size: 224.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1234d9f4a3a6689a4b9a03dda20e6f5e8246ebe330fb3b9aae2d3ecc26c64786
|
|
| MD5 |
5d53b394d8299e5a0c0419687a4b2021
|
|
| BLAKE2b-256 |
b20fc228fdadce9118cde56ad6300fb35ca309a9a19a8bf4de0bd247764ab0f7
|
Provenance
The following attestation bundles were made for openvc_core-1.20.3-py3-none-any.whl:
Publisher:
ci.yml on luisgf/openvc
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openvc_core-1.20.3-py3-none-any.whl -
Subject digest:
1234d9f4a3a6689a4b9a03dda20e6f5e8246ebe330fb3b9aae2d3ecc26c64786 - Sigstore transparency entry: 2194303495
- Sigstore integration time:
-
Permalink:
luisgf/openvc@1add5aa415933a812352b4634c6b64318f015a9d -
Branch / Tag:
refs/tags/v1.20.3 - Owner: https://github.com/luisgf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@1add5aa415933a812352b4634c6b64318f015a9d -
Trigger Event:
push
-
Statement type: