Skip to content

Sign with HashiCorp Vault

This page shows how to keep the API user's signing key in the HashiCorp Vault Transit secrets engine and have the Ripple Custody SDK sign with it. Use this setup when the private key must stay in HashiCorp Vault.

You create the key inside HashiCorp Vault as non-exportable, so the private key never leaves HashiCorp Vault. When the SDK needs a signature, for the login challenge or for an intent, it sends the data to the Transit sign endpoint and uses the signature that HashiCorp Vault returns. The SDK reads the public key from HashiCorp Vault and presents it at login.

Two different vaults

This page is about HashiCorp Vault, a third-party secrets manager that holds the API user's key on your side. It's unrelated to Ripple Custody vaults, which hold the platform keys that protect assets. For how the SDK uses the API user's key, see Authentication and signing.

SDK support

Each SDK supports HashiCorp Vault differently:

SDKSupport
TypeScriptBuilt in. Select it with RC_KMS_PROVIDER=hashicorp. Doesn't need an extra package.
PythonBuilt in. Install the kms-hashicorp extra and set RC_KMS_PROVIDER=hashicorp.
JavaNo built-in provider. Wire a custom signer that calls the Transit API, as shown in Configure the SDK.

Prerequisites

  • A HashiCorp Vault server that your application reaches over HTTPS. The TypeScript and Python SDKs accept plain HTTP only for a server on localhost or 127.0.0.1, for local testing.
  • HashiCorp Vault permissions to enable a secrets engine and to write keys and policies, or an operator who can do this for you.
  • The vault CLI, jq, and openssl on the machine where you run the setup commands.
  • An administrator who can register a public key for the API user. See Generate a key pair and register a public key.

Set up HashiCorp Vault

The examples on this page use the key name custody-api-signing-key and the policy name custody-sdk-signer. Replace them with your own names.

Step 1: Enable the Transit secrets engine

Enable Transit:

vault secrets enable transit

This command mounts Transit at transit/. The TypeScript SDK always uses the transit/ mount. Python can use another mount through VAULT_TRANSIT_MOUNT_POINT.

Step 2: Create the signing key

Create a non-exportable ECDSA P-256 key:

vault write -f transit/keys/custody-api-signing-key type=ecdsa-p256 exportable=false

The key must be ecdsa-p256, the only Transit key type the SDKs support. The TypeScript and Python SDKs refuse to sign with an exportable key.

Don't enable automatic rotation (auto_rotate_period) on this key. Rotating it replaces the API user's key. For details, see Limitations.

Step 3: Create a policy

Create a policy file named custody-sdk-signer.hcl that grants only what the SDK calls:

# Read the key's metadata and public key
path "transit/keys/custody-api-signing-key" {
  capabilities = ["read"]
}

# Sign with the key. TypeScript and Java call the /sha2-256 form;
# Python calls the base path.
path "transit/sign/custody-api-signing-key" {
  capabilities = ["update"]
}
path "transit/sign/custody-api-signing-key/sha2-256" {
  capabilities = ["update"]
}

Write the policy:

vault policy write custody-sdk-signer custody-sdk-signer.hcl

If you mounted Transit somewhere other than transit/, change the path prefix to match. If you use only one SDK, you can drop the sign path that SDK doesn't call.

Step 4: Create credentials for the SDK

The SDK doesn't renew HashiCorp Vault tokens. Choose a token lifetime that covers how long your process runs. For details, see Limitations.

Create either a token or, for the Python SDK, an AppRole role:

Create a token with the signer policy:

vault token create -policy=custody-sdk-signer -ttl=24h -field=token

HashiCorp Vault caps the token's time to live (TTL) at the maximum TTL of the token auth method.

Don't create the token with -no-default-policy. The Python SDK checks the token with a lookup-self call when it starts, and the default policy permits that call.

Step 5: Export and register the public key

Read the public key of the current key version, then convert it to the DER format with Base64 encoding that Ripple Custody registers:

vault read -format=json transit/keys/custody-api-signing-key \
  | jq -r '.data.keys[.data.latest_version | tostring].public_key' > vault-public-key.pem

openssl pkey -pubin -in vault-public-key.pem -outform DER | openssl base64 -A > publicKey.b64

