Issuer Integration
This guide explains how to configure the Issuer2 Service to require attestation-based client authentication when issuing credentials.
Overview
Issuer2 configures client authentication with clientAuthenticationConfig. When a client-attestation method is present, token requests must include valid OAuth-Client-Attestation and OAuth-Client-Attestation-PoP headers. The issuer then verifies:
- The attestation JWT signature against a trusted attester key (or X.509 chain)
- The PoP JWT signature against the
cnf.jwkfrom the attestation - Claims such as expiration, audience, timestamps, and replay protection
You can combine preauth-anonymous with client-attestation: wallets may then use anonymous pre-authorized code token requests (no client_id, no attestation headers) or present attestation headers, depending on the flow.
Omit clientAuthenticationConfig or use an empty supportedMethods list to keep legacy issuer2 behavior where this layer does not enforce client authentication.
Prerequisites
Before configuring issuer attestation:
- Issuer2 Service – An existing Issuer2 Service in your tenant
- Attester trust material – Public JWK, KMS key reference, or X.509 trust anchors matching how the Client Attestation Service signs attestations
Configuration Options
Use the clientAuthenticationConfig object on the Issuer2 Service. It contains a supportedMethods array. Each element is a tagged object (type discriminator):
type | Purpose |
|---|---|
preauth-anonymous | Allows OID4VCI pre-authorized code token requests without client_id when no attestation headers are sent. At most one entry. |
client-attestation | Requires valid attestation headers on the token request whenever that path is used. Includes nested config (see below). At most one entry. |
Example — attestation only (strict wallet authentication):
{
"clientAuthenticationConfig": {
"supportedMethods": [
{
"type": "client-attestation",
"config": {
"verificationMethod": { "...": "see below" },
"clockSkewSeconds": 300,
"replayWindowSeconds": 300
}
}
]
}
}
Example — anonymous pre-authorized code only (no attestation):
{
"clientAuthenticationConfig": {
"supportedMethods": [
{ "type": "preauth-anonymous" }
]
}
}
Example — both (wallets can use anonymous pre-auth for pre-authorized offers without client_id, or send attestation when required):
{
"clientAuthenticationConfig": {
"supportedMethods": [
{ "type": "preauth-anonymous" },
{
"type": "client-attestation",
"config": {
"verificationMethod": { "...": "see below" }
}
}
]
}
}
client-attestation → config parameters
These fields apply to the nested object under client-attestation:
| Parameter | Type | Default | Description |
|---|---|---|---|
verificationMethod | Object | Required | How to verify the attestation JWT signature (see below) |
clockSkewSeconds | Long | 300 | Allowed clock skew for time-based validations (exp, iat) |
replayWindowSeconds | Long | 300 | Replay window for PoP iat |
Verification Methods
Static JWK
Verify the attestation JWT using a static public key provided inline. This is the simplest method, suitable for testing or when the attester key is known and stable.
{
"verificationMethod": {
"type": "static-jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
}
}
KMS Key
Verify against a key stored in the KMS. This is useful when the attester and issuer share the same enterprise instance.
{
"verificationMethod": {
"type": "kms-key",
"keyReference": "waltid.tenant1.kms1.attester-signing-key"
}
}
X.509 Chain
Verify via X.509 certificate chain validation. The attestation JWT must carry an x5c header with the certificate chain, which is validated against configured trust anchors.
This method is required for regulated environments (e.g., VICAL, eIDAS) where attesters must be certified.
{
"verificationMethod": {
"type": "x509-chain",
"trustedRootCertificatesPem": [
"-----BEGIN CERTIFICATE-----
MIIB...
-----END CERTIFICATE-----"
]
}
}
Create an Issuer with Client Attestation
Endpoint: /v1/{target}/resource-api/services/create | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/resource-api/services/create' \
-H 'accept: */*' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"type": "issuer2",
"baseUrl": "https://myorg.enterprise-sandbox.waltid.dev",
"kms": "waltid.tenant1.kms1",
"tokenKeyId": "waltid.tenant1.kms1.tokenKey",
"supportedCredentialTypes": {
"identity_credential_vc+sd-jwt": {
"format": "vc+sd-jwt",
"vct": "https://example.com/credentials/identity_credential",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256"]
}
},
"clientAuthenticationConfig": {
"supportedMethods": [
{
"type": "client-attestation",
"config": {
"verificationMethod": {
"type": "static-jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
},
"clockSkewSeconds": 300,
"replayWindowSeconds": 300
}
}
]
}
}'
Body
{
"type": "issuer2",
"baseUrl": "https://myorg.enterprise-sandbox.waltid.dev",
"kms": "waltid.tenant1.kms1",
"tokenKeyId": "waltid.tenant1.kms1.tokenKey",
"supportedCredentialTypes": {
"identity_credential_vc+sd-jwt": {
"format": "vc+sd-jwt",
"vct": "https://example.com/credentials/identity_credential",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256"]
}
},
"clientAuthenticationConfig": {
"supportedMethods": [
{
"type": "client-attestation",
"config": {
"verificationMethod": {
"type": "static-jwk",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
},
"clockSkewSeconds": 300,
"replayWindowSeconds": 300
}
}
]
}
}
Update an Existing Issuer
To add or modify client attestation configuration on an existing Issuer2 Service, use the configuration update endpoint.
Endpoint: /v1/{target}/resource-api/configuration | API Reference
Example Request
curl -X 'PUT' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/resource-api/configuration' \
-H 'accept: */*' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"clientAuthenticationConfig": {
"supportedMethods": [
{
"type": "client-attestation",
"config": {
"verificationMethod": {
"type": "kms-key",
"keyReference": "waltid.tenant1.kms1.attester-signing-key"
}
}
}
]
}
}'
Path Parameters
orgID– Your organization's Base URLtarget– The issuer2 service target ({organizationID}.{tenantID}.{issuer2ServiceID}), e.g.waltid.tenant1.issuer1
Authorization Server Metadata
The issuer’s OAuth 2.0 Authorization Server metadata (RFC 8414) reflects clientAuthenticationConfig:
token_endpoint_auth_methods_supportedincludesattest_jwt_client_authonly when aclient-attestationmethod is configured.- When attestation is advertised,
client_attestation_signing_alg_values_supportedandclient_attestation_pop_signing_alg_values_supportedare currently emitted as["ES256"]each (what wallets should use for those JWTs in this profile). pre_authorized_grant_anonymous_access_supportedistrueonly when apreauth-anonymousmethod is included. It isfalsewhen attestation alone is configured.
Example - attestation only:
{
"issuer": "https://myorg.enterprise-sandbox.waltid.dev/v2/waltid.tenant1.issuer1/issuer-service-api/openid4vci",
"token_endpoint": "https://myorg.enterprise-sandbox.waltid.dev/v2/waltid.tenant1.issuer1/issuer-service-api/openid4vci/token",
"token_endpoint_auth_methods_supported": ["attest_jwt_client_auth"],
"client_attestation_signing_alg_values_supported": ["ES256"],
"client_attestation_pop_signing_alg_values_supported": ["ES256"],
"pre_authorized_grant_anonymous_access_supported": false
}
Example - preauth-anonymous only (no attestation in metadata):
{
"issuer": "https://myorg.enterprise-sandbox.waltid.dev/v2/waltid.tenant1.issuer1/issuer-service-api/openid4vci",
"token_endpoint": "https://myorg.enterprise-sandbox.waltid.dev/v2/waltid.tenant1.issuer1/issuer-service-api/openid4vci/token",
"pre_authorized_grant_anonymous_access_supported": true
}
Wallets should read this metadata before the token request to decide whether to attach attestation headers, send client_id, or rely on anonymous pre-authorized code.
Verification Flow
When attestation headers are present (or required because client-attestation is the only usable method for the request), the issuer performs the following verification steps:
- Routing – If either attestation header is present, the issuer validates both headers together. If attestation is configured but neither header is present, the request is rejected unless the
preauth-anonymouspath applies (pre-authorized code grant withoutclient_id). Sending only one of the two headers fails validation. - Attestation JWT Signature – Verify the attestation JWT signature against the configured verification method
- Attestation JWT Type – Verify
typisoauth-client-attestation+jwt - Subject Claim – Verify
subis present and matchesclient_idif provided - Expiration – Verify
expis not in the past (with clock skew tolerance) - Confirmation Key – Extract
cnf.jwk(must not be a private key) - PoP JWT Signature – Verify the PoP JWT signature against
cnf.jwk - PoP JWT Type – Verify
typisoauth-client-attestation-pop+jwt - Audience – Verify
audmatches the issuer's RFC 8414 issuer identifier URL - Issuance Time – Verify
iatis within the replay window - Replay Protection – Verify
jtihas not been used before
Error Responses
When attestation verification fails, the issuer returns an OAuth 2.0 error response:
| Error Code | Description |
|---|---|
invalid_client | Token request does not match configured client authentication (for example missing attestation headers when client-attestation is required, only one of the two attestation headers present, invalid signatures, claim validation failure, replay detected, or attestation headers sent when this issuer does not accept them) |
use_fresh_attestation | Attestation JWT has expired |
Example Error Response
{
"error": "invalid_client",
"error_description": "Invalid client attestation JWT signature"
}
Best Practices
- Use KMS Key or X.509 Chain in Production – Static JWK is convenient for testing but harder to rotate
- Set Appropriate Validity Periods – Balance security (shorter) with usability (longer)
- Monitor for Replay Attacks – The issuer logs warnings when replay is detected
- Rotate Attester Keys Periodically – Update the verification method when rotating keys
- Use X.509 Chain for Regulated Environments – Required for VICAL, eIDAS, and similar frameworks
