Skip to content

EU wallet credentials (SD-JWT VC)

EU Digital Identity Wallets (eIDAS 2.0) present credentials in two formats: SD-JWT VC and ISO mdoc. DID.is verifies SD-JWT VC through the same endpoint as other credentials, POST /v1/credentials/verify. ISO mdoc is recognised and reported as UNSUPPORTED; it is never reported as valid.

DID.is acts as a verification service. You receive the credential from the wallet yourself, then send it to DID.is to check. DID.is does not talk to wallets and is not a registered EU relying party.

Wallet holderSD-JWT VC
DID.is checks
  • Issuer signature and key
  • Each disclosed claim matches its signed digest
  • Dates and revocation status
  • Holder key binding, when present
Your serviceResult + what was not checked
  • Structure. The dc+sd-jwt envelope, at most 64 disclosures, nesting depth at most 8, 64 KiB in total.
  • Disclosures. Each disclosed claim must match exactly one SHA-256 digest in the issuer-signed payload, including nested objects and array elements. Duplicate or unreferenced disclosures make the result INVALID. Decoy digests are allowed.
  • Issuer signature. EdDSA, ES256 or ES256K, with the issuer key found in one of three ways (see Where the issuer key comes from).
  • Dates. iat, nbf and exp, with no clock-skew allowance.
  • Revocation. A Token Status List entry (statuslist+jwt) is fetched and checked. If the list cannot be reached or verified, the result is INDETERMINATE, never VALID.
  • Holder key binding. When a Key Binding JWT is attached, it must be signed with the key in cnf.jwk and cover this exact presentation (sd_hash). If you supply expectedNonce and expectedAudience, they must match.
  • EU trusted lists. DID.is does not check whether the issuer appears on an EU or Member State trusted list. Every result says so: issuerTrust.trustedListChecked is always false.
  • Certificate revocation (CRL or OCSP) for x5c certificates.
  • Credential type metadata (vct documents and vct#integrity).
  • Legacy formats. The legacy vc+sd-jwt type and RSA, ES384 or P-384 keys are reported as UNSUPPORTED.

Disclosed values (names, dates of birth, addresses) are never returned. The response lists only the paths of the claims that were disclosed, such as /given_name or /address/locality, and the registered claims iss, vct, iat, nbf, exp and status. The credential field is always null for SD-JWT VC.

Use issuer test credentials when you try the API. Do not send a real person’s credential unless your own data-protection basis covers it.

{
"credential": "<issuer-signed JWT>~<disclosure>~…~<optional Key Binding JWT>",
"expectedAudience": "optional; checked against the Key Binding JWT aud",
"expectedNonce": "optional; checked against the Key Binding JWT nonce",
"trustedIssuerCertificates": ["optional; up to 8 PEM or base64 DER certificates you trust"]
}

expectedNonce is what protects you against a replayed presentation. If you supply expectedNonce or expectedAudience and no Key Binding JWT is attached, the result is INDETERMINATE.

The endpoint uses the intensive budget (10 requests per minute). See Errors and rate limits.

From version 0.2.0 the SDKs accept these options: verifyCredential(credential, { expectedNonce, trustedIssuerCertificates }) in TypeScript and verify_credential(credential, expected_nonce=..., trusted_issuer_certificates=...) in Python. The examples below call the API directly.

Terminal window
# Root certificate as base64 DER (one line)
ROOT=$(openssl x509 -in issuer-root.pem -outform DER | base64 -w0)
curl -X POST https://did.is/api/v1/credentials/verify \
-H 'content-type: application/json' \
-d '{
"credential": "'"$SD_JWT_VC"'",
"expectedAudience": "https://verifier.example",
"expectedNonce": "'"$NONCE"'",
"trustedIssuerCertificates": ["'"$ROOT"'"]
}'

Check a condition without receiving the value

Section titled “Check a condition without receiving the value”

Often you only need a yes or no: is the holder over 18, does the credential carry a given membership. Send requiredClaims and DID.is answers per condition without returning the claim value.

