Issuer Profiles Configuration

The issuer2-profiles.conf file defines credential profiles — templates that bind a credential type (declared in credential-issuer-metadata.conf) to a concrete signing key, an issuer identifier, and the data that goes into the credential. All of the data defined in the profile can also be later overwritten using runtime overwrites when creating a credential offer.

The file has two parts:

  1. Top-level shared materialdefaultIssuerKey, defaultIssuerDid, defaultIssuerX5chain. These are not applied automatically; profiles reuse them through HOCON substitution (see Shared defaults).
  2. profiles — a map of named profiles.

Each profile is parsed into a strongly-typed object at startup and validated, so an invalid entry stops the service from loading.

File Location

waltid-services/waltid-issuer-api2/config/issuer2-profiles.conf

Configuration Options

PropertyTypeRequiredDescription
defaultIssuerKeyObjectNoShared issuer signing key, referenced by profiles via ${defaultIssuerKey}.
defaultIssuerDidStringNoShared issuer DID, referenced via ${defaultIssuerDid}.
defaultIssuerX5chainArray<String>NoShared X.509 certificate chain (PEM) for mDoc / x5c signing, referenced via ${defaultIssuerX5chain}.
profilesObject (map)NoCredential profiles, keyed by profile ID.

Shared defaults

The top-level defined defaultIssuerKey, defaultIssuerDid and defaultIssuerX5chain keys are global configuration values which can be used across multiple profiles via the ${...} syntax:

defaultIssuerKey = {
  type = "jwk"
  jwk = {
    kty = "EC"
    d   = "..."
    crv = "P-256"
    x   = "..."
    y   = "..."
  }
}

defaultIssuerDid = "did:jwk:eyJrdHk..."

profiles = {
  identityCredential = {
    name = "IdentityCredential"
    credentialConfigurationId = "IdentityCredential_jwt_vc_json"
    issuerKey = ${defaultIssuerKey}   # <- substitution
    issuerDid = ${defaultIssuerDid}   # <- substitution
    credentialData = { ... }
  }
}

This pattern is just a convenience for sharing one key across many profiles. A profile may equally declare its own inline issuerKey / issuerDid instead of referencing the defaults.

Profile properties

PropertyTypeRequiredDescription
nameStringYesHuman-readable profile name (non-blank).
credentialConfigurationIdStringYesMust match a key in credentialConfigurations in credential-issuer-metadata.conf.
issuerKeyObjectYesSerialized signing key. Must contain a type field.
credentialDataObjectYesThe credential body template (claims).
issuerDidStringNoIssuer DID. Used by jwt_vc_json and (optionally) dc+sd-jwt.
mappingObjectNoDynamic data functions for runtime fields.
selectiveDisclosureObjectNoSD-JWT selective-disclosure configuration.
idTokenClaimsMappingObjectNoMaps authorization-code-flow ID-token claims into the credential.
mDocNameSpacesDataMappingConfigObjectNoPer-namespace JSON→CBOR type conversions for mso_mdoc.
x5ChainArray<String>NoX.509 certificate chain (PEM) for x5c-based signing.
notificationsObjectNoIssuance webhook configuration.
credentialStatusObjectNoCredential status (revocation) configuration.

name, credentialConfigurationId, issuerKey and credentialData are all required — a profile that omits any of them stops the service from loading. issuerKey must contain a type field, and credentialConfigurationId must reference a credential configuration that actually exists in the metadata file.

Where each field sits

Every property in the table above is a direct child of a single profile — they all sit at the same level, inside one entry of the profiles map. The skeleton below shows that layout (the sections further down zoom into each block):

profiles = {

  myProfile = {                          # profile ID (left of the "=")

    # ---- required ----
    name = "..."                         # string
    credentialConfigurationId = "..."    # string, must exist in credential-issuer-metadata.conf
    issuerKey = { type = "...", ... }    # object
    credentialData = { ... }             # object (the credential body template)

    # ---- optional ----
    issuerDid = "did:..."                # string
    mapping = { ... }                    # object (data functions)
    selectiveDisclosure = {              # object (SD-JWT only)
      fields = { ... }
      decoyMode = "NONE"
      decoys = 0
    }
    idTokenClaimsMapping = {             # object: "<id-token path>" = "<credentialData path>"
      "$.given_name" = "$.given_name"
    }
    mDocNameSpacesDataMappingConfig = {  # object (mso_mdoc only), keyed by namespace
      "org.iso.18013.5.1" = { entriesConfigMap = { ... } }
    }
    x5Chain = [ "..." ]                  # array of PEM strings
    notifications = {                    # object
      webhook = { url = "..." }
    }
    credentialStatus = { ... }           # object
  }
}

Profile ID and uniqueness rules