Give the contents of publicKey.b64 to your administrator to register as the API user's public key. For the procedure, see Step 2: Register with your administrator.

Configure the SDK

Set the Ripple Custody connection settings as usual, but don't set RC_PRIVATE_KEY_FILE. For the connection settings, see Configure the SDK. The SDKs reject a configuration that sets both a private key file and a key management service (KMS) provider. The SDKs read the HashiCorp Vault settings from environment variables only.

Select the tab for your SDK:

Set the environment variables:

export RC_KMS_PROVIDER=hashicorp
export VAULT_ADDR=https://vault.example.com:8200
export VAULT_TOKEN=<token>
export VAULT_TRANSIT_KEY_NAME=custody-api-signing-key
# HashiCorp Vault Enterprise or HCP Vault namespaces only
export VAULT_NAMESPACE=<namespace>

Create the client:

import {
  CustodyClient,
  loadConfiguration,
} from '@ripple/ripple-custody-client-lib-typescript';

const client = new CustodyClient(loadConfiguration());

You must set VAULT_ADDR, VAULT_TOKEN, and VAULT_TRANSIT_KEY_NAME. You can set kms_provider = "hashicorp" in the configuration file instead of RC_KMS_PROVIDER.

To build the signer in code, call makeHashiCorpVaultSigner({ addr, token, keyName, namespace }) and pass it as a custom signer. For details, see Custom signer.

HashiCorp Vault auth methods

The SDKs support the following HashiCorp Vault auth methods:

Auth methodTypeScriptJavaPython
Token (VAULT_TOKEN)YesYes, in the example signerYes
AppRoleNoNot in the example signer. Your code can log in and pass the token.Yes, at the approle/ mount
Kubernetes, cloud IAM, JWT/OIDC (OpenID Connect), TLS certificates, and othersNoNot in the example signerNo

For an auth method the SDK doesn't support, log in to HashiCorp Vault outside the SDK and pass the resulting token as VAULT_TOKEN.

Limitations

  • Transit only. The SDKs sign only through the Transit secrets engine, with an ecdsa-p256 key. They don't support other secrets engines or key types.

  • No token renewal. The SDKs read the token, or log in with AppRole, once when they create the signer. They never renew the token. When the HashiCorp Vault token expires, signing fails, so the next login or intent fails. Use a token or AppRole role whose TTL covers the life of the process. To pick up a new token, restart the process or rebuild the client before the old token expires.

  • HTTPS required. The TypeScript and Python SDKs reject a plain http:// address unless it points to localhost or 127.0.0.1 (TypeScript also accepts ::1). The Java example signer doesn't check the scheme, so add that check if you need it. The RC_* TLS settings apply to the Ripple Custody API, not to HashiCorp Vault:

    • TypeScript: uses the Node.js global fetch, so the HashiCorp Vault certificate must chain to a CA that Node.js trusts.
    • Python: uses the hvac client, which honors VAULT_CACERT for a private CA.
    • Java: the example signer uses a default OkHttp client. If you use a private CA, configure the client to trust it.
  • Key rotation replaces the API user's key. Each rotated Transit version is a new key pair, and Ripple Custody accepts only the public key registered for the user. Each SDK handles rotation differently:

    • TypeScript: signs with the new version as soon as you rotate the key, and refreshes the public key it presents at most every 5 minutes. Login and intents fail at once, and keep failing until your administrator registers the new public key for the user.
    • Python: signs with the new version as soon as you rotate the key, but keeps the public key it read at startup. Intents fail at once with InvalidSignatureError, and login fails. After your administrator registers the new public key, restart the process.
    • Java: the example signer keeps using the version it read at startup until the process restarts.

    Don't rotate the key unless you're replacing the API user's key. TypeScript and Python start signing with the new version as soon as you rotate it, so requests fail until your administrator registers the new public key for the user.

  • Namespaces. TypeScript and Python support VAULT_NAMESPACE. The Java example signer sends the X-Vault-Namespace header when you pass a namespace.

  • Mount point. TypeScript always uses transit/. Python reads VAULT_TRANSIT_MOUNT_POINT. In Java, the path is in your signer code.

  • Retries. TypeScript retries a failed sign call up to 3 times in total on HTTP 5xx, HTTP 429, or a network error. Python and the Java example signer don't retry.