Metadata-Version: 2.4
Name: openvc-core
Version: 0.3.0
Summary: Generic, HSM-friendly Verifiable Credentials core — VC-JWT proofs and DID resolution (key/web) — with an optional read-only EBSI plugin.
Author-email: Luis González Fernández <luisgf@luisgf.es>
License-Expression: LGPL-3.0-or-later
Project-URL: Homepage, https://github.com/luisgf/openvc
Project-URL: Source, https://github.com/luisgf/openvc
Project-URL: Changelog, https://github.com/luisgf/openvc/blob/main/CHANGELOG.md
Keywords: verifiable-credentials,vc-jwt,w3c-vc,did,did-key,did-web,did-ebsi,ebsi,es256,eddsa,ed25519,cryptography,jose
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: COPYING
License-File: COPYING.LESSER
Requires-Dist: cryptography>=42
Requires-Dist: pyjwt>=2.8
Provides-Extra: ebsi
Requires-Dist: httpx>=0.27; extra == "ebsi"
Provides-Extra: data-integrity
Requires-Dist: pyld>=2.0.4; extra == "data-integrity"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: flake8>=7; extra == "dev"
Requires-Dist: gitlint>=0.19; extra == "dev"
Provides-Extra: all
Requires-Dist: openvc-core[data-integrity,dev,ebsi]; extra == "all"
Dynamic: license-file

# 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
  `SigningKey` backend, 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
  raw `R‖S` form (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. The `did:web` fetch 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`.

```sh
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):

```python
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:

```python
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:

```python
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):

```python
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](https://github.com/luisgf/openvc/blob/main/docs/ROADMAP.md) 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

```sh
pip install -e ".[all]"
pytest                        # offline: deterministic, no network
OPENVC_EBSI_LIVE=1 pytest     # also the opt-in live EBSI smoke test
```

## Project

- [Roadmap](https://github.com/luisgf/openvc/blob/main/docs/ROADMAP.md)
- [Changelog](https://github.com/luisgf/openvc/blob/main/CHANGELOG.md)
- [Contributing](https://github.com/luisgf/openvc/blob/main/CONTRIBUTING.md)
  (dev setup, checks, and the commit convention)
- [Security policy](https://github.com/luisgf/openvc/blob/main/SECURITY.md)

## License

LGPL-3.0-or-later. Copyright © 2026 Luis González Fernández.
See [COPYING.LESSER](https://github.com/luisgf/openvc/blob/main/COPYING.LESSER)
and [COPYING](https://github.com/luisgf/openvc/blob/main/COPYING).
