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:

  1. The attestation JWT signature against a trusted attester key (or X.509 chain)
  2. The PoP JWT signature against the cnf.jwk from the attestation
  3. 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:

  1. Issuer2 Service – An existing Issuer2 Service in your tenant
  2. 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):

typePurpose
preauth-anonymousAllows OID4VCI pre-authorized code token requests without client_id when no attestation headers are sent. At most one entry.
client-attestationRequires 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-attestationconfig parameters

These fields apply to the nested object under client-attestation:

ParameterTypeDefaultDescription
verificationMethodObjectRequiredHow to verify the attestation JWT signature (see below)
clockSkewSecondsLong300Allowed clock skew for time-based validations (exp, iat)
replayWindowSecondsLong300Replay 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

CURL

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.

CURL

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 URL
  • target – 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_supported includes attest_jwt_client_auth only when a client-attestation method is configured.
  • When attestation is advertised, client_attestation_signing_alg_values_supported and client_attestation_pop_signing_alg_values_supported are currently emitted as ["ES256"] each (what wallets should use for those JWTs in this profile).
  • pre_authorized_grant_anonymous_access_supported is true only when a preauth-anonymous method is included. It is false when 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:

  1. 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-anonymous path applies (pre-authorized code grant without client_id). Sending only one of the two headers fails validation.
  2. Attestation JWT Signature – Verify the attestation JWT signature against the configured verification method
  3. Attestation JWT Type – Verify typ is oauth-client-attestation+jwt
  4. Subject Claim – Verify sub is present and matches client_id if provided
  5. Expiration – Verify exp is not in the past (with clock skew tolerance)
  6. Confirmation Key – Extract cnf.jwk (must not be a private key)
  7. PoP JWT Signature – Verify the PoP JWT signature against cnf.jwk
  8. PoP JWT Type – Verify typ is oauth-client-attestation-pop+jwt
  9. Audience – Verify aud matches the issuer's RFC 8414 issuer identifier URL
  10. Issuance Time – Verify iat is within the replay window
  11. Replay Protection – Verify jti has not been used before

Error Responses

When attestation verification fails, the issuer returns an OAuth 2.0 error response:

Error CodeDescription
invalid_clientToken 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_attestationAttestation JWT has expired

Example Error Response

{
  "error": "invalid_client",
  "error_description": "Invalid client attestation JWT signature"
}

Best Practices

  1. Use KMS Key or X.509 Chain in Production – Static JWK is convenient for testing but harder to rotate
  2. Set Appropriate Validity Periods – Balance security (shorter) with usability (longer)
  3. Monitor for Replay Attacks – The issuer logs warnings when replay is detected
  4. Rotate Attester Keys Periodically – Update the verification method when rotating keys
  5. Use X.509 Chain for Regulated Environments – Required for VICAL, eIDAS, and similar frameworks
Last updated on May 15, 2026