Skip to main content

Security model

This page describes who EUDIPLO trusts, how each boundary is protected and which keys protect which messages. It explains the model only: hardening steps are in the production checklist, and the presentation session binding (OID4VP ยง13.3) is explained on Sessions.

Trust boundariesโ€‹

BoundaryCallersProtection
Management API (/api)Your backend, the web clientOAuth 2.0 bearer token of a client. The token names one tenant and the client's roles; allow lists can further restrict which configurations a client may use. Configuration changes are written to an audit log.
Protocol endpointsWallets, and your page for the DC APIPublic by design. Each step is protected by the protocol: single-use codes and nonces, PKCE, DPoP, wallet and key attestation, signed requests and encrypted responses.
Outbound callsEUDIPLO to URLs that tenants configure or that credentials and certificates nameOutbound URL policy (below), optional API key header for webhooks and attribute providers. Webhook requests are not signed.
Storage and key materialEUDIPLO to database, object storage, KMSSensitive columns encrypted at rest; with an external KMS the private keys never leave it. Configuration bundle exports never contain private keys. GET /api/key-chain/{id}/export returns the private key of a db key chain and, like the tenant KMS provider configuration with its provider credentials, needs tenant:admin or tenants:manage.

Tenants are isolated in the data layer: every entity carries a tenantId, and a client token only reaches its own tenant. A client with the tenants:manage role can manage all tenants, so give it only to platform operators (Tenants and access).

HTTPS and TLSโ€‹

Wallets expect HTTPS for every issuer and verifier URL, so PUBLIC_URL must be an HTTPS URL. TLS is terminated by a reverse proxy or by EUDIPLO itself (TLS).

