---
id: openid4vc-key-management
title: Key Management - Signing Keys and Certificates
hide_title: false
description: Configure the secure vaults of the Organisation Wallet Suite, create signing keys, generate a CSR, get an X.509 certificate, upload the certificate chain and list your keys.
keywords: [key management, signing key, secure vault, Hashicorp Vault, QTSP, CSR, X.509 certificate, certificate chain, x5c, trust list, Organisation Wallet, EUDI Wallet]
sidebar_label: Key Management
slug: /openid4vc-key-management/
---

> **Build this with an AI coding agent.** Install the iGrant.io Agent Skills, then ask your agent to build the integration:
>
> ```bash
> npx skills add L3-iGrant/skills
> ```


import NoteBox from '@site/src/components/NoteBox';
import { ApiKeyManager } from '@site/src/components/ApiKeyManager';
import DcqlStepTemplate from '@site/src/components/DCQL/DcqlStepTemplate';
import TestCertificateAuthority, { CSR_CREATED_EVENT } from '@site/src/components/KeyManagement/TestCertificateAuthority';
import UploadCertificateChain, { KEY_CREATED_EVENT } from '@site/src/components/KeyManagement/UploadCertificateChain';
import { refreshOrganisationKeys } from '@site/src/hooks/useOrganisationSigningKey';

The Organisation Wallet Suite signs with the keys of your organisation. A key has one of four purposes, and you set each one in a different request:

| Key | What it signs | Where you set it | Certificate profile |
|---|---|---|---|
| Credential signing key | The credentials that you issue (SD-JWT VC, W3C VC, mso_mdoc) | Credential definition: `cryptographicKeyFormat` and `cryptographicKeyIdentifier`, for the whole definition or for one entry of `credentialDefinitions` | Credential issuer; mdoc issuer (Document Signer) for `mso_mdoc` |
| Status list signing key | The status list token that tells verifiers if a credential is revoked or suspended | Credential definition, in an entry of `credentialDefinitions`: `statusListCryptographicKeyFormat` and `statusListCryptographicKeyIdentifier`. Leave them out to use the credential signing key | Credential issuer. For `mso_mdoc`: `x509` from the same CA as the Document Signer, without extended key usage (ISO/IEC 18013-5 Table B.9) |
| Verification request signing key | The OpenID4VP request object of a verifier | Presentation definition: `cryptographicKeyFormat` and `cryptographicKeyIdentifier` | Relying party (WRPAC); mdoc verifier for ISO/IEC 18013-7 Annex C |
| Issuer metadata signing key | The Credential Issuer Metadata, when you publish it signed (`signed_metadata`) | Global configuration: `issuerMetadata.signedMetadataEnabled`, `issuerMetadata.cryptographicKeyFormat` and `issuerMetadata.cryptographicKeyIdentifier` | Credential issuer |

Each key signs with `did:key` or with `x509`. With `x509` the signature carries the certificate chain (`x5c`), and EUDI Wallets check it against a trust list: an issuer list (EAA, QEAA, Pub-EAA or PID) for the credential, status list and metadata keys, and the WRPAC list for the verification request key. When the certificate is on the list, the wallet shows your organisation as trusted. Use `did:key` for tests only. `mso_mdoc` always needs `x509`.

**Choosing a key in a workflow:** in the request panels of the developer guides, each signing key field has a key label next to it, for example **Credential signing key** after `cryptographicKeyIdentifier`. Click it to list the keys from Step 7, see which have a certificate and its profile, and write the key into that field and its format field. A credential definition can have a different key for each purpose. Hover a field name to see what the field does.

This guide creates a key and gives it a certificate:

1. Configure the secure vaults that hold the keys.
2. Create a key.
3. Generate a certificate signing request (CSR) for the key.
4. Get an X.509 certificate for the CSR, with the profile of the purpose.
5. Upload the certificate chain to the key.
6. List the keys and check which ones have a certificate.
7. Register the certificate in a trust list.

