Credential schema
Opt-in credentialSchema (W3C VC JSON Schema) validation for the verification
pipeline: fetch the JSON Schema a credential declares and validate the whole
credential against it. Both W3C schema types are handled — a raw JsonSchema, and
a JsonSchemaCredential (the schema wrapped in its own signed VC, whose proof is
verified through the pipeline before its embedded schema is applied). Wired into
verify_credential via resolve_credential_schema=.
openvc.schema
openvc.schema — validate a credential against its credentialSchema (W3C VC
JSON Schema).
A credential may declare one or more JSON Schemas it claims to conform to via the
credentialSchema property (VCDM 2.0 + the Verifiable Credentials JSON Schema
Specification). The JsonSchema type points id at a URL that dereferences
to a JSON Schema; the whole credential document is then validated against it.
This check is opt-in, not fail-closed by default. Unlike revocation status,
schema conformance is a data-shape property, not a security gate: a
signature-valid credential whose shape violates its schema is a malformed issue,
not a forgery. So the pipeline validates only when the caller supplies a
resolve_credential_schema fetch. Set policy.require_schema=True to make a
credential that declares a credentialSchema but is verified without a
resolver fail (symmetric with require_status). Once you do opt in, every
part of the check is fail-closed — an unreachable schema, a resource that is not a
valid JSON Schema, an unsupported schema type, or a validation error all raise.
Transport is the caller's concern. resolve_credential_schema takes a URL
and returns the parsed JSON Schema resource as a dict; pass
:func:openvc.fetch.https_json_fetch for the SSRF-guarded one (the same guard
did:web uses). This module never opens a socket. Remote $ref resolution
inside a fetched schema is off: the validator is built with an empty
referencing.Registry (no retrieve hook), so a remote $ref fails closed as
:class:SchemaResolutionError with no network call — whereas jsonschema's
default registry would urllib.urlopen an unresolvable remote $ref, an
SSRF vector, since the schema body is attacker-influenced. Local (#/…) refs
still resolve against the schema document itself.
Security caveat — untrusted schemas can be a ReDoS vector. A fetched schema
is attacker-influenced (the issuer names credentialSchema.id). JSON Schema
pattern keywords run on Python's backtracking re engine, so a schema
carrying a catastrophic-backtracking regex can burn unbounded CPU during
validation. There is no clean dependency-light in-process bound (a hard timeout
needs a subprocess; a linear-time engine needs re2); a very deep schema is
capped only by the recursion limit (surfaced as :class:SchemaResolutionError).
Point resolve_credential_schema at schema hosts you trust, or bound the work
in the injected fetch. Validation being opt-in limits exposure.
Dependency-light: the JSON Schema processor (jsonschema) lives behind the
[schema] extra and is imported lazily. Without it, a credential that requires
schema validation raises :class:SchemaBackendUnavailable — the rest of the
library keeps working.
Both W3C schema types are validated. A JsonSchema entry dereferences to a raw
JSON Schema. A JsonSchemaCredential entry dereferences to the schema wrapped
in its own signed Verifiable Credential: its proof is verified through the same
pipeline (an injected verify_inner — this module never imports
:mod:openvc.verify, so no import cycle) before its verified
credentialSubject.jsonSchema is applied to the outer credential. That
recursion is bounded: the schema-defining VC's own credentialSchema is
not re-fetched (the proof is the trust anchor), so a hostile chain of schema-VCs
cannot loop. Standalone :func:validate_credential_schema still raises
:class:UnsupportedSchemaType for a JsonSchemaCredential unless a
verify_inner is supplied — the pipeline always supplies one, so a declared
schema is never silently skipped when you asked for it to be checked.
Spec notes (W3C VC JSON Schema, CR Draft 2025-02-04):
- The whole credential document is the validation instance (schemas key into
credentialSubjectat their own top level); we never pre-extract the subject. - The dialect follows the schema's
$schema(draft 2020-12 is the required one). A resource with no$schema"MUST NOT be processed" — we reject it. formatkeywords (e.g.email) are treated as annotations, not assertions — JSON Schema's default; we do not pull in format libraries.digestSRIon an entry is enforced: the resolver returns the raw schema bytes, and asha256-/sha384-/sha512-SRI hash is verified over them (constant-time) before the schema is parsed — a mismatch fails closed. An issuer can thus pin the exact schema so even a compromised schema host cannot swap it.- The spec models the outcome as Success / Failure / Indeterminate; this
fail-closed verifier collapses Indeterminate (unreachable / non-schema /
unsupported) into a raised :class:
SchemaErrorrather than a distinct value.
SchemaError
Bases: OpenvcError
Base class for every credentialSchema validation failure.
Source code in src/openvc/schema.py
113 114 | |
SchemaBackendUnavailable
Bases: SchemaError
Schema validation was required but the jsonschema processor is not
installed (pip install openvc-core[schema]).
Source code in src/openvc/schema.py
117 118 119 | |
SchemaUnavailable
Bases: SchemaError
The credential declares a credentialSchema but no
resolve_credential_schema was supplied and require_schema is set
(fail-closed, symmetric with :class:~openvc.verify.StatusUnavailable).
Source code in src/openvc/schema.py
122 123 124 125 | |
SchemaResolutionError
Bases: SchemaError
The schema could not be fetched, or the fetched resource is not a usable JSON Schema (bad transport, not JSON, or not a valid schema).
Source code in src/openvc/schema.py
128 129 130 | |
UnsupportedSchemaType
Bases: SchemaError
A declared credentialSchema entry has a type this verifier cannot
validate (e.g. JsonSchemaCredential).
Source code in src/openvc/schema.py
133 134 135 | |
SchemaValidationError
Bases: SchemaError
The credential does not conform to a declared JSON Schema.
Source code in src/openvc/schema.py
138 139 | |
CredentialSchemaRef
dataclass
One parsed credentialSchema entry.
Source code in src/openvc/schema.py
146 147 148 149 150 151 | |
SchemaValidationResult
dataclass
The outcome of validating a credential against its declared schema(s).
Source code in src/openvc/schema.py
154 155 156 157 158 | |
parse_credential_schemas(credential)
Parse the credential's credentialSchema property into typed refs.
Each entry needs an id (the schema URL) and a type. The recognised
type (JsonSchema / JsonSchemaCredential) is kept; an entry whose type
is none of those is preserved with its first declared type so the caller can
decide (the pipeline treats an unknown type as unsupported when opted in).
Source code in src/openvc/schema.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
validate_credential_schema(credential, *, resolve_credential_schema, verify_inner=None)
Validate credential against every schema it declares (JsonSchema and
JsonSchemaCredential).
Fetches each declared schema via resolve_credential_schema and validates the
whole credential document against it. A JsonSchemaCredential entry
dereferences to a signed VC whose proof is verified with verify_inner before
its embedded schema is applied; without a verify_inner such an entry raises
:class:UnsupportedSchemaType (the :func:openvc.verify.verify_credential
pipeline always injects one). Raises :class:SchemaValidationError on a
mismatch, :class:SchemaResolutionError if a schema cannot be fetched/verified
or is not a valid JSON Schema, and :class:UnsupportedSchemaType for an
unrecognised schema type. Returns a :class:SchemaValidationResult recording
which schema URLs were applied (which may be empty if the credential declares
no credentialSchema).
Source code in src/openvc/schema.py
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | |
validate_credential_schema_async(credential, *, resolve_credential_schema, verify_inner=None)
async
Async :func:validate_credential_schema — awaits an async
resolve_credential_schema (and, for a JsonSchemaCredential, an async
verify_inner). Identical validation and fail-closed semantics; the
JSON-Schema validation itself is the same sync CPU code.
Source code in src/openvc/schema.py
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 | |
SD-JWT VC Type Metadata
Resolve the Type Metadata a credential's vct points to, pin it with vct#integrity
(W3C SRI), walk the extends chain, and validate the disclosed claims against the
type's claims metadata. Opt-in fetch via
openvc.resolvers.default_type_metadata_resolver.
openvc.type_metadata
openvc.type_metadata — SD-JWT VC Type Metadata (draft-ietf-oauth-sd-jwt-vc-17 §4).
Verifier side: resolve the Type Metadata a credential's vct points to, pin it with
vct#integrity (a W3C Subresource-Integrity hash), enforce metadata.vct equals
the credential's vct, walk the extends chain (parent before child, each
integrity-pinned, cycle-/depth-bounded), compose the inherited claim metadata, and
validate the processed SD-JWT payload against it — the DCQL-style path engine plus
mandatory. Fetch is opt-in and every failure is fail-closed (§4.7: "If
claim metadata processing or validation fails, the SD-JWT VC MUST be rejected").
Two scope notes tied to the current draft:
- Embedded JSON Schema was removed from Type Metadata in draft-12 — there is no
schema/schema_urimember and no schema dialect. Payload validation is done through theclaimsarray, not a JSON Schema. - The per-claim
sd(selective-disclosure) constraint is an issuance-time rule; a verifier can only check it against per-claim disclosure provenance, which the SD-JWT layer does not currently expose, so it is not enforced here (path structure andmandatorypresence are). This is a documented boundary, not a silent skip.
Reuses :func:openvc.schema._verify_sri (the reviewed, constant-time SRI check) and,
via the caller-supplied resolver, :func:openvc.fetch.https_bytes_fetch — so the fetch
inherits the SSRF / size / time guards. Type Metadata URLs are as untrusted as
did:web; never resolve them through the EBSI client.
TypeMetadataError
Bases: OpenvcError
Base class for SD-JWT VC Type Metadata failures.
Source code in src/openvc/type_metadata.py
58 59 | |
TypeMetadataResolutionError
Bases: TypeMetadataError
The Type Metadata could not be fetched, parsed, integrity-checked, or the
extends chain cycles / is too deep.
Source code in src/openvc/type_metadata.py
62 63 64 | |
TypeMetadataMismatch
Bases: TypeMetadataError
A resolved document's vct differs from the credential's vct.
Source code in src/openvc/type_metadata.py
67 68 | |
TypeMetadataClaimsInvalid
Bases: TypeMetadataError
The processed SD-JWT payload does not satisfy the composed claim metadata.
Source code in src/openvc/type_metadata.py
71 72 | |
TypeMetadataResult
dataclass
The outcome of Type Metadata processing for a verified SD-JWT VC.
Source code in src/openvc/type_metadata.py
75 76 77 78 79 80 | |
validate_type_metadata(payload, *, vct, vct_integrity=None, resolve, max_extends_depth=_MAX_EXTENDS_DEPTH)
Resolve and enforce the Type Metadata for a verified SD-JWT VC's payload.
vct / vct_integrity are the credential's vct and vct#integrity claims
(from the processed payload). resolve fetches a Type Metadata document by URL
(opt-in — pass e.g. :func:openvc.resolvers.default_type_metadata_resolver). Fails
closed: an integrity mismatch, a document whose vct differs, an extends
cycle / over-depth, or a claim-metadata violation each raise a
:class:TypeMetadataError.
Source code in src/openvc/type_metadata.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |