23 July 2026
What testing against a real wallet taught me that the reference implementation couldn't
By Jorge
The comfortable illusion
For weeks, my age verification verifier passed everything I threw at it. Presentation requests built correctly, responses parsed, signatures validated, results returned. Tested against the European Commission's reference age verification wallet, the implementation published alongside the Age Verification Blueprint. Green across the board.
Then I got test access to AltID — Denmark's wallet, and the first age verification credential issuer on the EU's production Trusted List — pointed the same verifier at it, and the request was rejected outright before a single credential moved.
That gap is the subject of this article. Not because anything was wrong with the reference implementation, but because of what a reference implementation is for, and what it quietly can't tell you.
Getting access, since this comes up a lot
A practical note first, because the most common question I get about AltID is whether a non-Dane can test against it at all. The answer is yes.
The Danish Agency for Digital Government provides test access to organisations: a test version of the AltID app, a test tool that generates presentation requests, and — the part that unblocks everyone outside Denmark — help provisioning a test MitID user, which removes the need for a Danish CPR number. The request goes to their published AltID contact address with the subject line they specify, from a corporate email address. Their integration documentation covers the process.
One detail worth knowing before you start, because it changes how much work you're signing up for: AltID does not require relying party registration for age verification. Registration is required to request the identity credential (PID), not the proof of age. For an age verification use case, there's no registry gate to clear.
Two profiles, and you probably want the simpler one
AltID supports two OID4VP profiles, and the choice matters more than it first appears.
HAIP — the High Assurance Interoperability Profile — covers all credential types. It requires the verifier to register a certificate and sign its requests, mandates encrypted responses, and requires same-device flows with redirect for phishing resistance. It's the general-purpose, heavier path.
AVP — the EU Age Verification Profile — is limited to proof of age. It requires no registration, authenticates the verifier by URI, and carries the request as plain URL parameters. It exists specifically to make the age verification case simple, and it's aligned with the EU Age Verification Blueprint.
If you're building age verification, AVP is the path. It's less work, fewer moving parts, and it's the profile the EU age verification ecosystem is converging on. Neither profile involves the browser Digital Credentials API — AltID doesn't support DC API at all. It's classic OID4VP over a QR code or a same-device link.
The actual finding: reference implementations are permissive
Here's the part I didn't expect.
My verifier and AltID were speaking the same protocol, using the same profile, with the same configuration. The rejection wasn't a protocol mismatch. It was that my presentation request contained a handful of extra fields — leftovers and conveniences that aren't part of the DCQL specification — and AltID's request validator rejected the whole thing rather than ignoring them.
The reference wallet had accepted those same requests without complaint, every time, for weeks.
Both behaviours are defensible. A reference implementation exists to demonstrate that a flow works and to give implementers something to develop against; being tolerant of imperfect input makes it more useful for that. A production wallet issued by a national authority has different incentives: strict schema validation is a security property, and rejecting anything that doesn't match the specification exactly is the correct conservative choice.
But the consequence for anyone building a verifier is worth stating plainly: passing the reference implementation does not mean you're interoperable. It means you haven't been told you're wrong yet. The reference is a floor, not a ceiling — and the gap between "works against the reference" and "works against a production wallet" is real, silent, and only discoverable by testing against the real thing.
Whose fault it was
Worth being clear about this, because it would be easy to frame the reference implementation as the problem. It wasn't.
The extra fields were mine. They had accumulated the way things do during development — a convenience here, a leftover from an earlier approach there — and none of them belonged in a spec-conformant request. AltID was right to reject them. The reference wallet was simply never going to be the thing that told me.
That's the asymmetry worth internalising. A permissive validator gives you no signal when you drift from the specification, and drift is the default state of any codebase under active development. You don't find out you've drifted until something strict looks at your output. If the only strict thing that ever looks at it is a user's real wallet in production, that's a bad place to find out.
Configured is not the same as working
A related habit this pushed me toward, and one I'd suggest to anyone building a verifier.
There's a category of check — trust list validation is the obvious one — where the failure mode is silent. Everything returns success, so everything looks correct, and you can go a long time without noticing that a check isn't actually exercising the path you think it is. A positive test can't distinguish "validation ran and passed" from "validation never ran".
So the question worth asking about your own implementation isn't "is trust list validation configured?" It's "have I watched it reject an issuer that isn't on the list?" Those are different questions, and only the second one is evidence. The same applies to expiry, to signature validation, to every check whose job is to say no: if you have never seen it say no, you do not know that it can.
What I can and can't claim now
Being precise about this, because the distinction matters and it's easy to overstate.
What's verified: the verifier completes an end-to-end age verification presentation with AltID in AltID's test environment, with trust list validation active. Selective disclosure behaves as specified — the credential carries nine age thresholds, the request asks for one, and exactly one is disclosed. That last point isn't a design claim any more; I watched eight attributes stay hidden.
What isn't: this is a test environment with a test credential and a development issuer certificate, not production traffic with real users. Negative-case testing — expired credentials, untrusted issuers, tampered signatures — is covered by synthetic vectors in CI, and I'm still working through the manual cases against the live test issuer. "It accepts what it should" is only half of a verifier. The other half is "it rejects what it should", and I'd rather say that when I've watched every case do it.
The practical takeaway
If you're building age verification for DSA Article 28 — or evaluating someone who has — the question worth asking isn't "does it work?" It's "what has it been tested against?"
An implementation validated only against a reference wallet has been tested against something designed to be forgiving. The interoperability problems live in the gap between that and the wallets your users will actually have. There are seven front-runner member states, and each national wallet will have its own strictness, its own quirks, and its own reading of the same specification. Denmark is the first. It won't be the last one to reject something the reference accepted.
Technical points in this article are based on AltID's published integration documentation (Digitaliseringsstyrelsen), the EU Age Verification Blueprint technical specification and its Age Verification Profile (ageverification.dev), and my own integration testing against AltID's test environment. Nothing here reflects the internals of any employer's systems; all sources are public.
Sources for this article are linked inline. For the full list of primary sources espuni relies on, see references.