A verifier, and a way to measure it
The walkthrough shows what the EUDI Wallet feels like. This page is what was built underneath it, and why — a wallet running on real browser primitives, and a labelled corpus of the ways a presentation can lie.
Diagnosis
By December 2027, every EU bank — including Lara Bank's Irish subsidiary — must accept EUDI Wallet credentials wherever it requires strong customer authentication. The UK is building the same capability on a different trust stack: GOV.UK Wallet issuing state credentials, DIATF/DVS-certified private providers verifying them. Both regimes make the bank a relying party whose core new component is a verifier: software that takes a credential presentation and decides accept/reject against a trust chain.
Almost nobody in a bank product organisation can say what that verifier actually checks, byte by byte. Vendor pitches describe the wallet; regulation describes the obligation; neither describes the thirteen distinct ways a presentation can be defective and what a correct verifier does about each. That gap is the learning target.
The instrument: what replaces the eval
Bio-authn had a measurable substrate — matcher scores, thresholds, ROC curves. Credentials and signatures are deterministic; there is no accuracy curve. The eval discipline survives anyway, transposed:
Build a corpus of presentations where the defects are controlled and labelled, point the verifier at it, and score accept/reject per defect species. "Attack presentation" becomes "tampered credential." The confusion matrix per species is the headline result — the APCER/BPCER analogue. A verifier that has never been run against a hostile corpus is exactly as trustworthy as a liveness check that has never seen a printed photo.
This framing is the publishable insight: relying-party verification logic is an evaluable surface, and by late 2027 it is a regulated gate at every EU bank. Nobody is writing about it that way yet.
The attestation wall
A wallet you can install from this site, holding a non-extractable key and gated by your device's own biometrics. It is still not an EUDI Wallet Unit, and the exact point where it stops being one is the lesson.
The claim being tested
This PWA holds a genuine non-extractable P-256 key (portal/lib/wallet/crypto.ts), gates every release behind a real WebAuthn platform-authenticator gesture, produces a real KB-JWT holder binding, and completes a real (if local) cross-device OpenID4VP-lite presentation that the verifier accepts. Does that make it an EUDI Wallet Unit?
No. Not because any of it is fake — it isn't — but because none of it reaches the specific bar the ARF sets, and that gap is structural, not a missing feature left unbuilt.
What's real here
Worth stating plainly, because the wall means nothing if the rest is hand-waved:
- Non-extractable key.
crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, false, ["sign"])— thefalseisextractable.components/wallet/KeyExportDemo.tsxcallscrypto.subtle.exportKey("jwk", privateKey)against that key and it throwsInvalidAccessError: ... key is not extractable, live, on a button press. That's not a claim — it's a demonstrated failure, verified in this build against a real Chrome instance. - WebAuthn-gated release.
lib/wallet/unlock.ts'sauthorize()realizescontracts/wallet_unlock.py's frozenWalletUnlockProvidershape in the browser:navigator.credentials.get()is the gesture, a cancelled or failed assertion returns{authorized: false}, and — verified via network inspection during this build — the wallet never callssdjwt.presentor POSTs anything when denied. Not merely "declines to send the result": no presentation is ever built. - Real KB-JWT holder binding.
lib/wallet/sdjwt.ts'spresent()is a line-for-line re-expression ofissuer/sdjwt.py's presentation logic — the samesd_hash, the same nonce/aud binding, verified against the same verifier. - A real cross-device presentation. The two-tab (QR) flow in
app/verify-demo/andapp/wallet/present/was driven end to end during this build: a separate "verifier" tab renders a live authorization request as a QR, a separate "wallet" tab scans it, presents, and the verifier tab — coordinating only through the FastAPI relay (service/main.py's/present+/present/{nonce}) — renders a realACCEPTwith all eight checks passing.
The wall, precisely
Three separately-named gaps, each a specific ARF term this PWA cannot produce:
No WSCD. The ARF's Wallet Secure Cryptographic Device is certified hardware — a secure element, an eSIM, an external smartcard — where keys physically live and signing physically happens, keys never leaving it. extractable: false is a genuine, useful property, but it's a software promise enforced by the browser's WebCrypto implementation, not hardware-backed key isolation with a certification trail behind it. The practical difference: an OS-level compromise (malware running with the same user's privileges) cannot export this key, but it can still invoke it — ask the browser process to sign something, exactly as the legitimate wallet code does. A WSCD is certified specifically against that stronger threat model, not just against export.
No WUA. Nothing this build produced attests the wallet app's own provenance or integrity to anyone. Look at service/main.py's POST /issue handler: it verifies the wallet's key-proof JWT (proof of possession of the holder key) and then issues a PID — full stop. A real PID Provider's issuance flow additionally requires a Wallet Unit Attestation before that point: a signed statement, from a certified Wallet Provider, that this Wallet Unit is genuine and backed by a real WSCD. This build's /issue endpoint has no such check to perform, because there is no WUA to check — that absence is the literal, concrete place a real issuer would refuse this wallet.
No certification. No CIR-defined conformance or security assessment has run against any of this code. The Wallet Solution as a whole — not just one key or one flow — is what the ARF requires certified before an issuer will provision a genuine PID to it.
What a certified Wallet Unit does instead
Keys live in a certified WSCD from the moment of provisioning, managed by a WSCA the certification regime has also assessed. At issuance, the wallet presents a WUA — a signed attestation of its own genuineness — the way this build's credential presents its cnf claim to prove holder-key possession, one level up: the Wallet Unit proving itself before it's trusted to hold anything. That attestation is periodically re-checked, not a one-time formality. The whole chain — WSCD, WSCA, WUA, certification — is what lets an issuer provision a PID to a device it has never seen, and lets a relying party trust a presentation from a wallet it has never talked to directly.
Why this still teaches the real thing
The protocol shape built here is faithful: OpenID4VCI-lite issuance, OpenID4VP-lite presentation with DCQL-lite, SD-JWT VC selective disclosure, KB-JWT holder binding, nonce/audience freshness, a real cross-device relay. None of that is simulated or approximated — it's the same mechanism proved in Python, now realized against browser primitives. The wall is specifically about hardware trust and certification, a layer beneath the protocol, not about the credential format or presentation protocol being make-believe. Understanding exactly where that layer starts — by hitting it in working code rather than reading about it in the ARF's prose — is the point.
Thirteen defect species
One row per way a presentation can be wrong, each labelled with the check that should catch it. Six break the wire bytes themselves and run live in Try It. The other seven need a different verifier configuration — another trust list, registration scope, or status entry — than the single fixed instance Try It runs against.
- What it is
- The control: an unmodified, validly issued presentation revealing the requested claims.
- What breaks
- Nothing — every check should pass.
- What a verifier must handle
- Establishes the baseline accept path every defect species is measured against.
- What it is
- One bit flipped in the issuer JWT's signature; the payload and disclosures are untouched.
- What breaks
- The JWS no longer verifies against the issuer's public key.
- What a verifier must handle
- Recompute the signature over the exact bytes received, not the bytes it expected.
- What it is
- A disclosed claim's value (nationality) is changed after issuance.
- What breaks
- The recomputed digest of the disclosure no longer matches the signed _sd entry.
- What a verifier must handle
- Re-hash every disclosed value with its salt and compare against the signed digest list — never trust a disclosed value on its face.
- What it is
- The KB-JWT is removed entirely; the credential is presented with no holder-binding proof at all.
- What breaks
- There is nothing proving this presentation came from the credential's legitimate holder.
- What a verifier must handle
- Reject a presentation with no key-binding proof outright — a bearer credential is not an acceptable substitute.
- What it is
- The KB-JWT's aud names a different verifier than the one actually checking it.
- What breaks
- The holder-binding proof was made for someone else's request.
- What a verifier must handle
- Check the KB-JWT's aud against its own identity, not just that an aud is present.
- What it is
- The KB-JWT signs over a nonce from a previous request instead of the one just issued.
- What breaks
- The proof of freshness is stale — this could be a captured, replayed presentation.
- What a verifier must handle
- Bind every request to a single-use nonce and check the KB-JWT against exactly that value.
- What it is
- A relaying attacker sits between the verifier's QR and the wallet, substituting its own origin as the audience the wallet binds to. Byte-for-byte the same defect as wrong_audience_kb_jwt, deliberately not reimplemented — the check that stops one stops the other.
- What breaks
- The wallet's KB-JWT is bound to the phishing relay, not the real verifier.
- What a verifier must handle
- The same aud check that catches an ordinary audience mismatch catches a phishing relay too — no special-casing needed.
- What it is
- A cryptographically valid presentation, checked by a verifier with no trust-list registration for this issuer.
- What breaks
- Nothing about the wire bytes — the defect is entirely in the verifier's own trust configuration.
- What a verifier must handle
- Never treat a valid signature as proof of an accredited issuer — trust-path is a separate lookup (ATLAS_EUDI.md §11).
- What it is
- A valid presentation whose credential has since been revoked by its issuer.
- What breaks
- The credential was legitimate at issuance and is no longer live.
- What a verifier must handle
- Check status separately from signature validity — a valid signature does not mean a live credential.
- What it is
- The credential's exp claim is in the past; everything else about it verifies cleanly.
- What breaks
- The credential has aged out, independent of any tampering.
- What a verifier must handle
- Check expiry against the verification time, every time — a policy-layer check, not a crypto one.
- What it is
- The issuer resolves at LoA substantial in the verifier's trust registration; the journey requires LoA high.
- What breaks
- The assurance level backing this credential doesn't meet what the relying party's journey demands.
- What a verifier must handle
- Compare the resolved LoA against the journey's own requirement — a bank-specific policy decision, not a protocol constant.
- What it is
- age_over_18 is set to deliberately disagree with birth_date at issuance, then both are disclosed together — the flagship red-team finding.
- What breaks
- A derived claim and its source claim tell two different stories, and nothing in the credential format stops that from being issued.
- What a verifier must handle
- Cross-check derived claims against their source claims whenever both are disclosed — the crypto core cannot catch this; only a verifier's own consistency policy can.
- What it is
- A verifier registered only for age-check purposes requests and receives nationality as well.
- What breaks
- The claims requested/disclosed exceed what this verifier is actually accredited to ask for.
- What a verifier must handle
- Check the requested-and-revealed claim set against the verifier's own registered scope, not just that a registration exists.