Generic, HSM-friendly Verifiable Credentials core — VC-JWT proofs and DID resolution (key/web) — with an optional read-only EBSI plugin.
Project description
openvc
A small, dependency-light Verifiable Credentials core for Python: sign and
verify credentials in three proof formats — VC-JWT (JOSE), SD-JWT VC
(selective disclosure), and Data Integrity (eddsa-rdfc-2022 and the
selective-disclosure ecdsa-sd-2023) — resolve DIDs (did:key, did:web),
check
status-list revocation, and — via an optional plugin — verify against the
EBSI trust registries. Designed so private keys can live behind an
HSM/Vault and never enter the process.
It is intentionally not an Open Badges library: openvc is the generic VC
machinery that a badge issuer (or an EBSI verifier, or a EUDI wallet backend)
builds on. It never imports anything upward.
Why
- VC-JWT first, HSM-friendly. Signing delegates the raw signature to a
SigningKeybackend, so a PKCS#11 / Vault Transit key is a drop-in — the private key never has to be in-process. ES256 signatures are the correct JOSE rawR‖Sform (the classic reason a locally-produced token fails elsewhere). - Safe by construction. The verifier pins an algorithm allow-list
(
ES256,EdDSA) before any crypto runs, and reconciles the JWT envelope with the embedded credential. Thedid:webfetch and the EBSI client both guard against SSRF. - Version drift, contained. EBSI ships versioned registries whose response shapes change; every version specific lives behind one adapter, so the domain model and trust logic never see wire formats.
Layout
src/openvc/ core — knows nothing about EBSI or badges
keys.py Ed25519 (EdDSA) & P-256 (ES256) SigningKey backends
multibase.py base58btc multibase + multicodec varint
proof/vc_jwt.py VcJwtProofSuite: peek / verify / sign
proof/sd_jwt.py SdJwtVcProofSuite: issue / present (key binding) / verify
proof/data_integrity.py DataIntegrityProofSuite: eddsa-rdfc-2022 (needs pyld)
proof/ecdsa_sd.py EcdsaSdProofSuite: ecdsa-sd-2023 selective disclosure
proof/contexts/ bundled JSON-LD contexts + offline document loader
did/base.py DidDocument, resolver protocol, W3C parser, registry
did/did_key.py offline did:key (Ed25519, P-256)
did/did_web.py did:web -> https -> fetch (fetch is injected)
fetch.py SSRF- + DNS-rebinding-safe https JSON fetch for did:web
status/ status lists — W3C Bitstring + IETF Token Status List
src/openvc_ebsi/ optional EBSI plugin (read-only); depends on openvc only
http.py EbsiHttpClient: TTL cache, retries, host allow-list
versioning.py DID Registry / TIR version adapters + DidEbsiResolver
trust.py recursive TI->TAO->RootTAO trust-chain verification
verify.py verify_ebsi_badge: signature + trust + revocation
models.py Accreditation, IssuerRecord (version-agnostic domain)
Dependency rule: openvc imports nothing upward. openvc_ebsi depends on
openvc, never the reverse.
Install
The PyPI distribution is openvc-core; the Python import package is
openvc — so pip install openvc-core, then import openvc.
pip install openvc-core # core: VC-JWT, did:key, did:web, status list
pip install "openvc-core[ebsi]" # + the EBSI registry client (httpx)
pip install "openvc-core[data-integrity]" # + eddsa-rdfc-2022 Data Integrity (pyld)
pip install -e ".[all]" # everything + dev tools (from a checkout)
Quick start
Issue and verify a VC-JWT with an in-process key (swap for an HSM backend in production):
from openvc.keys import P256SigningKey
from openvc.proof.vc_jwt import VcJwtProofSuite
sk = P256SigningKey.generate(kid="did:web:issuer.example#key-1")
suite = VcJwtProofSuite()
credential = {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"id": "urn:uuid:...",
"type": ["VerifiableCredential"],
"issuer": "did:web:issuer.example",
"credentialSubject": {"id": "did:key:z6Mk..."},
}
token = suite.sign(credential, signing_key=sk)
verified = suite.verify(token, public_key_jwk=sk.public_jwk())
print(verified.issuer, verified.subject)
Resolve a did:web with the SSRF-guarded fetch, then verify against its key:
from openvc.fetch import default_did_web_resolver
resolver = default_did_web_resolver() # https-only, blocks private ranges
doc = resolver.resolve("did:web:issuer.example")
vm = doc.key_by_kid("did:web:issuer.example#key-1")
verified = suite.verify(token, public_key_jwk=vm.public_key_jwk)
EBSI (read-only) — resolve a did:ebsi and check issuer trust:
from openvc.proof.vc_jwt import VcJwtProofSuite
from openvc_ebsi.http import for_ebsi
from openvc_ebsi.versioning import DidEbsiResolver
from openvc_ebsi.verify import verify_ebsi_badge
suite = VcJwtProofSuite()
with for_ebsi("pilot") as http:
resolver = DidEbsiResolver(http.get_json, decode_jwt=suite.peek_claims)
result = verify_ebsi_badge(token, resolver=resolver, proof_suite=suite,
expected_types=["VerifiableAttestation"])
print(result.trusted, result.issuer)
SD-JWT VC — issue with selective disclosure, then verify a holder presentation
(the holder proves possession of the cnf key and reveals only what it chooses):
from openvc.keys import Ed25519SigningKey
from openvc.proof.sd_jwt import SdJwtVcProofSuite
issuer = Ed25519SigningKey.generate(kid="did:web:issuer.example#key-1")
holder = Ed25519SigningKey.generate(kid="did:key:zHolder#0")
suite = SdJwtVcProofSuite()
sd_jwt = suite.issue(
{"iss": "did:web:issuer.example", "vct": "https://credentials.example/id",
"given_name": "Ada", "age": 36},
signing_key=issuer, disclosable=["given_name", "age"],
holder_jwk=holder.public_jwk(),
)
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)
print(result.claims["given_name"], result.key_bound)
Status
Alpha. The proof suites (VC-JWT, SD-JWT VC, and Data Integrity —
eddsa-rdfc-2022, verified byte-for-byte against the official W3C vc-di-eddsa
vector, plus the selective-disclosure ecdsa-sd-2023), the key
backends, DID resolution (did:key, did:web, did:ebsi read), the EBSI
registry client (verified against recorded pilot fixtures and a live smoke test),
the recursive TI→TAO→RootTAO trust chain (with per-hop delegation scoping and
revocation of the accreditations themselves), and status-list revocation in both
the W3C Bitstring and IETF Token Status List encodings are implemented and tested
offline. See
the roadmap for
what is next (byte-level interop validation of ecdsa-sd-2023 against the W3C
test vectors).
did:ebsi write/onboarding (JSON-RPC + OID4VP) is out of scope — this is a
verifier/issuer library, not a node operator.
Tests
pip install -e ".[all]"
pytest # offline: deterministic, no network
OPENVC_EBSI_LIVE=1 pytest # also the opt-in live EBSI smoke test
Project
- Roadmap
- Changelog
- Contributing (dev setup, checks, and the commit convention)
- Security policy
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-0.3.0.tar.gz.
File metadata
- Download URL: openvc_core-0.3.0.tar.gz
- Upload date:
- Size: 83.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6465baa0e1cc2f3a14222b9c243704cd97be3ccabfabdb11a54597106636694f
|
|
| MD5 |
92ddf32910eb2315c01856eaa1cf168a
|
|
| BLAKE2b-256 |
2b6fb96bfa1891ec9f01717e951ca15a83d44a0b88a72041e6f788219507ebb5
|
Provenance
The following attestation bundles were made for openvc_core-0.3.0.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-0.3.0.tar.gz -
Subject digest:
6465baa0e1cc2f3a14222b9c243704cd97be3ccabfabdb11a54597106636694f - Sigstore transparency entry: 2082555239
- Sigstore integration time:
-
Permalink:
luisgf/openvc@149a66c59e2ef9d6d7846ee55f7c1c09f088c694 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/luisgf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@149a66c59e2ef9d6d7846ee55f7c1c09f088c694 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openvc_core-0.3.0-py3-none-any.whl.
File metadata
- Download URL: openvc_core-0.3.0-py3-none-any.whl
- Upload date:
- Size: 75.3 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 |
ca8eb3efcd328eef9e5d355d2e4ceeab9cf311ef46433f334386671e47957e55
|
|
| MD5 |
5863b74bd1e13c9bc44147d4c7258dee
|
|
| BLAKE2b-256 |
12066cc3956bc692ecc35f1c28bca736211f309d310a894cf8818c63bb055582
|
Provenance
The following attestation bundles were made for openvc_core-0.3.0-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-0.3.0-py3-none-any.whl -
Subject digest:
ca8eb3efcd328eef9e5d355d2e4ceeab9cf311ef46433f334386671e47957e55 - Sigstore transparency entry: 2082555262
- Sigstore integration time:
-
Permalink:
luisgf/openvc@149a66c59e2ef9d6d7846ee55f7c1c09f088c694 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/luisgf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@149a66c59e2ef9d6d7846ee55f7c1c09f088c694 -
Trigger Event:
push
-
Statement type: