HashiCorp Vault Transit Secrets Engine

The issuer API can leverage HashiCorp Vault's Transit Secrets Engine to hook into the "cryptography as a service" or "encryption as a service" features provided by HashiCorp Vault. With that, the issuer API can sign and verify credentials whilst keeping the secrets (signing keys) in an external secure environment.

If you are new to HashiCorp Vault, make sure to check out the guides here. The rest of this document assumes you already know how Vault works and have a transit secrets engine setup and running.

Setting Up Authentication Method

Once you have the HashiCorp Server up and running and enabled the transit secrets engine mentioned above, we need to either enable AppRole Authentication or User/Pass Authentication to use HashiCorp Vault with the walt.id issuer API.

Setup Policy

Before enabling and configuring authentication methods, a policy must be created to define the permissions and actions that authenticated users or roles are allowed to perform.

Create The Policy File

echo 'path "transit/*" {
  capabilities = ["create", "update", "read", "delete", "list"]
}' > transit-policy.hcl

Check the Client Configuration

If you are configuring vault locally make sure that the client is configured to communicate via HTTP. This can be achieved by setting the VAULT_ADDR environment variable.

export VAULT_ADDR=https://127.0.0.1:8200

Upload the Policy

vault policy write transit-policy transit-policy.hcl
# Expected result:
# Success! Uploaded policy: transit-policy

Setup Auth Method

AppRole Auth
User/Pass Auth

To enable AppRole auth you have two options, the Web UI of HashiCorp Vault or the CLI:

Web UI
CLI
  1. Navigate to Access.
  2. Choose Authentication Methods.
  3. Click on Enable new method.
  4. Select Generic / AppRole.
  5. Click Enable method.

Please note: You might want to disable lockout: select the created approle method, press "Configure", scroll to "User lockout configuration" and select "Disable lockout for this mount".

Configure AppRole

Add a Role

vault write auth/approle/role/my-role token_type=batch
# Additional parameters can include:
# secret_id_ttl, token_num_uses, token_ttl, token_max_ttl, secret_id_num_uses

Show Role ID

vault read auth/approle/role/my-role/role-id
# Save the value of role_id for later use.

Issue a Secret ID

vault write -f auth/approle/role/my-role/secret-id
# Save the value of secret_id for later use.

Associate Policy with AppRole

vault write auth/approle/role/my-role token_policies="transit-policy"
# Expected result:
# Success! Data written to: auth/approle/role/my-role

If you already have a token policy with the AppRole:

vault write auth/approle/role/my-role token_policies="existing-policy,transit-policy"

Key Creation

To create the key you can use the onboard endpoint provided by the issuer API or the CLI, UI or the API from Vault's Transit Secrets Engine.

The issuer API accepts walt.id keyType values and maps them to the matching HashiCorp Vault Transit key type before creating or resolving the key.

walt.id keyTypeVault Transit key type
Ed25519ed25519
secp256r1ecdsa-p256
secp384r1ecdsa-p384
secp521r1ecdsa-p521
RSArsa-2048
RSA3072rsa-3072
RSA4096rsa-4096

secp256k1 is not supported by the TSE integration. See the HashiCorp Vault Transit Secrets Engine key types full list for the provider-side names.

Creation via Issuer API

CURL

Endpoint: /onboard/issuer | API Reference

Example Request

AppRole Auth
User/Pass Auth
AccessToken Auth (Deprecated)
curl -X 'POST' \
  'https://issuer.demo.walt.id/onboard/issuer' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "roleId": "my_role",
        "secretId": "my_secret"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}'

Body

{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "roleId": "my_role",
        "secretId": "my_secret"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}

Body Parameters

  • key
    • backend: String - Specifies the storage type of key. It can be jwk (managed by you), TSE (managed by HashiCorp Vault) and others. Learn more about different types here.
    • keyType: String - the algorithm used to generate the key. For Vault TSE, use one of Ed25519, secp256r1, secp384r1, secp521r1, RSA, RSA3072, or RSA4096.
    • config
      • server: URL - The endpoint of your Vault instance. Following the structure of https://<yourHost>/v1/<pathOfTransitEngine>, e.g. "https://vault.walt.id/v1/transit". By default the transit engine will live at /transit.
      • auth:
        • AppRole Auth:
          • roleId: String - The Role ID for AppRole authentication.
          • secretId: String - The Secret ID for AppRole authentication.
        • User/Pass Auth:
          • username: String - The username for user/pass authentication.
          • password: String - The password for user/pass authentication.
        • AccessToken Auth (Deprecated):
          • accessKey: String - The access token for token-based authentication.
      • namespace: String (optional) - The namespace within the Vault instance.
  • did:
    • method: String - Specifies the DID method. It can be key, jwk, web, cheqd.

Example Response

The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.

{
  "issuerKey": {
    "type": "tse",
    "server": "http://127.0.0.1:8200/v1/transit",
    "auth": {
      "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
      "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
    },
    "id": "k732628061",
    "_publicKey": [
      86,
      -55,
      122,
      -77,
      109,
      -60,
      61,
      -40,
      -122,
      -57,
      -101,
      -53,
      69,
      -125,
      12,
      92,
      -9,
      89,
      61,
      -78,
      116,
      -76,
      111,
      -58,
      -63,
      -122,
      -26,
      79,
      -110,
      -32,
      -73,
      -98
    ],
    "_keyType": "Ed25519"
  },
  "issuerDid": "did:key:z6MkkJ3JAhCpSGe5QD9UQ2WGzna7kcswa9ahMC2o3Dd1KRXb"
}