Inside the profiles block, each entry's name on the left of the = is its profile ID. In the example below, identityCredential is the profile ID:

profiles = {
  identityCredential = {   # <- "identityCredential" is the profile ID
    name = "IdentityCredential"
    # ...
  }
}
  • A profile ID must be non-blank and must not contain a . character.
  • Only one profile per credentialConfigurationId is allowed — two profiles pointing at the same configuration ID will break issuance for that credential.

Signing: what's needed for each credential type

Which signing fields a profile needs depends on the credential format:

FormatIssuer mode
jwt_vc_json (W3C JWT VC)issuerKey + issuerDid (no x5Chain).
mso_mdoc (mDoc)issuerKey + x5Chain (no issuerDid).
dc+sd-jwt (SD-JWT VC)issuerKey + exactly one of issuerDid or x5Chain.

Issuer Key

A serialized walt.id key. The type field selects the backend and is required. The same key backends apply as elsewhere in walt.id:

typeBackend
jwkLocal in-memory / file JWK.
tseHashiCorp Vault Transit Engine.
aws-rest-apiAWS KMS.
azure-rest-apiAzure Key Vault.
oci-rest-apiOracle Cloud KMS.

The KMS backend identifiers are aws-rest-api, azure-rest-api and oci-rest-apinot aws, azure or oci. Using a short form is an unregistered type and fails at startup.

Local JWK
HashiCorp Vault
AWS KMS
Azure Key Vault

The jwk field may be written as a nested HOCON object (the form used by the shipped config):

issuerKey = {
  type = "jwk"
  jwk = {
    kty = "EC"
    d   = "..."
    crv = "P-256"
    x   = "..."
    y   = "..."
  }
}

Credential Data

credentialData is the static data template for the credential body. Fields that should be computed per issuance are filled in by data functions defined in the mapping object such as <uuid>, <timestamp>, <timestamp-in:365d>, <issuerDid> and <subjectDid>:

