Selective Disclosure and the EUDI Wallet: Verifying Age or Status Without Harvesting Identity

The EUDI Wallet lets you prove someone is over eighteen without learning their name, date of birth, or anything else. Most verification flows will throw that away and collect the lot, because that is how they were built to collect it.

Under the revised eIDAS Regulation (Regulation (EU) 2024/1183, in force since 20 May 2024), every Member State must make at least one European Digital Identity Wallet available to those who want one within 24 months of the relevant implementing acts. The first of those acts took effect at the end of 2024, which puts wallet availability at the close of 2026. That is not a distant horizon. It is the next planning cycle for anyone who checks age, residency, professional status or entitlement before granting access to a service.

The wallet’s most consequential technical property, for the people who build verification, is selective disclosure. A credential in the wallet can carry twenty attributes, and the holder can present exactly one of them, cryptographically signed by the issuer, without revealing the other nineteen and without the issuer being told where it was used. That capability is either the best thing that has happened to your data-minimisation posture in a decade, or it is a feature you will quietly defeat by asking for the whole credential anyway. Which one depends on how you design the request.

Free · 4 minutes

Would you survive contact with a determined attacker — or an auditor?

Fourteen questions on access, patching, detection, and recovery — the basics that prevent most real incidents, and the ones most often assumed rather than verified. Banded finding on screen, full sheet by email.

What the wallet can actually prove

Two credential formats matter, and the Architecture and Reference Framework requires wallets to support both. The first is the SD-JWT Verifiable Credential, built on Selective Disclosure for JSON Web Tokens — published as RFC 9901 in late 2025, with the VC profile (draft-ietf-oauth-sd-jwt-vc) still an active IETF draft at the time of writing. The second is the ISO/IEC 18013-5 mobile document, or mdoc, the format underpinning the mobile driving licence and carrying its own selective-disclosure mechanism.

The mechanism in SD-JWT is worth understanding, because it dictates what a well-designed request looks like. When an issuer creates the credential, each selectively disclosable claim is turned into a disclosure: an array of a random salt, the claim name, and the claim value, base64url-encoded. The signed token does not contain the claim values. It contains a _sd array of SHA-256 digests, one per disclosure, plus an _sd_alg field naming the hash. The holder keeps the disclosures. When they present the credential, they send the signed token plus only the disclosures they choose to reveal, tilde-delimited, followed by a Key Binding JWT that proves possession of the credential’s private key and pins the presentation to a specific verifier and a specific moment.

So a holder can send you a credential proving age_over_18 is true, and mathematically nothing else. You verify the issuer’s signature, recompute the digest of the one disclosure you received, and confirm it appears in the _sd array. If it does, the issuer attested that attribute and you can trust it. You never see the birth date. You never see the name. There is nothing to leak, because you were never given it.

The verification flow, minimally

Presentation is carried by OpenID for Verifiable Presentations, and the discipline lives in the presentation request: you name the credential type and the single claim path you need, and you send a fresh nonce. The wallet shows the user precisely what you asked for, which is also your defence — an over-broad request is visible to the person approving it. Here is the verifier side, reduced to what matters, checking an age_over_18 disclosure and refusing anything more than it asked for.

# Verify an SD-JWT VC presentation that should disclose ONLY age_over_18.
# Depends on a JOSE library for signature checks; logic is the point here.
import hashlib, base64, json

REQUESTED = {"age_over_18"}          # the single claim we asked for

def b64url_digest(disclosure_str: str) -> str:
    d = hashlib.sha256(disclosure_str.encode("ascii")).digest()
    return base64.urlsafe_b64encode(d).rstrip(b"=").decode("ascii")

def verify(presentation: str, issuer_pubkey, expected_nonce, expected_aud):
    issuer_jwt, *rest = presentation.split("~")
    kb_jwt = rest.pop() if rest and rest[-1] else None
    disclosures = [d for d in rest if d]

    payload = verify_jws(issuer_jwt, issuer_pubkey)   # raises on bad signature
    sd_digests = set(payload.get("_sd", []))
    if payload.get("_sd_alg", "sha-256") != "sha-256":
        raise ValueError("unsupported digest algorithm")

    disclosed = {}
    for d in disclosures:
        if b64url_digest(d) not in sd_digests:
            raise ValueError("disclosure not bound to this credential")
        _salt, name, value = json.loads(
            base64.urlsafe_b64decode(d + "=="))
        disclosed[name] = value

    # Data-minimisation guard: reject anything beyond what we requested.
    if set(disclosed) - REQUESTED:
        raise ValueError("over-collection: extra claims presented")

    # Key binding: freshness and audience, else it is replayable.
    verify_key_binding(kb_jwt, payload, expected_nonce, expected_aud)

    return disclosed.get("age_over_18") is True

The two lines that do the governance work are the over-collection guard and the key-binding check. The first turns data minimisation from a policy into a runtime failure: if a wallet or a misconfigured request ever hands you more than age_over_18, the transaction stops rather than silently banking the surplus. The second stops a captured presentation being replayed against you later. Neither is optional if you want the flow to hold up.

Where a minimising flow turns into an identity grab

The failure modes are predictable, and none of them are cryptographic. They are product and logging decisions.

Requesting the whole credential because it is easier. The Personal Identification Data credential carries name, birth date and more. Asking for it to derive age yourself is the old pattern wearing new clothing. If the issuer offers an age_over_18 attribute — or a dedicated age attestation — request that, not the birth date. Deriving the answer means you are now holding the raw identifier you did not need.

Logging the presentation blob. The single most common way to undo selective disclosure is to write the full presentation — token, disclosures and all — into an access log or an analytics pipeline for debugging. You have now persisted, in a system with looser controls than your identity store, exactly the data the wallet went to great lengths not to give you. Log the decision and the proof reference, not the personal data. Under the GDPR you should be able to show a supervisor that your verification path collects a boolean and retains a boolean.

Skipping issuer and relying-party trust. A signature you cannot chain to a trusted issuer proves nothing, and a wallet is entitled to refuse a verifier that has not registered its intended data. Both sides are governed. Getting your own registration and the issuer trust list right is the plumbing that makes the rest lawful, and it is worth treating as its own workstream rather than an afterthought bolted onto the integration.

Design it as a data question, not an auth question

The teams that get this right start from the attribute, not the credential. They write down the single fact the service actually needs — over eighteen, resident in the jurisdiction, holder of a valid licence — and they design the request to ask for that fact and refuse the rest at the code level. That is a different exercise from standing up relying-party acceptance as an authentication feature, and it belongs earlier in the design. Where you also need to prove possession or step up to a payment, the same wallet supports strong customer authentication, but that is a separate request with its own minimisation logic — do not fold identity into an age check because the plumbing happens to be shared.

Two pieces of the surrounding machinery decide whether any of this is trustworthy. One is relying-party registration and the trust list, which is what lets a wallet decide you are allowed to ask. The other sits upstream: if you are the party attesting the attribute rather than consuming it, the discipline shifts to issuing qualified electronic attestations of attributes that a verifier can rely on. Build for one attribute at a time and the flow stays defensible. Build for the credential and you have reinvented the identity grab the wallet was designed to end.

Free interactive tool

Website compliance checklist

What your site has to do, based on what it actually does

Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.

Can you trust the architecture you have?

Architecture diagrams rarely show the reality of how systems actually operate. An independent review establishes what is really there.