{
"credential": "<SD-JWT VC>",
"expectedNonce": "<your nonce>",
"requiredClaims": [{ "path": "/age_over_18", "equals": true }]
}
"requirements": {
"met": true,
"results": [{ "path": "/age_over_18", "result": "SATISFIED" }]
}
  • path is a JSON Pointer into the issuer-signed claims after disclosures are applied. equals is a boolean, an integer or a non-empty string. Up to 16 conditions; anything else is refused with 400.
  • Each result is SATISFIED, NOT_SATISFIED, NOT_DISCLOSED (the holder did not disclose it, or it is absent) or NOT_EVALUATED.
  • Conditions are evaluated only when the status is VALID. An expired, revoked, tampered or unanchored credential gives NOT_EVALUATED and met: false, so an unverified value can never satisfy a condition.
  • met is true only when the credential is VALID and every condition is SATISFIED. Decide on met, not on status alone.
  • The same option works for other credential formats; the path then points into the signed credential, for example /credentialSubject/ageOver18.

met: true shows that a trusted issuer signed that value. It does not show that the issuer checked it correctly. Add expectedNonce so an old presentation cannot be replayed.

SDK (from 0.2.2): verifyCredential(credential, { requiredClaims: [...] }) in TypeScript and verify_credential(credential, required_claims=[...]) in Python.

DID.is looks for the issuer key in this order. The issuerTrust object in the response states which way was used.

Source When it is used Best possible result issuerTrust.result
DID iss is a DID, or kid is a DID URL VALID NOT_APPLICABLE
X.509 certificate (x5c) The header carries a certificate chain VALID only if the chain reaches a certificate you supplied in trustedIssuerCertificates; otherwise INDETERMINATE CALLER_ANCHORED, NOT_ESTABLISHED or CHAIN_INVALID
Issuer metadata iss is an HTTPS URL that publishes /.well-known/jwt-vc-issuer VALID NOT_APPLICABLE

Most EU wallet credentials use an X.509 certificate. A certificate travels inside the credential and anyone can create one, so a matching signature alone does not say who the issuer is. That is why such a credential is INDETERMINATE until you tell DID.is which root certificates you trust.

What a VALID result shows

  • The issuer-signed part was signed by the issuer key, found through a DID, issuer metadata or a certificate chain that reaches a root you trust.
  • Every disclosed claim is one the issuer signed, unchanged.
  • The credential is within its dates and not revoked or suspended.
  • When key binding is present, the presenter holds the bound key.

What it does not show

  • That the issuer is listed or regulated in the EU. No trusted list is checked.
  • That the attribute values are accurate.
  • That the presentation is fresh, unless you supplied expectedNonce.

Common results for EU wallet credentials:

Result What it means What to do
VALID, CALLER_ANCHORED Signature, disclosures, dates and status passed, and the certificate chain reaches a root you supplied. Apply your own policy on that issuer.
INDETERMINATE, NOT_ESTABLISHED The signature matches the certificate in the credential, but nothing ties that certificate to an issuer you trust. Supply the issuer’s root certificate in trustedIssuerCertificates.
INDETERMINATE (no DNS or URI name) The certificate does not name the issuer in iss, so DID.is cannot confirm it belongs to that issuer. Check with the issuer how its certificate identifies it.
INVALID, CHAIN_INVALID The certificate has expired, is not yet valid, fails path validation, or names a different issuer. Reject the credential. The proof check gives the reason and the date.
INVALID A disclosure does not match a signed digest, or the key binding fails. Reject the credential.
EXPIRED, REVOKED, SUSPENDED The credential is out of date, or its issuer withdrew it. Reject, or ask the holder for a current credential.
UNSUPPORTED ISO mdoc, the legacy vc+sd-jwt type, or an unsupported key type. DID.is draws no conclusion.

Each check in the result has an id, a status and a detail line: structure, disclosures, issuer, proof, issuer.trust, status and holder.binding.

Paste an SD-JWT VC into did.is/verify. The page shows “SD-JWT detected”, an Issuer trust panel and the list of disclosed claim paths. Under Verifier inputs · optional you can add an expected audience, an expected nonce and trusted issuer certificates.

Public test credentials from the EU reference wallet repositories are useful for trying this, but many of them have expired certificates or certificates that do not name their issuer. Expect INVALID or INDETERMINATE from those, with the reason in the proof check.