credentialData = {
  "@context" = ["https://www.w3.org/2018/credentials/v1"]
  type = ["VerifiableCredential", "IdentityCredential"]
  credentialSubject = {
    id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
    given_name = "John"
    family_name = "Doe"
  }
  issuer = { id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION" }
}
mapping = {
  id = "<uuid>"
  issuer = { id = "<issuerDid>" }
  credentialSubject = { id = "<subjectDid>" }
  issuanceDate = "<timestamp>"
  expirationDate = "<timestamp-in:365d>"
}

For SD-JWT VC, time claims are typically driven with the seconds variants:

mapping = {
  id  = "<uuid>"
  iat = "<timestamp-seconds>"
  nbf = "<timestamp-seconds>"
  exp = "<timestamp-in-seconds:365d>"
}

Selective Disclosure

For dc+sd-jwt credentials, declare which claims are selectively disclosable. Each field carries sd (boolean), and nested objects can be described with children. The block may also configure decoy digests:

selectiveDisclosure = {
  fields = {
    birth_date  = { sd = true }
    family_name = { sd = false }
    address = {
      sd = true
      children = {
        fields = {
          street = { sd = true }
          city   = { sd = true }
        }
      }
    }
  }
  decoyMode = "NONE"   # NONE | FIXED | RANDOM
  decoys = 0
}

ID Token Claim Mapping

For the authorization-code flow, copy claims from the IdP's ID token into the credential. Each entry is "<source>" = "<destination>":

  • The key is a JSONPath into the ID token (the source of the value).
  • The value is a JSONPath into credentialData (where the value is written).
idTokenClaimsMapping = {
  # read $.family_name from the ID token, write it to $.registered_family_name in credentialData
  "$.family_name" = "$.registered_family_name"
  "$.given_name"  = "$.registered_given_name"
}

Both sides must be full JSONPath expressions (starting with $), and both must already resolve to a non-null value — the source path must exist in the ID token, and the destination path must already exist in your credentialData template. The mapping updates an existing field; it does not create new ones. Bare keys like given_name or address.street are not valid here.

mDocNameSpacesDataMappingConfig

Why this is needed

mDoc credentials (mso_mdoc) are CBOR-encoded as defined by ISO/IEC 18013-5, and the standard mandates a specific CBOR type for certain data elements:

  • Dates such as birth_date, issue_date and expiry_date must be a CBOR full-date; timestamps must be a tdate.
  • Binary values such as portrait must be a CBOR byte string (bstr).

But you author credentialData as JSON, which has no native date or byte-string type — those values arrive as plain text strings. Without instructions, the issuer encodes them as CBOR text strings, which is not standards-conformant: the resulting credential does not match the CBOR types that ISO/IEC 18013-5 (and profiles built on it, such as the EUDI PID Rulebook) require for those data elements.

mDocNameSpacesDataMappingConfig is what tells the issuer which JSON string fields to re-encode into the correct CBOR type.

Is it required?

The field itself is optional — but it is effectively required for any data element the standard defines as a non-text type (dates, binary). Plain text and numeric fields (e.g. family_name, document_number) need no entry; only list the fields that require a conversion.

Syntax

credentialData is organized by namespace, and each namespace maps to an entriesConfigMap keyed by claim name:

mDocNameSpacesDataMappingConfig = {
  "org.iso.18013.5.1" = {
    entriesConfigMap = {
      birth_date = { type = "string", conversionType = "stringToFullDate" }
      issue_date = { type = "string", conversionType = "stringToFullDate" }
      portrait   = { type = "string", conversionType = "base64StringToByteString" }

      # Arrays and nested objects are described recursively:
      driving_privileges = {
        type = "array"
        arrayConfig = [
          {
            type = "object"
            entriesConfigMap = {
              issue_date  = { type = "string", conversionType = "stringToFullDate" }
              expiry_date = { type = "string", conversionType = "stringToFullDate" }
            }
          }
        ]
      }
    }
  }
}

Supported conversionType values:

ValueEffect
stringToFullDateParses a date string into a CBOR full-date element.
stringToTDateParses a date-time string into a CBOR tdate element.
base64StringToByteStringDecodes standard Base64 into a CBOR byte string.
base64UrlStringToByteStringDecodes Base64URL into a CBOR byte string.

Notifications

Configure an issuance webhook, learn more here:

notifications = {
  webhook = {
    url = "https://example.com/webhook/issuance"
  }
}

Credential Status

Embed a status-list reference into every credential issued from this profile (e.g. for revocation/suspension). The accepted shape is format-specific:

  • W3C JWT VC (jwt_vc_json) — the object is embedded verbatim as the credentialStatus claim:
    credentialStatus = {
      type = "BitstringStatusListEntry"
      id = "https://issuer.example.com/status/1#94567"
      statusPurpose = "revocation"
      statusListIndex = "94567"
      statusListCredential = "https://issuer.example.com/status/1"
    }
    
  • SD-JWT VC (dc+sd-jwt) and mDoc (mso_mdoc) — a Token Status List reference with idx and uri:
    credentialStatus = {
      status_list = {
        idx = 94567
        uri = "https://issuer.example.com/status/1"
      }
    }
    

In the Community Stack you host and serve the status list yourself — the issuer only embeds this reference into the credential. The same value can also be supplied as a per-offer runtime override. See Credential Status for the full reference across all formats.

Example Configuration

# issuer2-profiles.conf

defaultIssuerKey = {
  type = "jwk"
  jwk = {
    kty = "EC"
    d   = "..."
    crv = "P-256"
    x   = "..."
    y   = "..."
  }
}

defaultIssuerDid = "did:jwk:eyJrdHk..."

defaultIssuerX5chain = [
  """-----BEGIN CERTIFICATE-----
MIIBeTCCAR8CFHrWgrGl5KdefSvRQhR...
-----END CERTIFICATE-----"""
]

profiles = {

  # W3C JWT VC — uses issuerDid
  identityCredential = {
    name = "IdentityCredential"
    credentialConfigurationId = "IdentityCredential_jwt_vc_json"
    issuerKey = ${defaultIssuerKey}
    issuerDid = ${defaultIssuerDid}
    credentialData = {
      "@context" = ["https://www.w3.org/2018/credentials/v1"]
      type = ["VerifiableCredential", "IdentityCredential"]
      credentialSubject = {
        id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
        given_name = "John"
        family_name = "Doe"
      }
      issuer = { id = "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION" }
    }
    mapping = {
      id = "<uuid>"
      issuer = { id = "<issuerDid>" }
      credentialSubject = { id = "<subjectDid>" }
      issuanceDate = "<timestamp>"
      expirationDate = "<timestamp-in:365d>"
    }
  }

  # mDoc — uses x5Chain, no issuerDid
  mDL = {
    name = "ISO 18013-5 mDL"
    credentialConfigurationId = "org.iso.18013.5.1.mDL"
    issuerKey = ${defaultIssuerKey}
    credentialData = {
      "org.iso.18013.5.1" = {
        family_name = "Mustermann"
        given_name  = "Erika"
        birth_date  = "1971-09-01"
      }
    }
    mDocNameSpacesDataMappingConfig = {
      "org.iso.18013.5.1" = {
        entriesConfigMap = {
          birth_date = { type = "string", conversionType = "stringToFullDate" }
        }
      }
    }
    x5Chain = ${defaultIssuerX5chain}
  }
}
Last updated on July 1, 2026