For outgoing requests to URLs that tenants configure or that credentials and certificates name (webhooks, attribute providers, issuer metadata, rulebooks and schemas, trust lists, status lists, OpenID Federation entity configurations, certificate revocation lists, and the metadata, keys, token and introspection endpoints of external authorization servers and of the chained server's upstream provider), EUDIPLO applies an outbound URL policy against server-side request forgery: only HTTPS targets that resolve to public addresses are allowed, checked after DNS resolution, and every redirect of a download is checked again. OUTBOUND_URL_ALLOW_HTTP and OUTBOUND_URL_ALLOW_PRIVATE_NETWORK relax this for development or in-cluster services. OUTBOUND_URL_ALLOWED_HOSTS narrows it: when set, only the listed hosts and their subdomains are reachable, and they still need HTTPS and public addresses unless the two flags allow otherwise (environment variables).

Two exceptions keep standard deployments working. Trust lists, status lists, federation entity configurations and authorization server keys (JWKS) on EUDIPLO's own PUBLIC_URL or INTERNAL_URL origin skip the policy, because managed trust lists, the status lists of credentials EUDIPLO issued and the keys of its chained and OID4VP-based authorization servers are fetched from there; a redirect to another origin is checked again. CRL distribution points may use plain HTTP regardless of OUTBOUND_URL_ALLOW_HTTP, as is usual for CRLs (RFC 5280); their address is still checked, and a CRL only counts if the CA that issued the certificate signed it (revocation check).

The token request to the upstream provider and token introspection requests do not follow redirects. KMS providers, including those in a tenant's KMS configuration, are called without the policy; restrict EUDIPLO's egress at the network level if they must not reach internal services.

Trust decisionsโ€‹

DecisionBased on
Is a presented credential's issuer trusted?Its certificate chain against the LoTE trust lists named in the DCQL query, if any, and optionally OpenID Federation (Presentation under the hood)
Is an external or upstream authorization server trusted?With a federation policy in the issuance configuration, the server must be trusted by it before EUDIPLO fetches its metadata
Is a wallet trusted?Wallet and key attestations, checked against wallet-provider trust lists (below)
Is EUDIPLO trusted by the wallet?The access certificate on requests and signed metadata, the registration certificate, and the issuer certificate in credentials

OpenID Federation support does not yet verify entity statements cryptographically; OpenID Federation describes what is and is not checked.

Tokens and codesโ€‹

TokenIssued byAccepted atLifetimeBound to
Management API token/api/oauth2/token, or your OIDC provider/api/...24 hours (built-in server)Client, tenant, roles
Pre-authorized codeEUDIPLO, in the offerToken endpointUntil the session TTL ends; single useSession; optional tx_code
Authorization codeHosted authorization serverToken endpoint60 seconds (built-in), 300 seconds (chained, OID4VP-based); single useSession, grant, PKCE challenge, DPoP key from PAR
OID4VCI access tokenHosted or external authorization serverCredential, deferred and notification endpointsBuilt-in: token.lifetimeSeconds, default 300 seconds; chained and OID4VP-based: default 3600 secondsSession; DPoP key (cnf.jkt) if DPoP is used
Refresh tokenHosted authorization serverToken endpoint30 days by default; not extended by useSession, DPoP key, wallet attestation key
Credential noncePOST /issuers/{tenant}/vci/nonceKey proofs at the credential endpointSingle useTenant

Refreshing never extends the authorization: the built-in server keeps the refresh token, the chained and OID4VP-based servers replace it, and in both cases its original expiry stays. Presentation-side values (nonce, response_code) are described on Sessions.

PKCEโ€‹

Every authorization code requires PKCE with S256: a PAR request without code_challenge, or with any other method, is rejected, and the token request must send the matching code_verifier. This applies to the built-in, chained and OID4VP-based servers and to interactive authorization. The chained server also uses S256 towards the upstream provider.

DPoPโ€‹

DPoP (RFC 9449) binds an access token to a key the wallet holds, so a stolen token is useless without that key. EUDIPLO verifies every DPoP proof:

  • the signature with the public key in the proof header,
  • htm and htu against the request method and URL,
  • iat no older than 300 seconds, with 60 seconds of clock skew,
  • jti used once per key, tracked in the database until the proof leaves the freshness window, so a replay is caught across backend instances,
  • at the credential, deferred and notification endpoints, ath (the access token hash) and the key against the token's cnf.jkt.

A key presented at PAR must be used again at the token endpoint and for refreshes. DPoP is required at the credential, deferred and notification endpoints when the issuance configuration sets dPopRequired. At the token endpoint, the built-in server requires it with dPopRequired or its own requireDPoP, the chained and OID4VP-based servers with their requireDPoP. A bad proof is answered with invalid_dpop_proof at PAR and token, and with HTTP 401 invalid_token at the other endpoints. Configuration: Authorization servers.

Wallet and key attestationโ€‹

Both mechanisms are signed by the wallet provider and trusted through wallet-provider trust lists, but they answer different questions:

Wallet attestationKey attestation
QuestionIs this wallet a trusted OAuth client of this authorization server?Is this holder key, and the way it is stored and unlocked, acceptable for the credential?
SentOAuth-Client-Attestation and OAuth-Client-Attestation-PoP headers at PAR and tokenAn attestation proof, or a key_attestation header in a jwt proof, at the credential endpoint
Required byThe authorization server entry, with issuance-level defaultsThe credential configuration (keyAttestationsRequired, proofTypesSupported)
Trust anchorThe authorization server's wallet-provider trust lists, or the issuance-level onesThe issuance configuration's walletProviderTrustLists

External authorization servers cannot enforce wallet attestation through EUDIPLO. How to configure both: Wallet and key attestation.

Keys and algorithmsโ€‹

EUDIPLO signs with ES256 (ECDSA on P-256) only; CRYPTO_ALG accepts no other value, and the verifier metadata advertises ES256. Each operation uses the following key:

OperationKeyAlgorithm
Access tokens of hosted authorization serverstoken.signingKeyId, then the issuance signingKeyId, else the tenant's first signing key chainES256
SD-JWT VC credentials, mdoc MSOattestation key chain (keyChainId of the credential configuration)ES256
Status listsstatusList key chainES256
Trust lists hosted by EUDIPLOtrustList key chainES256
OID4VP request objects, signed issuer metadata, client_idaccess key chain (accessKeyChainId of the presentation configuration)ES256
Encrypted credential requests, ISO 18013-7 responsesencrypt key chain, published in the issuer metadataECDH-ES (JWE), HPKE (ISO)
OID4VP responsesA fresh key pair per session, not a key chainECDH-ES with A128GCM or A256GCM
Credential responsesThe wallet's key from the credential requestECDH-ES with A128GCM or A256GCM

Key chains live in a KMS provider. With the db provider, the private key is stored encrypted in the database; with Vault, AWS KMS, PKCS#11, CSC or an HTTP signer, EUDIPLO only sends data to be signed. Usage types and certificates are explained in Keys and certificates, providers in Key management.

Rotationโ€‹

A key chain rotates its signing key automatically by its rotationPolicy (a daily job checks intervalDays) or on demand (POST /api/key-chain/{id}/rotate). Rotation creates a new key in the same KMS provider and a new certificate, issued by the chain's internal root CA if it has one and self-signed otherwise; new signatures use it immediately. The previous key and certificate stay available for a fixed grace period of 30 days, so relying parties can still validate recently signed tokens, credentials and lists.

Encryption at restโ€‹

Sensitive columns are encrypted with AES-256-GCM: the private key material of db key chains, session data such as offers, authorization requests, presented credentials and response encryption keys, and the state of interactive authorization. The data encryption key comes from ENCRYPTION_KEY_SOURCE: by default it is derived from MASTER_SECRET with HKDF; with vault, aws or azure it is fetched from a secret store at startup and held only in memory. Losing or changing this key makes the encrypted data unreadable, and it also anchors the fingerprints of the single active credential policy. Operating the key: Encryption keys.