Issuer Integration
This guide explains how to configure the Issuer2 Service to require attestation-based client authentication when issuing credentials.
Overview
When client attestation is enabled on an Issuer2 Service, every token request must include valid attestation headers. The issuer verifies:
- The attestation JWT signature against a trusted attester key
- The PoP JWT signature against the
cnf.jwkfrom the attestation - Various claims (expiration, audience, timestamps, replay protection)
Prerequisites
Before configuring issuer attestation:
- Issuer2 Service – An existing Issuer2 Service in your tenant
- Attester Public Key – The public key of the Client Attestation Service that will sign attestations
Configuration Options
The clientAttestationConfig object on the Issuer2 Service controls attestation verification:
{
"clientAttestationConfig": {
"required": true,
"verificationMethod": { ... },
"clockSkewSeconds": 300,
"replayWindowSeconds": 300
}
}
| Parameter | Type | Default | Description |
|---|---|---|---|
required | Boolean | true | When true, attestation headers are mandatory. When false, headers are verified if present but not required. |
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 | Window for replay protection. PoP JWTs with iat older than this are rejected. |
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"]
}
},
"clientAttestationConfig": {
"required": true,
"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"]
}
},
"clientAttestationConfig": {
"required": true,
"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 '{
"clientAttestationConfig": {
"required": true,
"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
When client attestation is configured, the issuer's OAuth 2.0 Authorization Server metadata (RFC 8414) will advertise support for attestation-based client authentication:
{
"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",
"ES384",
"ES512",
"EdDSA"
]
}
Wallets can discover this metadata to determine if attestation is required before initiating the credential request flow.
Verification Flow
When a token request arrives, the issuer performs the following verification steps:
- Header Presence – Check that both
OAuth-Client-AttestationandOAuth-Client-Attestation-PoPheaders are present - 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 | Missing headers, invalid signature, claim validation failure, or replay detected |
use_fresh_attestation | Attestation JWT has expired |
Example Error Response
{
"error": "invalid_client",
"error_description": "Invalid client attestation JWT signature"
}
Lenient Mode
For development or gradual rollout, you can set required: false to enable lenient mode:
{
"clientAttestationConfig": {
"required": false,
"verificationMethod": { ... }
}
}
In lenient mode:
- Requests without attestation headers are accepted
- Requests with valid attestation headers are accepted
- Requests with invalid attestation headers are accepted (with a warning logged)
Lenient mode should only be used for development and testing. Production deployments should always use required: true.
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
