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

VC-JWT (JOSE)

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).

Sign and verify

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.

What the verifier enforces

  • The algorithm allow-list runs before any crypto. Only {ES256, ES384, EdDSA, Ed25519} are accepted (Ed25519 is 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 crit header 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's validFrom / validUntil; a present-but-unparseable timestamp rejects.

Signing keys

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.

Clone this wiki locally