Skip to content

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──► AnvilBase
5. 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") + audit

The 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

Terminal window
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 wallet
const { request_id, request_uri } = await startVerify()
showQrCode(request_uri) // desktop → wallet scans
// or
window.location.href = request_uri // mobile → opens the wallet app

The 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:

Terminal window
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:

  1. Binds the response to your request — the presented nonce must match the stored request’s nonce (single-use, replay-protected).
  2. Checks the credential type is in accepted_credentials.
  3. Enforces the required Level of Assurance (low / substantial / high), read from the token claims (not cryptographically attested).
  4. Checks expiry and extracts the disclosed claims.
  5. Returns the claims with verification_level: "structural", experimental: true, a warning, and checks_performed / checks_skipped lists, plus the X-AnvilBase-eIDAS-Experimental: true header, 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:

LoAUse for
lowlow-risk, basic attribute checks
substantialmost KYC / account-binding flows
highhigh-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:

  1. After account creation, call verify with the claims you need.
  2. Show the QR/deep link; the user presents from their EUDI Wallet.
  3. 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).