Properties

  • type: String - the type of key can be either "tse" when using HashiCorp Vault or "jwk" when providing the key in full as JWK.
  • server: URL - The endpoint of your Vault instance. Following the structure of https://<yourHost>/v1/<pathOfTransitEngine>, e.g. "https://vault.walt.id/v1/transit". By default the transit engine will live at /transit.
  • auth:
    • AppRole Auth:
      • roleId: String - The Role ID for AppRole authentication.
      • secretId: String - The Secret ID for AppRole authentication.
    • User/Pass Auth:
      • username: String - The username for user/pass authentication.
      • password: String - The password for user/pass authentication.
    • AccessToken Auth (Deprecated):
      • accessKey: String - The access token for token-based authentication.
  • id: String - the ID of the key in the Transit Engine.
  • _publicKey (optional): Array - The public key can be fetched by the issuer API or directly provided, saving resources and reducing network requests.
  • _keyType (optional): String - The key type can be fetched by the issuer API or directly provided, saving resources and reducing network requests.

Key Usage

Once you have successfully created a key that is one of the supported types listed above, you can use it in sign and issue operations offered by the issuer API.

If you've already had a look at our /sign, /issue, /batchIssue endpoints, you have seen that they all follow a similar request body structure, where the key that should be used for signing credentials is provided via the issuerKey property. Now instead of providing the key as JWK, we provide a reference to a key stored in Vault with the required parameters and access credentials.

Below you can see an example of issuerKey object referencing a key stored in Vault.

Example IssuerKey Object
{
  "type": "tse",
  "server": "https://vault.walt.id/v1/transit",
  "auth": {
    "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
    "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
  },
  "id": "k1474327600",
  "_publicKey": [
    -17,
    83,
    1,
    26,
    9,
    -104,
    -64,
    -50,
    48,
    92,
    -112,
    -76,
    7,
    90,
    -40,
    -100,
    72,
    106,
    -51,
    -58,
    64,
    -26,
    73,
    121,
    26,
    -118,
    39,
    -8,
    -25,
    -34,
    82,
    37
  ],
  "_keyType": "Ed25519"
}

Properties

  • type: String - the type of key can be either "tse" when using HashiCorp Vault or "jwk" when providing the key in full as JWK.
  • server: URL - The endpoint of your Vault instance. Following the structure of https://<yourHost>/v1/<pathOfTransitEngine>, e.g. "https://vault.walt.id/v1/transit". By default the transit engine will live at /transit.
  • auth:
    • AppRole Auth:
      • roleId: String - The Role ID for AppRole authentication.
      • secretId: String - The Secret ID for AppRole authentication.
    • User/Pass Auth:
      • username: String - The username for user/pass authentication.
      • password: String - The password for user/pass authentication.
    • AccessToken Auth (Deprecated):
      • accessKey: String - The access token for token-based authentication.
  • id: String - the ID of the key in the Transit Engine.
  • _publicKey (optional): Array - The public key can be fetched by the issuer API or directly provided, saving resources and reducing network requests.
  • _keyType (optional): String - The key type can be fetched by the issuer API or directly provided, saving resources and reducing network requests. ::

Example Issuance Request

Below you can see example issuance request to jwt/issue using a key created in the Vault Transit Secrets Engine to sign the credential.

CURL

Api Reference

curl -X 'POST' \
  'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
  -H 'accept: text/plain' \
  -H 'statusCallbackUri: https://example.com/$id' \
  -H 'Content-Type: application/json' \
  -d '{
  "issuerKey": {
    "type": "tse",
    "server": "https://vault.walt.id/v1/transit",
    "auth": {
      "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
      "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
    },
    "id": "k-1481938705"
  },
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialData": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1",
      "https://purl.imsglobal.org/spec/ob/v3p0/context.json"
    ],
    "id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
    "type": [
      "VerifiableCredential",
      "OpenBadgeCredential"
    ],
    "name": "JFF x vc-edu PlugFest 3 Interoperability",
    ... 
  },
  "mapping": {
    "id": "<uuid>",
     ... 
  }
}'

Header Parameters

  • statusCallbackUri: URL - Receive updates on the created issuance process, e.g. when a credential was successfully claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a $id, which will be replaced by the issuance session id. For example: https://myurl.com/$id, https://myurl.com or https://myurl.com/test/$id

    Expand To Learn More

    Body

    The data send to the provided URL will contain a JSON body:

    • id : String - the issuance session id
    • type: String - the event type
    • data: JsonObject - the data for the event

    Event Types

    Possible events (event types) and their data are:

    • resolved_credential_offer with the credential offer as JSON (in our Web Wallet: called when the issuance offer is entered into the wallet, but not processing / accepted yet)
    • requested_token with the issuance request for the token as json object (called for the token required to receive the credentials)

    Credential issuance (called for every credential that's issued (= requested from wallet))

    • jwt_issue with jwt being the issued jwt
    • sdjwt_issue with sdjwt being the issued sdjwt
    • batch_jwt_issue with jwt being the issued jwt
    • batch_sdjwt_issue with sdjwt being the issued sdjwt
    • generated_mdoc with mdoc being the CBOR (HEX) of the signed mdoc

    To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.

Body

As you can see for the property issuerKey we only provided the required parameters of the TSE Key Reference Object described above and left out _publicKey and _keyType

{
  "issuerKey": {
    "type": "tse",
    "server": "https://vault.walt.id/v1/transit",
    "auth": {
      "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
      "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
    },
    "id": "k-1481938705"
  },
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialData": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1",
      "https://purl.imsglobal.org/spec/ob/v3p0/context.json"
    ],
    "id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
    "type": [
      "VerifiableCredential",
      "OpenBadgeCredential"
    ],
    "name": "JFF x vc-edu PlugFest 3 Interoperability",
    ...
  },
  "mapping": {
    "id": "<uuid>",
    ...
  },
  "standardVersion": "DRAFT13"
}
Last updated on July 7, 2026