Skip to content

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_sets for 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's statusList.onUnavailable, cache and allowed hosts.
  • Validity: iat / nbf / exp (SD-JWT VC) and validityInfo (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 is DCQL_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. trustAnchors or didIssuers of the PID naming one is refused on save with a 400 that 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 with ISSUER_OWN_DEPLOYMENT, audited as WALLET_LOGIN_FAILURE. An administrator with manage-credential-issuance writes 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_REFUSED with reason own-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 provenance OWN_ISSUER, an untrusted link that never signs in an administrator.
  • Trusting your own issuer means trusting the administrators who hold manage-credential-issuance in 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