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 material — defaultIssuerKey, 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.
authorizedTransactionDataTypesArray<String>NoOID4VP transaction-data types this mDoc's device key may sign. Embedded in the MSO as KeyAuthorizations. See Authorized transaction data types.
msoDataObjectNoMSO ValidityInfo (validFrom, validUntil, expectedUpdate) for mso_mdoc. Static ISO-8601 or timestamp data functions, resolved at claim time. Rejected on other formats. See MSO validity.
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 = { ... } }
    }
    authorizedTransactionDataTypes = [   # array of strings (mso_mdoc only)
      "urn:eudi:sca:payment:1"
    ]
    msoData = {                          # object (mso_mdoc only)
      validFrom = "<timestamp>"
      validUntil = "<timestamp-in:365d>"
      expectedUpdate = "<timestamp-in:180d>"
    }
    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-api — not 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.

Authorized transaction data types

For mso_mdoc credentials that must later present OpenID4VP transaction_data, the device key can only device-sign types that appear in the MSO's KeyAuthorizations. Issuer2 embeds that grant from authorizedTransactionDataTypes.

authorizedTransactionDataTypes = ["urn:eudi:sca:payment:1"]

What the issuer writes into the MSO for each listed type:

  • Namespace = the transaction-data type string itself (walt.id convention, matching what the wallet presenter emits)
  • Data elements = transaction_data_hash and transaction_data_hash_alg
  • No blanket nameSpaces authorization — only those hash elements are granted

Blank entries are dropped, duplicates collapsed. If the list is null or empty after filtering, no KeyAuthorizations are emitted.

Presentation of an unlisted type is rejected for that mDoc. Register the same type in transaction-data-profiles.conf on verifier and wallet, and list it here on the issuing profile.

This field applies to mDoc (mso_mdoc) only. SD-JWT VC transaction-data binding uses the KB-JWT and does not consume authorizedTransactionDataTypes.

Example — demo SCA payment card profile (not a normative EUDI TS-12 SCA Attestation):

scaPaymentCardMdoc = {
  name = "SCA Payment Card (demo)"
  credentialConfigurationId = "sca_payment_card_mso_mdoc"
  issuerKey = ${defaultIssuerKey}
  credentialData = {
    "eu.europa.ec.eudi.sca.payment_card.1" = {
      card_scheme = "visa"
      card_last4 = "4242"
      pan_reference = "V0010013020000000000004301679"
      card_holder_name = "Jane Doe"
      expiry_date = "2028-12"
    }
  }
  authorizedTransactionDataTypes = ["urn:eudi:sca:payment:1"]
  x5Chain = ${defaultIssuerX5chain}
}

MSO validity (msoData)

For mso_mdoc profiles, msoData sets ISO 18013-5 MSO ValidityInfo. This is not W3C mapping.validFrom / validUntil and not the mDL namespace issue_date / expiry_date claims.

msoData = {
  validFrom = "<timestamp>"
  validUntil = "<timestamp-in:365d>"
  expectedUpdate = "<timestamp-in:180d>"
}

Each field accepts a static ISO-8601 timestamp or a timestamp data function, resolved at claim time. Defaults: validFrom equals the MSO signed time, validUntil is 365 days after claim, and expectedUpdate is omitted. validUntil must be after validFrom; validFrom cannot be before the signed time; if set, expectedUpdate must fall between them. signed is not configurable.

Offer runtimeOverrides.msoData field-merges onto the profile. Authorization-code requests without issuer_state (including offerless) inherit the profile msoData. Credential-request validUntil does not set MSO ValidityInfo. msoData is rejected on non-mDoc profiles.

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}
    msoData = {
      validFrom = "<timestamp>"
      validUntil = "<timestamp-in:365d>"
      expectedUpdate = "<timestamp-in:180d>"
    }
  }
}
Last updated on September 28, 2026