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, PAR and 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" }
        }
      }
    ]
  }
}

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

A client-attestation entry requires one verificationMethod.

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": "key-reference",
    "reference": "waltid.tenant1.kms1.attester-signing-key"
  }
}

The reference must be a non-empty KMS Key resource target that the issuer deployment can resolve.

X.509 Certificate Chain

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://{orgID}.enterprise-sandbox.waltid.dev",
    "kms": "waltid.tenant1.kms1",
    "tokenKeyId": "waltid.tenant1.kms1.tokenKey",
    "credentialConfigurations": {
      "identity_credential": {
        "format": "dc+sd-jwt",
        "scope": "identity_credential",
        "vct": "{vctBaseURL}/identity_credential",
        "cryptographic_binding_methods_supported": ["jwk"],
        "credential_signing_alg_values_supported": ["ES256"],
        "proof_types_supported": {
          "jwt": {
            "proof_signing_alg_values_supported": ["ES256"]
          }
        }
      }
    },
    "clientAuthenticationConfig": {
      "supportedMethods": [
        {
          "type": "client-attestation",
          "config": {
            "verificationMethod": {
              "type": "key-reference",
              "reference": "waltid.tenant1.kms1.attester-signing-key"
            }
          }
        }
      ]
    }
  }'

Body

{
  "type": "issuer2",
  "baseUrl": "https://{orgID}.enterprise-sandbox.waltid.dev",
  "kms": "waltid.tenant1.kms1",
  "tokenKeyId": "waltid.tenant1.kms1.tokenKey",
  "credentialConfigurations": {
    "identity_credential": { "...": "..." }
  },
  "clientAuthenticationConfig": {
    "supportedMethods": [
      { "type": "client-attestation", "config": { "...": "..." } }
    ]
  }
}

Update an Existing Issuer

To add or modify clientAuthenticationConfig on an existing Issuer2 Service, first retrieve its complete configuration. Keep the existing fields and add or replace the clientAuthenticationConfig block.

CURL

Endpoint: /v2/{target}/issuer-service-api/configuration/update | API Reference

Example Request

curl -X 'PUT' \
  'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{target}/issuer-service-api/configuration/update' \
  -H 'accept: */*' \
  -H 'Authorization: Bearer {yourToken}' \
  -H 'Content-Type: application/json' \
  -d '{
  "...": "...",
  "clientAuthenticationConfig": {
    "supportedMethods": [
      {
        "type": "client-attestation",
        "config": {
          "verificationMethod": {
            "type": "key-reference",
            "reference": "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

Client-attestation failures return an OAuth 2.0 invalid_client error response. This includes missing or duplicate headers, malformed JWTs, unsupported algorithms, invalid or untrusted signatures, expired attestations, invalid claims, a mismatched client_id, and invalid PoP JWTs.

Error CodeDescription
invalid_clientClient authentication is missing, malformed, unsupported, or failed attestation or PoP validation. Expired attestations also use this error code.

Example Error Response

{
  "error": "invalid_client",
  "error_description": "Client attestation PoP signature is invalid"
}

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 August 24, 2026