# 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](mappers.md). (Preview, not yet tested with a real national wallet.)

!!! warning "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](trusted-lists.md) 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](dids.md) 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](#several-credentials-one-holder).

```json
"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.

!!! warning "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](../operations/upgrade-rc13-1.1.0-rc1.md).

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](credential-issuance.md) 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](../integration/verified-claims.md).

## 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)](eudi-wallet.md) — the connection these credentials belong to
- [Trusted lists](trusted-lists.md) — per-credential trust from signed lists
- [Mappers](mappers.md) — sync modes and mapper types
- [Terraform provider](../integration/terraform.md) — `credentials`, `credential_sets`, `holder_binding_same_key`
