eIDAS Verifier (OpenID4VP)
EXPERIMENTAL — structural validation only
This verifier performs structural validation only. It decodes the VP token
without verifying its signature, then checks the nonce binding, credential
type, Level of Assurance, and expiry. It does NOT cryptographically verify
the credential (no SD-JWT issuer-signature check, no key-binding check, no
disclosure-digest check) and does NOT enforce the EU Trust List. The result
self-declares verification_level: "structural" + experimental: true. Do
not use it for production identity assurance. See the
roadmap.
As a Relying Party, your project asks a user’s EUDI Wallet to present a Verifiable Credential. Today the module runs the OpenID4VP request/callback flow and structurally validates the response. Cryptographic verification (signature, enforced issuer trust) is deferred.
Prerequisite: enable eIDAS — this is also the experimental opt-in; the routes are 403 until you do — and configure the verifier (Overview).
The flow
1. App ──POST /v1/eidas/{id}/verify──► AnvilBase (start a presentation request)2. AnvilBase: store the request nonce (single-use, TTL) ──► request (request_id, nonce)3. App ──► EUDI Wallet (user approves, wallet builds a VP token)4. Wallet ──VP token──► App ──POST /v1/eidas/{id}/verify/callback──► AnvilBase5. AnvilBase: look up the STORED request by request_id → compare presented nonce against the STORED nonce → structural checks (type, LoA, expiry)6. AnvilBase ──► structural credential data (verification_level: "structural") + auditThe callback validates the wallet’s nonce against the stored nonce from the
original request (not a freshly generated one), so a legitimate response now
completes the flow end-to-end. The stored request is single-use — replaying
the same request_id fails.
1. Start a verification
curl -X POST "http://localhost:39001/v1/eidas/<project_id>/verify" \ -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \ -d '{ "credential_type": "eu.europa.ec.eudi.pid", "required_claims": ["given_name", "family_name", "birth_date"] }'The response contains a presentation request with a request_id and a nonce.
Hand the request to the user’s wallet — typically by rendering it as a QR code or a
deep link (openid4vp://...) that the EUDI Wallet app opens.
2. Deliver the request to the wallet
// Pseudocode: render the presentation request for the walletconst { request_id, request_uri } = await startVerify()showQrCode(request_uri) // desktop → wallet scans// orwindow.location.href = request_uri // mobile → opens the wallet appThe wallet authenticates the user, lets them consent to sharing the requested claims, and produces a VP token.
3. Complete the verification (callback)
The wallet returns the VP token to your app; submit it to the callback with the
original request_id:
curl -X POST "http://localhost:39001/v1/eidas/<project_id>/verify/callback" \ -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \ -d '{ "request_id": "<request_id>", "vp_token": "<vp-token-from-wallet>" }'AnvilBase then performs structural checks only:
- Binds the response to your request — the presented nonce must match the stored request’s nonce (single-use, replay-protected).
- Checks the credential type is in
accepted_credentials. - Enforces the required Level of Assurance (
low/substantial/high), read from the token claims (not cryptographically attested). - Checks expiry and extracts the disclosed claims.
- Returns the claims with
verification_level: "structural",experimental: true, awarning, andchecks_performed/checks_skippedlists, plus theX-AnvilBase-eIDAS-Experimental: trueheader, and writes an audit entry.
Not performed (yet): SD-JWT/mdoc issuer-signature verification, holder
key-binding verification, selective-disclosure digest verification, and enforced
EU Trust List validation. The trust_list_verified field is a best-effort,
non-blocking lookup against a single reference issuer — a false does not fail
the response. See checks_skipped and the
roadmap.
A successful response returns the claims the token carried — but because the signature is not verified, treat these as unattested until the cryptographic core ships.
Response shape
{ "experimental": true, "verification_level": "structural", "warning": "structural validation only — not cryptographic credential verification; do not use for production identity assurance", "credential_type": "eu.europa.ec.eudi.pid", "issuer": "https://issuer.eudiw.dev", "loa": "high", "claims": { "given_name": "Max", "family_name": "Mustermann" }, "trust_list_verified": false, "checks_performed": ["jwt_decode", "nonce_binding", "credential_type", "level_of_assurance", "expiry", "issuer_lookup_advisory", "claims_extraction"], "checks_skipped": ["sd_jwt_issuer_signature", "key_binding_jwt", "disclosure_digests", "enforced_trust_list", "mdoc_support"]}Level of Assurance
required_loa rejects presentations below the bar you set:
| LoA | Use for |
|---|---|
low | low-risk, basic attribute checks |
substantial | most KYC / account-binding flows |
high | high-risk (financial, government, health) |
A credential that doesn’t meet the required LoA fails verification.
Credential formats
- SD-JWT-VC — selective-disclosure JWT credentials (the common EUDI format). The module currently decodes these (no signature/disclosure-digest verification).
- mdoc — ISO 18013-5 mobile documents. Not yet supported (deferred).
You accept specific types via accepted_credentials.
Trust List
EU Trust List enforcement is not yet implemented. The trust_list_url is
configurable, but the issuer check is a best-effort, non-blocking lookup
against a single hardcoded reference issuer (trust_list_verified reflects only
this lookup and does not gate the response). Enforced Trust List validation is on
the roadmap. Until then, the
attributes are not legally meaningful.
Putting it together
A typical “verify identity at sign-up” integration — for prototyping only while the module is experimental:
- After account creation, call
verifywith the claims you need. - Show the QR/deep link; the user presents from their EUDI Wallet.
- On the callback you receive structurally-validated claims. Until the cryptographic core ships, do not treat this as proof of identity or mark a user “verified” for any production trust decision.
Privacy & data minimization
- Request the minimum claims (
required_claims) — selective disclosure means you only receive what you ask for. - Store the verification result and a minimal claim set, not the raw credential, unless you have a legal basis to retain more.
- All verifications are audit-logged for your compliance record.
Next: Issuer (OpenID4VCI).