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.
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.
Each SDK supports HashiCorp Vault differently:
| SDK | Support |
|---|---|
| TypeScript | Built in. Select it with RC_KMS_PROVIDER=hashicorp. Doesn't need an extra package. |
| Python | Built in. Install the kms-hashicorp extra and set RC_KMS_PROVIDER=hashicorp. |
| Java | No built-in provider. Wire a custom signer that calls the Transit API, as shown in Configure the SDK. |
- A HashiCorp Vault server that your application reaches over HTTPS. The TypeScript and Python SDKs accept plain HTTP only for a server on
localhostor127.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
vaultCLI,jq, andopensslon 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.
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.
Enable Transit:
vault secrets enable transitThis command mounts Transit at transit/. The TypeScript SDK always uses the transit/ mount. Python can use another mount through VAULT_TRANSIT_MOUNT_POINT.
Create a non-exportable ECDSA P-256 key:
vault write -f transit/keys/custody-api-signing-key type=ecdsa-p256 exportable=falseThe 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.
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.hclIf 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.
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=tokenHashiCorp 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.
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.b64Give 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.
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.
The SDKs support the following HashiCorp Vault auth methods:
| Auth method | TypeScript | Java | Python |
|---|---|---|---|
Token (VAULT_TOKEN) | Yes | Yes, in the example signer | Yes |
| AppRole | No | Not 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 others | No | Not in the example signer | No |
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.
Transit only. The SDKs sign only through the Transit secrets engine, with an
ecdsa-p256key. 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 tolocalhostor127.0.0.1(TypeScript also accepts::1). The Java example signer doesn't check the scheme, so add that check if you need it. TheRC_*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
hvacclient, which honorsVAULT_CACERTfor a private CA. - Java: the example signer uses a default OkHttp client. If you use a private CA, configure the client to trust it.
- TypeScript: uses the Node.js global
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 theX-Vault-Namespaceheader when you pass a namespace.Mount point. TypeScript always uses
transit/. Python readsVAULT_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.
- Authentication and signing: how the SDKs use the API user's key for login and intents, and the other signers they support.
- Configure the SDK: the configuration file keys, environment variables, and TLS settings each SDK recognizes.
- Generate a key pair and register a public key: register the API user's public key.
- Authenticate API requests: the underlying token and intent-signing flow of the Ripple Custody API.