### Step 1: Get the API Key (Admin)

To obtain your API key, please contact [support@igrant.io](mailto:support@igrant.io?subject=Request%20API%20Key). Once you have received your API key, enter it in the field below and click the **Set API Key** button to save it for future use.

<ApiKeyManager />

### Step 2: Configure the Secure Vaults (Admin)

A secure vault holds the private keys. The private key never leaves the vault: the API returns public keys only. Your organisation can use these vaults:

| Key management service | Use it when |
|---|---|
| Local Vault | You want the platform to hold the keys. Always enabled. |
| External Vault | Your keys are in your own Hashicorp Vault. |
| Qualified Trust Service Provider (QTSP) | You sign qualified electronic signatures with a QTSP over the Cloud Signature Consortium (CSC) API. |

Read the current configuration first. Then select a sample, change it to your values, and run it. Every organisation starts with the Local Vault, so this step uses the [Update Secure Vault](/docs/openid4vc-api/config-digital-wallet-open-id-update-secure-vault/) API (`PUT`). The [Configure Secure Vault](/docs/openid4vc-api/config-digital-wallet-open-id-configure-secure-vault/) API (`POST`) works only for an organisation without a vault configuration.

**Read the configuration**

<DcqlStepTemplate
  endpointPath="/v2/config/digital-wallet/openid/key-management"
  method="GET"
  initialJsonData={{}}
/>

**Update the vaults**

<DcqlStepTemplate
  endpointPath="/v2/config/digital-wallet/openid/key-management"
  method="PUT"
  jsonOptionLabel="Vault setup"
  jsonOptions={[
    {
      value: 'local',
      label: 'Local Vault only',
      json: {
        igrantioVault: { enabled: true },
        hashicorpVault: { enabled: false },
        qtsp: { enabled: false },
      },
    },
    {
      value: 'hashicorp',
      label: 'Local Vault and External Vault (Hashicorp)',
      json: {
        igrantioVault: { enabled: true },
        hashicorpVault: {
          enabled: true,
          vaultAddress: 'https://vault.example.com:8200',
          vaultNamespace: '<yourNamespace>',
          vaultUsername: '<yourUsername>',
          vaultPassword: '<yourPassword>',
        },
        qtsp: { enabled: false },
      },
    },
    {
      value: 'qtsp',
      label: 'Local Vault and Qualified Trust Service Provider (CSC API)',
      json: {
        igrantioVault: { enabled: true },
        hashicorpVault: { enabled: false },
        qtsp: {
          enabled: true,
          cscUrl: 'https://csc.example-qtsp.eu/csc/v2',
          cscApiVersion: 'v2',
          clientId: '<yourClientId>',
          clientSecret: '<yourClientSecret>',
          userID: '<yourUserId>',
        },
      },
    },
  ]}
/>

For a QTSP, select the signing credential after this step with the [List QTSP Credentials](/docs/openid4vc-api/config-digital-wallet-open-id-list-qtsp-credential/) and [Configure QTSP Credential](/docs/openid4vc-api/config-digital-wallet-open-id-configure-qtsp-credential/) APIs. The QTSP holds that key, so Steps 3 to 5 do not apply to it.

### Step 3: Create a Key (Admin)

Create an ECDSA P-256 key in the Local Vault (`vaultType` `1`). The API does not create keys in an External Vault or at a QTSP. The vault sets the key type, the curve (`P-256`) and the algorithm (`ES256`). Run the request. Alternatively, use the [Create Key](/docs/openid4vc-api/config-digital-wallet-open-id-create-key/) API. The response has the `keyId`, which the next steps fill in for you.

<DcqlStepTemplate
  endpointPath="/v2/config/digital-wallet/openid/key-management/keys"
  method="POST"
  initialJsonData={{ vaultType: 1 }}
  emitResultEvent={KEY_CREATED_EVENT}
  extractResultEvent={(result) => (result?.keyId ? { keyId: result.keyId } : null)}
  onSuccess={refreshOrganisationKeys}
