-
Notifications
You must be signed in to change notification settings - Fork 0
VC JWT
The VC-JWT suite wraps a W3C credential in a signed JWT
(VC-JOSE-COSE). It is the
EBSI/EUDI-compatible path (ES256) and supports ES384 and EdDSA as well
(plus the RFC 9864 fully-specified name Ed25519 for the latter).
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/ns/credentials/v2"],
"id": "urn:uuid:5f1c-example",
"type": ["VerifiableCredential"],
"issuer": "did:web:issuer.example",
"credentialSubject": {"id": "did:example:alice"},
}
token = suite.sign(credential, signing_key=sk)
# Peek reads the claims WITHOUT verifying — for routing/key discovery only.
print(suite.peek_claims(token)["iss"])
verified = suite.verify(token, public_key_jwk=sk.public_jwk())
print(verified.issuer, verified.subject)suite.verify(..., public_key_jwk=...) is the low-level form for when you
already hold the key. Normally you let the pipeline resolve it from the
issuer DID — verify_credential(token) — see
Resolving issuer keys.
-
The algorithm allow-list runs before any crypto. Only
{ES256, ES384, EdDSA, Ed25519}are accepted (Ed25519is the RFC 9864 fully-specified name for EdDSA — see Versioning & deprecation);alg: none, RS*, and HS* are rejected up front, which closes the classic alg-confusion attacks. -
Unknown
critheader extensions are rejected (RFC 7515 §4.1.11). openvc processes no JWS extension parameters, so a token that marks any as critical fails closed — on the VC-JWT, SD-JWT, KB-JWT and status-list-token lanes alike (the COSE and JWE paths already take the same stance). -
Envelope ↔ credential reconciliation. The JWT claims (
iss,sub,exp,nbf) must agree with the embedded credential (issuer,credentialSubject.id, validity window) — a token cannot smuggle a credential that says something else. -
Temporal checks fail closed.
exp/nbf/iat(with a small clock-skew leeway) and the credential'svalidFrom/validUntil; a present-but-unparseable timestamp rejects.
sign(...) takes anything that implements the SigningKey protocol. The
in-process backends are Ed25519SigningKey (EdDSA), P256SigningKey (ES256),
and P384SigningKey (ES384) from openvc.keys; an HSM/KMS/Vault backend
drops in the same way — see Keys & HSM backends.
One wire-format detail worth knowing even if you never touch it: JOSE ES256
signatures are raw R‖S (64 bytes), never DER. openvc's backends produce
the right form; hand-rolled remote signers often don't (KMS and PKCS#11 return
DER), which is the classic reason a locally-fine token fails at another
verifier.
📖 Published automatically from the wiki/ directory of the main repository — edits made directly on the wiki are overwritten by the next sync. To change a page, open a PR against wiki/. Every python block on these pages is executed by CI. · API reference · LGPL-3.0-or-later
Start
Proof formats
Verifying in practice
- Presentations & OpenID4VP
- Resolving issuer keys
- Relying-party certificates
- Credential schemas
- Status lists
- Trust anchors: EU TL & EBSI
- Async verification
Walkthroughs
Operating
Reference