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:
- Top-level shared material —
defaultIssuerKey,defaultIssuerDid,defaultIssuerX5chain. These are not applied automatically; profiles reuse them through HOCON substitution (see Shared defaults). 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
| Property | Type | Required | Description |
|---|---|---|---|
defaultIssuerKey | Object | No | Shared issuer signing key, referenced by profiles via ${defaultIssuerKey}. |
defaultIssuerDid | String | No | Shared issuer DID, referenced via ${defaultIssuerDid}. |
defaultIssuerX5chain | Array<String> | No | Shared X.509 certificate chain (PEM) for mDoc / x5c signing, referenced via ${defaultIssuerX5chain}. |
profiles | Object (map) | No | Credential 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
| Property | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Human-readable profile name (non-blank). |
credentialConfigurationId | String | Yes | Must match a key in credentialConfigurations in credential-issuer-metadata.conf. |
issuerKey | Object | Yes | Serialized signing key. Must contain a type field. |
credentialData | Object | Yes | The credential body template (claims). |
issuerDid | String | No | Issuer DID. Used by jwt_vc_json and (optionally) dc+sd-jwt. |
mapping | Object | No | Dynamic data functions for runtime fields. |
selectiveDisclosure | Object | No | SD-JWT selective-disclosure configuration. |
idTokenClaimsMapping | Object | No | Maps authorization-code-flow ID-token claims into the credential. |
mDocNameSpacesDataMappingConfig | Object | No | Per-namespace JSON→CBOR type conversions for mso_mdoc. |
x5Chain | Array<String> | No | X.509 certificate chain (PEM) for x5c-based signing. |
notifications | Object | No | Issuance webhook configuration. |
credentialStatus | Object | No | Credential 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
credentialConfigurationIdis 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:
| Format | Issuer 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:
type | Backend |
|---|---|
jwk | Local in-memory / file JWK. |
tse | HashiCorp Vault Transit Engine. |
aws-rest-api | AWS KMS. |
azure-rest-api | Azure Key Vault. |
oci-rest-api | Oracle Cloud KMS. |
The KMS backend identifiers are aws-rest-api, azure-rest-api and oci-rest-api — not aws, azure or oci. Using a short form is an unregistered type and fails at startup.
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_dateandexpiry_datemust be a CBORfull-date; timestamps must be atdate. - Binary values such as
portraitmust 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:
| Value | Effect |
|---|---|
stringToFullDate | Parses a date string into a CBOR full-date element. |
stringToTDate | Parses a date-time string into a CBOR tdate element. |
base64StringToByteString | Decodes standard Base64 into a CBOR byte string. |
base64UrlStringToByteString | Decodes 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 thecredentialStatusclaim: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 withidxanduri: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}
}
}
Related Configuration
- Credential Issuer Metadata – Declares the credential configurations referenced by
credentialConfigurationId - Issuer Service – Core service configuration
- Data Functions – Dynamic data population in
mapping
