Hashicorp Vault

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 checkout 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 only thing that is important to note for the creation, that our system is only compatible with the following Key types offered by the Transit Secrets Engine:

  • ed25519

Transit Secrets Engine key types full list.

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.portal.walt-test.cloud/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 (manged 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 only ed25519 is possible.
    • 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 Secret Engine to sign the credential.

CURL

Api Reference

curl -X 'POST' \
  'https://issuer.portal.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>",
    ...
  }
}