/>

### Step 4: Generate a CSR (Admin)

The certificate signing request (CSR) carries the public key of the key from Step 3 and the details of your organisation. `commonName` is required. `organization`, `country` (2 letter code), `sanDns` and `sanUri` are optional in the API, but give `country`: the certificate profiles below need it, and ISO/IEC 18013-5 requires it for mdoc. Put the domain of your service in `sanDns`: the `x509_san_dns` client ID prefix of OpenID4VP needs it. Alternatively, use the [Generate CSR](/docs/openid4vc-api/config-digital-wallet-open-id-generate-csr/) API.

<DcqlStepTemplate
  endpointPath="/v2/config/digital-wallet/openid/key-management/keys/{keyId}/csr"
  method="POST"
  listenPathParamsEvent={KEY_CREATED_EVENT}
  initialJsonData={{
    commonName: 'My Organisation',
    organization: 'My Organisation',
    country: 'SE',
    sanDns: ['verifier.example.com'],
    sanUri: ['https://verifier.example.com'],
  }}
  emitResultEvent={CSR_CREATED_EVENT}
  extractResultEvent={(result) => (result?.csr ? { csr: result.csr } : null)}
/>

### Step 5: Get a Certificate for the CSR

For production, send the CSR to your Certificate Authority (CA) or QTSP, and ask for the certificate profile of your use. For tests, use one of the two options below. Both give a chain with the leaf certificate first, then the CA certificate. That is the order that Step 6 needs.

Select the certificate profile for what the key signs:

| Profile | Use the key for | Extensions of the leaf certificate |
|---|---|---|
| Credential issuer | SD-JWT VC and W3C VC credentials (PID, EAA, QEAA, Pub-EAA), status lists, signed issuer metadata | Base set |
| mdoc issuer | `mso_mdoc` credentials, such as mDL (ISO/IEC 18013-5 Document Signer) | Base set, extended key usage `1.0.18013.5.1.2` (critical), CRL distribution point |
| Relying party | Signed OpenID4VP requests of a verifier (WRPAC, ETSI TS 119 411-8) | Base set, extended key usage `clientAuth`, certificate policy `0.4.0.194118.1.2` |
| mdoc verifier | ISO/IEC 18013-7 Annex C requests (ISO/IEC 18013-5 reader authentication) | Base set, extended key usage `1.0.18013.5.1.6` (critical) |
| mdoc status list signer | Status lists of `mso_mdoc` credentials (ISO/IEC 18013-5 Table B.9) | Base set, signed by the same CA as the Document Signer. Use the credential issuer profile |

The base set for every profile: key usage `digitalSignature` (critical), subject and authority key identifiers, the subject alternative names of the CSR, an issuer alternative name with a contact email, and no basic constraints. The test CA has the shape of an ISO/IEC 18013-5 IACA root: country in the subject, basic constraints `CA:TRUE` with path length 0, key usage `keyCertSign` and `cRLSign`, and an issuer alternative name. The leaf is valid for 397 days, within the 457 days that ISO/IEC 18013-5 allows a Document Signer.

**Option A: test CA in your browser**

Select the profile and choose **Sign CSR**. The page creates a test CA in your browser and signs the CSR from Step 4 with it. Every key that you sign on this page chains to the same test CA, until you reload the page. Nothing leaves the page.

<NoteBox title="Note:" variant="caution">
The test CA is for tests only. Wallets do not trust it, and the CA key is gone when you reload the page. Download <code>test-ca.pem</code> if you want to keep the CA certificate.
</NoteBox>

<TestCertificateAuthority />

**Option B: test CA on your machine with OpenSSL 3**

Save the CSR from Step 4 as `request.csr`, then run:

