Credentials beyond the PID (EAA)¶
A wallet connection can ask the wallet, in the same request as the PID, for other electronic attestations of attributes ((Q)EAA): a mobile driving licence, a diploma, a professional qualification, a permit. Each one is verified against its own issuers, revocation and validity, and its claims can grant attributes and roles through the connection's identity-provider mappers. (Preview, not yet tested with a real national wallet.)
Status: Preview — not yet tested with a real national wallet
New in 1.1.0-rc.1. Tested with the project's own test wallet (SD-JWT VC and mdoc); not yet with a real wallet or a real (Q)EAA issuer.
How it works¶
- The PID still identifies the citizen. Every combination a connection accepts must include the PID; "PID or passport" is refused when the connection is saved.
- Each other credential is a DCQL credential query of its own, with its own
trusted_authorities,claim_setsfor optional claims and, for mdoc,intent_to_retain. - Alternatives and combinations ("the PID and either a diploma or a professional registration") are DCQL
credential_sets. - A wallet link from the account console asks for the PID only.
In the console¶
Open the wallet connection's Credentials tab:
- Other credentials: add a credential from a template, edit it, remove it;
- Combinations: which credentials are required together, and which are alternatives;
- What the citizen is asked for: a plain-language summary of the request.
The templates are:
| Template | What it fills in |
|---|---|
| Mobile driving licence | ISO/IEC 18013-5 mdoc, docType org.iso.18013.5.1.mDL, namespace org.iso.18013.5.1; driving_privileges required, document_number and expiry_date optional |
| Educational credential (diploma) | SD-JWT VC with the vct left empty for you to fill in (no EU-wide SD-JWT VC type for diplomas is settled), and suggested claim paths degree and awarding_body to edit |
| Custom SD-JWT VC | blank |
Labels are English defaults; reword them. The wallet page lists the credentials asked for and why.
Configuration¶
Over the admin API, three keys of the connection's config (Terraform: credentials, credential_sets, holder_binding_same_key on helix_wallet_identity_provider).
credentials is a JSON array, one object per credential type:
| Member | Meaning |
|---|---|
id |
DCQL query id and the prefix of its claims in mappers. 1–32 of a-z 0-9 _, starting with a letter; not pid, pid_…, helix, address or place_of_birth; unique |
label |
what the citizen and the admin see (required, at most 80 characters) |
format |
dc+sd-jwt (then vctValues: 1–10 accepted vct) or mso_mdoc (then doctype) |
claims |
1–32 {path, required}. path: SD-JWT VC members (1–4), or [namespace, element] for mdoc. required defaults to true; at least one claim must be required. Optional claims are asked for through DCQL claim_sets |
trustAnchors |
PEM CA certificates of this credential's issuers (at most 64 KiB) |
trustedListSources, trustedListServiceTypes |
the realm's trusted lists for these issuers (at most 10 lists and 20 service types), for example service type http://uri.etsi.org/Svc/Svctype/Provider/QEAA. Anchors or lists (or both) are required |
didIssuers |
SD-JWT VC only: issuers identified by a DID that this credential accepts |
statusRequired |
refuse a credential without a status list reference. Default true. A status that is present is always checked |
intentToRetain |
mso_mdoc only: intent_to_retain on every claim |
purpose |
plain text (at most 300 characters) shown on the wallet page. OpenID4VP 1.0 has no per-credential purpose member, so it is not sent to the wallet |
credentialSets is a JSON array of {options, required, purpose}, sent as DCQL credential_sets. options are the alternatives, each a list of credential ids that together satisfy the set; pid stands for the PID. Some required set must have pid in each option, and every credential must be in some set; otherwise the save is refused. Unset: the PID and every credential are required.
holderBinding.sameKey (default false) is described below.
"config": {
"credentials": "[{\"id\":\"mdl\",\"label\":\"Mobile driving licence\",\"format\":\"mso_mdoc\",\"doctype\":\"org.iso.18013.5.1.mDL\",\"claims\":[{\"path\":[\"org.iso.18013.5.1\",\"driving_privileges\"]}],\"trustAnchors\":\"-----BEGIN CERTIFICATE-----…\"},{\"id\":\"qualification\",\"label\":\"Medical registration\",\"format\":\"dc+sd-jwt\",\"vctValues\":[\"https://registry.example.eu/medical\"],\"claims\":[{\"path\":[\"profession\"]}],\"trustedListSources\":[\"eu-qeaa\"]}]",
"credentialSets": "[{\"options\":[[\"pid\"]]},{\"options\":[[\"mdl\"],[\"qualification\"]]}]"
}
This connection asks for the PID, and for either a mobile driving licence or a medical registration.
A trusted list named only by a credential counts as used by its connection, exactly like one the PID names: changing it needs the same admin permissions, and renaming or deleting it is refused while it is in use.
Verification: per-credential trust¶
Each credential is verified with the same transaction binding as the PID (nonce, client_id or Digital Credentials API origin, response key, clock), but with its own trust anchors, accepted vct or docType and status requirement:
- Issuer trust: the chain must reach the credential's own anchors or trusted lists, with the connection's
revocationChecking. A diploma signed by a PID issuer is refused, and so is a PID signed by a diploma issuer (ISSUER_UNTRUSTED). - Revocation: the credential's Token Status List, verified against its own anchors (
CREDENTIAL_REVOKED), with the connection'sstatusList.onUnavailable, cache and allowed hosts. - Validity:
iat/nbf/exp(SD-JWT VC) andvalidityInfo(mdoc), with the connection's clock skew. - Holder binding: the KB-JWT (SD-JWT VC) or
deviceSignature(mdoc) over this request, as for the PID. - The sets: every required set needs an option whose credentials were all presented; an answer outside every satisfied option is refused (
INVALID_RESPONSE); claims not asked for are dropped, and anything missing isDCQL_NOT_SATISFIED.
A credential other than the PID can never set the subject, a name or a birth date, whatever claims it carries. The login audit event names the PID issuer.
Several credentials, one holder?¶
Every presentation is bound to this one request. But OpenID4VP 1.0 and HAIP 1.0 define no proof that credentials bound to different keys are in one wallet, and wallets use a key per credential on purpose (unlinkability). So, in general, a citizen who controls two wallets could combine a PID from one with a credential from the other. Where your issuers bind every credential to one key, set holderBinding.sameKey=true: a response whose presentations use different keys is refused (HOLDER_BINDING_INVALID).
Mapping claims to attributes and roles¶
The credentials' verified claims reach the connection's identity-provider mappers (console: the connection's Mappers tab, which suggests these names) as upstream claims named <id>.<path>: qualification.profession, or for mdoc <id>.<element>, for example mdl.driving_privileges.vehicle_category_code. helix_wallet_credentials lists the ids presented. Strings come as they are, numbers and booleans as text, arrays as several values.
They are never written to the account by themselves: only an attribute-importer mapper copies one.
Example: a claim-to-role mapper with source qualification.profession, match value doctor, role doctor and sync mode FORCE. A citizen who presents a medical registration with profession doctor gets the role, and loses it at a later sign-in whose registration names another profession.
Upgrade action: review your wallet connections' mappers
Wallet logins now run the typed identity-provider mappers like every other connection: at just-in-time provisioning every mapper applies (attribute importer, hardcoded attribute and role, claim-to-role with EQUALS or REGEX); at a later sign-in the FORCE mappers apply (attributes overwritten, mapper-assigned roles added and removed). Before, only the legacy mappers setting applied, and only at just-in-time provisioning. Typed mappers saved on a wallet connection earlier now run on wallet sign-in: review them, especially role mappers, before you upgrade. See Upgrading from rc.13 to 1.1.0-rc.1.
A mapping that would make a user an administrator through a link that may not sign one in is refused, as for OIDC and SAML. A mapper that cannot be applied refuses the sign-in; a freshly created account is removed again (audited USER_DELETE with reason wallet_mapping_failed). The username template is not applied to wallet accounts: their username is the subject.
This deployment's own issuer¶
Helix can issue credentials itself. Its issuers — every realm's did:web on any host this server answers for, every realm's issuer key in any state, the certificates of their chains and the CA that issued them, and a did:jwk / did:key of such a key — are treated like this, in this realm and across realms:
- Never for the PID.
trustAnchorsordidIssuersof the PID naming one is refused on save with a400that says why; a stored connection that does is skipped at start-up (logged). A PID that still arrives signed by one (through a trusted list, or a key created after the connection was saved) is refused at verification withISSUER_OWN_DEPLOYMENT, audited asWALLET_LOGIN_FAILURE. An administrator withmanage-credential-issuancewrites what that issuer signs; trusting it for the PID would let them sign in as any wallet-linked account. - Allowed for another credential, but the presentation is marked: the sign-in's mappers may then grant no role with an admin permission (refused, audited
PRIVILEGED_LINK_REFUSEDwith reasonown-issuer-mapped-administrator; at just-in-time provisioning the fresh account is removed again), and the link a just-in-time sign-in creates gets the provenanceOWN_ISSUER, an untrusted link that never signs in an administrator. - Trusting your own issuer means trusting the administrators who hold
manage-credential-issuancein that realm for every non-admin role you map. Map only roles you would let them hand out.
Unreadable issuer data fails closed: the PID is refused, another credential counts as own.
To release these claims to applications, see Verified data for applications.
Not in this release¶
A non-PID identity document instead of the PID; several copies of one credential type (multiple); claim value filters; the etsi_tl and openid_federation trusted-authority types; the username template for wallet accounts.
See also¶
- EU Digital Identity Wallet (OpenID4VP) — the connection these credentials belong to
- Trusted lists — per-credential trust from signed lists
- Mappers — sync modes and mapper types
- Terraform provider —
credentials,credential_sets,holder_binding_same_key