Skip to content
github-actions[bot] edited this page Jul 8, 2026 · 4 revisions

Trust anchors: X.509, EU Trusted Lists, EBSI

A verified signature only proves someone signed — trust is deciding who may vouch. openvc keeps that decision caller-pinned: you hand it the anchors; it never ships an implicit root.

X.509 (x5c) anchors

For tokens that carry their certificate chain, pass the roots you trust; openvc validates the path and binds the leaf's SAN to the iss, so a valid-but-unrelated certificate cannot vouch for an arbitrary issuer:

result = verify_credential(token, x5c_trust_anchors=my_trusted_roots)

EU Trusted Lists (eIDAS / EUDI)

openvc.trustlist turns the European Commission's List of Trusted Lists and the national Trusted Lists it points at (ETSI TS 119 612) into X.509 anchors for that same x5c path — closing the gap between "the chain is internally valid" and "the chain roots in an EU-recognised anchor, granted now, for the right service type":

from openvc import verify_credential
from openvc.trustlist import verify_xades_enveloped, walk_lotl   # [trustlist] extra

anchors = walk_lotl(
    LOTL_URL,
    lotl_signer_certs=commission_certs,       # caller-pinned: the Commission's keys
    verify_signature=verify_xades_enveloped,  # XAdES check on every list (fail-closed)
)
result = verify_credential(token, x5c_trust_anchors=anchors.certificates)

Properties worth knowing:

  • Trust is caller-pinned — you supply the LOTL signer certificates; there is no implicit root.
  • Fail-closed — a national TL that cannot be fetched, verified, or is expired contributes zero anchors and is recorded in anchors.problems, never silently trusted.
  • Selective — the default selection keeps granted qualified-CA services; pass select=None for everything.
  • Hardened XML — stdlib parsing with DTD/DOCTYPE rejected (no XXE, no entity-expansion bombs), size-bounded input; XAdES verification lives behind the [trustlist] extra (signxml) and pins the signer to the certs the parent list vouched for.

Design rationale: ADR-0003.

EBSI (read-only plugin)

openvc_ebsi resolves did:ebsi and reads the Trusted Issuers Registry, then verifies the full accreditation chain — TI → TAO → RootTAO — recursively, with per-hop delegation scoping and revocation of the accreditations themselves:

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)
  • Read-only by design: resolve DIDs, read the registries. Onboarding / writing (JSON-RPC + OID4VP) is out of scope.
  • SSRF-guarded: the EBSI HTTP client is https-only with a host allow-list, short client-side TTL caching, and bounded retries (ADR-0001). Never resolve did:web through it — did:web has its own guarded fetch (see Resolving issuer keys).
  • Version drift, contained: every EBSI API version specific lives behind one adapter in openvc_ebsi.versioning; the trust logic never sees wire formats. Conformance is pinned by recorded pilot fixtures, plus an opt-in live smoke test (OPENVC_EBSI_LIVE=1 pytest).

Clone this wiki locally