```bash
# 1. Create a test CA (once), in the shape of an ISO/IEC 18013-5 IACA root.
#    Use the country of your CSR.
openssl ecparam -name prime256v1 -genkey -noout -out test-ca.key
openssl req -x509 -new -key test-ca.key -sha256 -days 825 -out test-ca.pem \
  -subj "/C=SE/CN=My Test CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "issuerAltName=email:admin@example.com"

# 2. The base set of extensions for every leaf certificate
cat > leaf.ext <<'EXT'
keyUsage=critical,digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
issuerAltName=email:admin@example.com
EXT

# 3. Add the lines of your profile to leaf.ext (credential issuer: add nothing)
# mdoc issuer (Document Signer):
#   extendedKeyUsage=critical,1.0.18013.5.1.2
#   crlDistributionPoints=URI:https://example.com/crl/test-ca.crl
# Relying party (WRPAC):
#   extendedKeyUsage=clientAuth
#   certificatePolicies=0.4.0.194118.1.2
# mdoc verifier (reader authentication):
#   extendedKeyUsage=critical,1.0.18013.5.1.6

# 4. Check the CSR, then sign it. -copy_extensions keeps the subject alternative names of the CSR.
openssl req -in request.csr -noout -verify
openssl x509 -req -in request.csr -CA test-ca.pem -CAkey test-ca.key -CAcreateserial \
  -days 397 -sha256 -copy_extensions copy -extfile leaf.ext -out leaf.pem

# 5. Build the chain: leaf first, then the CA. Then check it.
cat leaf.pem test-ca.pem > chain.pem
openssl verify -CAfile test-ca.pem leaf.pem
openssl x509 -in leaf.pem -noout -text
```

### Step 6: Upload the Certificate Chain (Admin)

Upload the chain to the key. The key ID and the chain from the earlier steps are filled in for you; you can also paste your own. The file must be PEM text with the leaf certificate first, then the intermediate certificates, then the root certificate. Alternatively, use the [Upload Certificate Chain](/docs/openid4vc-api/config-digital-wallet-open-id-upload-certificate-chain/) API.

The service refuses the file when it holds a private key, when the leaf certificate does not use the P-256 curve, when a certificate is expired or not yet valid, or when a chain signature is not correct.

<UploadCertificateChain />

The same request with curl:

```bash
curl -X POST "https://demo-api.igrant.io/v2/config/digital-wallet/openid/key-management/keys/$KEY_ID/certificate-chain" \
  -H "Authorization: ApiKey $API_KEY" \
  -F "certificate_file=@chain.pem"
```

The response has `key_id`, `certificates_count`, `x5t` and `x5t_s256`. The key now has the chain as its `x5c` value.

### Step 7: List the Keys (Admin)

List the keys of all vaults. Each key has its public JWK, `isDefault` (the wallet signs with this key by default) and its DIDs. A key with a certificate chain has `x5c`, `x5t` and `x5t#S256` in its JWK. Alternatively, use the [List Keys](/docs/openid4vc-api/config-digital-wallet-open-id-list-keys/) API.

<DcqlStepTemplate
  endpointPath="/v2/config/digital-wallet/openid/key-management/keys"
  method="GET"
  initialJsonData={{}}
/>

### Step 8: Register the Certificate in a Trust List

A wallet trusts your organisation when your certificate is on a trust list. To add your certificate to the NXD trust list, follow [Register as a Wallet-Relying Party](/docs/trust-relying-party-registration/) and send the request to [support@igrant.io](mailto:support@igrant.io?subject=Trust%20list%20registration). Use the leaf certificate from Step 5. To learn how trust lists work, see [Trust in the Wallet Ecosystem](/docs/trust-overview/).

| You are | Trust list |
|---|---|
| Issuer of (Q)EAAs, Pub-EAAs or PIDs | EAA, QEAA, Pub-EAA or PID list |
| Verifier (relying party) | Wallet-Relying Party Access Certificate (WRPAC) list |
