{"templateId":"markdown","versions":[{"version":"v1.43","label":"v1.43 LTS","link":"/products/custody/developer-experience/sdks/hashicorp-vault","default":true,"active":true,"folderId":"c15a2701"},{"version":"v1.42","label":"v1.42 STS","link":"/products/custody/v1.42/developer-experience/sdks/hashicorp-vault","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.41","label":"v1.41 STS","link":"/products/custody/v1.41/developer-experience/sdks/hashicorp-vault","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.34","label":"v1.34 LTS","link":"/products/custody/v1.34/developer-experience/sdks/hashicorp-vault","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.26","label":"v1.26 LTS","link":"/products/custody/v1.26/developer-experience/sdks/hashicorp-vault","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.19","label":"v1.19 LTS","link":"/products/custody/v1.19/developer-experience/sdks/hashicorp-vault","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.15","label":"v1.15 LTS","link":"/products/custody/v1.15/developer-experience/sdks/hashicorp-vault","default":false,"active":false,"folderId":"c15a2701"}],"sharedDataIds":{"sidebar":"sidebar-products/custody/@v1.15/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition","tabs","tab"]},"type":"markdown"},"seo":{"title":"Sign with HashiCorp Vault","description":"User guides, API reference, and support resources.","siteUrl":"https://docs.ripple.com","lang":"en-US"},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"sign-with-hashicorp-vault","__idx":0},"children":["Sign with HashiCorp Vault"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sign"]}," endpoint and uses the signature that HashiCorp Vault returns. The SDK reads the public key from HashiCorp Vault and presents it at login."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info","name":"Two different vaults"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/developer-experience/sdks/authentication-and-signing"},"children":["Authentication and signing"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"sdk-support","__idx":1},"children":["SDK support"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Each SDK supports HashiCorp Vault differently:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"SDK"},"children":["SDK"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Support"},"children":["Support"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["TypeScript"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Built in. Select it with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_KMS_PROVIDER=hashicorp"]},". Doesn't need an extra package."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Python"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Built in. Install the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["kms-hashicorp"]}," extra and set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_KMS_PROVIDER=hashicorp"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Java"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No built-in provider. Wire a custom signer that calls the Transit API, as shown in ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#configure-the-sdk"},"children":["Configure the SDK"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"prerequisites","__idx":2},"children":["Prerequisites"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A HashiCorp Vault server that your application reaches over HTTPS. The TypeScript and Python SDKs accept plain HTTP only for a server on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["localhost"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["127.0.0.1"]},", for local testing."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["HashiCorp Vault permissions to enable a secrets engine and to write keys and policies, or an operator who can do this for you."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["vault"]}," CLI, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["jq"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["openssl"]}," on the machine where you run the setup commands."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["An administrator who can register a public key for the API user. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/identity-and-access/authentication/generate-api-keys-and-register"},"children":["Generate a key pair and register a public key"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"set-up-hashicorp-vault","__idx":3},"children":["Set up HashiCorp Vault"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The examples on this page use the key name ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["custody-api-signing-key"]}," and the policy name ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["custody-sdk-signer"]},". Replace them with your own names."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-1-enable-the-transit-secrets-engine","__idx":4},"children":["Step 1: Enable the Transit secrets engine"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Enable Transit:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"vault secrets enable transit\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This command mounts Transit at ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transit/"]},". The TypeScript SDK always uses the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transit/"]}," mount. Python can use another mount through ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TRANSIT_MOUNT_POINT"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-2-create-the-signing-key","__idx":5},"children":["Step 2: Create the signing key"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create a non-exportable ECDSA P-256 key:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"vault write -f transit/keys/custody-api-signing-key type=ecdsa-p256 exportable=false\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The key must be ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ecdsa-p256"]},", the only Transit key type the SDKs support. The TypeScript and Python SDKs refuse to sign with an exportable key."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Don't enable automatic rotation (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["auto_rotate_period"]},") on this key. Rotating it replaces the API user's key. For details, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#limitations"},"children":["Limitations"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-3-create-a-policy","__idx":6},"children":["Step 3: Create a policy"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create a policy file named ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["custody-sdk-signer.hcl"]}," that grants only what the SDK calls:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"hcl","header":{"controls":{"copy":{}}},"source":"# Read the key's metadata and public key\npath \"transit/keys/custody-api-signing-key\" {\n  capabilities = [\"read\"]\n}\n\n# Sign with the key. TypeScript and Java call the /sha2-256 form;\n# Python calls the base path.\npath \"transit/sign/custody-api-signing-key\" {\n  capabilities = [\"update\"]\n}\npath \"transit/sign/custody-api-signing-key/sha2-256\" {\n  capabilities = [\"update\"]\n}\n","lang":"hcl"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Write the policy:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"vault policy write custody-sdk-signer custody-sdk-signer.hcl\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you mounted Transit somewhere other than ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transit/"]},", change the path prefix to match. If you use only one SDK, you can drop the sign path that SDK doesn't call."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-4-create-credentials-for-the-sdk","__idx":7},"children":["Step 4: Create credentials for the SDK"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The SDK doesn't renew HashiCorp Vault tokens. Choose a token lifetime that covers how long your process runs. For details, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#limitations"},"children":["Limitations"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create either a token or, for the Python SDK, an AppRole role:"]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"TabItemFragment","attributes":{"label":"Token","disable":false},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create a token with the signer policy:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"vault token create -policy=custody-sdk-signer -ttl=24h -field=token\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["HashiCorp Vault caps the token's time to live (TTL) at the maximum TTL of the token auth method."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Don't create the token with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["-no-default-policy"]},". The Python SDK checks the token with a lookup-self call when it starts, and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["default"]}," policy permits that call."]}]},{"$$mdtype":"Tag","name":"TabItemFragment","attributes":{"label":"AppRole (Python only)","disable":false},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Enable AppRole, create a role with the signer policy, and read its role ID and secret ID:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"vault auth enable approle\nvault write auth/approle/role/custody-sdk \\\n    token_policies=custody-sdk-signer \\\n    token_ttl=24h \\\n    token_max_ttl=24h\nvault read -field=role_id auth/approle/role/custody-sdk/role-id\nvault write -f -field=secret_id auth/approle/role/custody-sdk/secret-id\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Python SDK logs in with AppRole at the default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["approle/"]}," mount. It logs in once, when it creates the signer, and uses that token until it expires. So ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["token_ttl"]}," sets how long the client can sign."]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-5-export-and-register-the-public-key","__idx":8},"children":["Step 5: Export and register the public key"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Read the public key of the current key version, then convert it to the DER format with Base64 encoding that ","Ripple Custody"," registers:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"vault read -format=json transit/keys/custody-api-signing-key \\\n  | jq -r '.data.keys[.data.latest_version | tostring].public_key' > vault-public-key.pem\n\nopenssl pkey -pubin -in vault-public-key.pem -outform DER | openssl base64 -A > publicKey.b64\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Give the contents of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["publicKey.b64"]}," to your administrator to register as the API user's public key. For the procedure, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/identity-and-access/authentication/generate-api-keys-and-register#step-2-register-with-your-administrator"},"children":["Step 2: Register with your administrator"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"configure-the-sdk","__idx":9},"children":["Configure the SDK"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set the ","Ripple Custody"," connection settings as usual, but don't set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_PRIVATE_KEY_FILE"]},". For the connection settings, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/developer-experience/sdks/configuration"},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Select the tab for your SDK:"]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"TabItemFragment","attributes":{"label":"TypeScript","disable":false},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set the environment variables:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"export RC_KMS_PROVIDER=hashicorp\nexport VAULT_ADDR=https://vault.example.com:8200\nexport VAULT_TOKEN=<token>\nexport VAULT_TRANSIT_KEY_NAME=custody-api-signing-key\n# HashiCorp Vault Enterprise or HCP Vault namespaces only\nexport VAULT_NAMESPACE=<namespace>\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create the client:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import {\n  CustodyClient,\n  loadConfiguration,\n} from '@ripple/ripple-custody-client-lib-typescript';\n\nconst client = new CustodyClient(loadConfiguration());\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You must set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_ADDR"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TOKEN"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TRANSIT_KEY_NAME"]},". You can set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["kms_provider = \"hashicorp\""]}," in the configuration file instead of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_KMS_PROVIDER"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To build the signer in code, call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["makeHashiCorpVaultSigner({ addr, token, keyName, namespace })"]}," and pass it as a custom signer. For details, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/developer-experience/sdks/authentication-and-signing#custom-signer"},"children":["Custom signer"]},"."]}]},{"$$mdtype":"Tag","name":"TabItemFragment","attributes":{"label":"Java","disable":false},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Java SDK has no built-in HashiCorp Vault provider, and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CustodyClient.fromConfig"]}," throws an exception if you set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_KMS_PROVIDER"]},". Leave it unset and wire a signer that calls the Transit API. The Java example ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Example08HashiCorpVaultTransit"]}," shows a complete signer. To get the examples, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/developer-experience/sdks#examples"},"children":["Examples"]},". The following class uses OkHttp, which the SDK exposes, and Gson. Add Gson to your build if it isn't already a direct dependency:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"import com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.ripple.custody.Keys;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Base64;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\n\n/** Signs with a HashiCorp Vault Transit key. The private key never leaves HashiCorp Vault. */\nfinal class VaultTransitSigner {\n  private static final MediaType JSON = MediaType.get(\"application/json\");\n\n  private final OkHttpClient http = new OkHttpClient();\n  private final String vaultAddr;\n  private final String vaultToken;\n  private final String namespace; // null unless you use namespaces\n  private final String keyName;\n  private final int keyVersion;\n  private final String publicKey;\n\n  VaultTransitSigner(String vaultAddr, String vaultToken, String namespace, String keyName)\n      throws IOException {\n    this.vaultAddr = vaultAddr.replaceAll(\"/+$\", \"\");\n    this.vaultToken = vaultToken;\n    this.namespace = namespace;\n    this.keyName = keyName;\n\n    JsonObject data = call(\"GET\", \"transit/keys/\" + keyName, null).getAsJsonObject(\"data\");\n    if (data.get(\"exportable\").getAsBoolean()) {\n      throw new IllegalStateException(\"Transit key \" + keyName + \" must not be exportable\");\n    }\n    // Sign with the version whose public key is presented, so the two always match.\n    this.keyVersion = data.get(\"latest_version\").getAsInt();\n    String pem = data.getAsJsonObject(\"keys\")\n        .getAsJsonObject(String.valueOf(keyVersion))\n        .get(\"public_key\").getAsString();\n    this.publicKey = Keys.normalizePublicKey(pem); // PEM to base64-DER SPKI\n  }\n\n  String publicKey() {\n    return publicKey;\n  }\n\n  String sign(String message) {\n    JsonObject body = new JsonObject();\n    body.addProperty(\"input\",\n        Base64.getEncoder().encodeToString(message.getBytes(StandardCharsets.UTF_8)));\n    body.addProperty(\"key_version\", keyVersion);\n    body.addProperty(\"marshaling_algorithm\", \"asn1\");\n    try {\n      String signature = call(\"POST\", \"transit/sign/\" + keyName + \"/sha2-256\", body)\n          .getAsJsonObject(\"data\").get(\"signature\").getAsString();\n      return signature.substring(signature.lastIndexOf(':') + 1); // strip \"vault:vN:\"\n    } catch (IOException e) {\n      throw new UncheckedIOException(e);\n    }\n  }\n\n  private JsonObject call(String method, String path, JsonObject body) throws IOException {\n    Request.Builder request = new Request.Builder()\n        .url(vaultAddr + \"/v1/\" + path)\n        .header(\"X-Vault-Token\", vaultToken)\n        .method(method, body == null ? null : RequestBody.create(body.toString(), JSON));\n    if (namespace != null) {\n      request.header(\"X-Vault-Namespace\", namespace);\n    }\n    try (Response response = http.newCall(request.build()).execute()) {\n      String text = response.body() == null ? \"\" : response.body().string();\n      if (!response.isSuccessful()) {\n        throw new IOException(\"Vault \" + method + \" \" + path + \" failed: HTTP \"\n            + response.code() + \" \" + text);\n      }\n      return JsonParser.parseString(text).getAsJsonObject();\n    }\n  }\n}\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use the signer for both login and intents:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"import com.ripple.custody.CustodyClient;\nimport com.ripple.custody.CustodyClientConfig;\nimport com.ripple.custody.auth.ChallengeSigner;\nimport com.ripple.custody.invoker.ApiClient;\n\nCustodyClientConfig config = CustodyClientConfig.loadConfiguration();\nVaultTransitSigner vault = new VaultTransitSigner(\n    System.getenv(\"VAULT_ADDR\"),\n    System.getenv(\"VAULT_TOKEN\"),\n    System.getenv(\"VAULT_NAMESPACE\"),\n    System.getenv(\"VAULT_TRANSIT_KEY_NAME\"));\n\nApiClient apiClient = ApiClient.newBuilder()\n    .tokenEndpoint(config.getEffectiveTokenEndpoint())\n    .clientId(config.getClientId() != null ? config.getClientId() : \"customer_api\")\n    .grantType(\"password\")\n    .signer(new ChallengeSigner(vault::sign, unused -> vault.publicKey()))\n    .verifySsl(config.isVerifySsl())\n    .sslCaCert(config.getSslCaCert())\n    .caCertData(config.getCaCertData())\n    .certFile(config.getCertFile())\n    .keyFile(config.getKeyFile())\n    .build();\napiClient.setBasePath(config.getApiBaseUrl());\n\n// The same Transit key signs intents.\nconfig.apiClient(apiClient).signer(vault::sign);\nCustodyClient client = new CustodyClient(config);\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ChallengeSigner"]}," signs the login challenge. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["config.signer(...)"]}," takes an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["IntentSigner"]},", which signs the canonical JSON of each intent. Both call the same Transit key."]}]},{"$$mdtype":"Tag","name":"TabItemFragment","attributes":{"label":"Python","disable":false},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Install the SDK with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["kms-hashicorp"]}," extra:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"pip install \"./ripple_custody_client_lib_python-{version}-py3-none-any.whl[kms-hashicorp]\"\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set the environment variables:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"sh","header":{"controls":{"copy":{}}},"source":"export RC_KMS_PROVIDER=hashicorp\nexport VAULT_ADDR=https://vault.example.com:8200\nexport VAULT_TRANSIT_KEY_NAME=custody-api-signing-key\n\n# Token auth\nexport VAULT_TOKEN=<token>\n# ...or AppRole auth\nexport VAULT_ROLE_ID=<role-id>\nexport VAULT_SECRET_ID=<secret-id>\n\n# Optional\nexport VAULT_TRANSIT_MOUNT_POINT=transit\nexport VAULT_NAMESPACE=<namespace>  # HashiCorp Vault Enterprise or HCP Vault only\n","lang":"sh"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create the client:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"from ripple_custody_client_lib_python import CustodyClient, load_configuration\n\nconfig = load_configuration()\n\nwith CustodyClient(config) as client:\n    ...\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You must set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_ADDR"]},", and either ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TOKEN"]}," or both ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_ROLE_ID"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_SECRET_ID"]},". If you set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TOKEN"]},", the SDK uses it and ignores the AppRole variables. Set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TRANSIT_KEY_NAME"]}," explicitly. Its default, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["harmonize-signing-key"]},", is unlikely to match your key. You can set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["kms_provider = \"hashicorp\""]}," in the configuration file instead of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_KMS_PROVIDER"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["load_configuration()"]}," connects to HashiCorp Vault when it runs. It authenticates, reads the key, and rejects an exportable key, so a misconfiguration fails at startup rather than at the first API call."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To build the signer in code, create ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HashiCorpVaultSigner"]}," from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ripple_custody_client_lib_python.custody.kms"]}," and assign its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sign_message"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["read_public_key"]}," methods to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["config.sign_string"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["config.read_public_key"]},"."]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"hashicorp-vault-auth-methods","__idx":10},"children":["HashiCorp Vault auth methods"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The SDKs support the following HashiCorp Vault auth methods:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Auth method"},"children":["Auth method"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"TypeScript"},"children":["TypeScript"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Java"},"children":["Java"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Python"},"children":["Python"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Token (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TOKEN"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes, in the example signer"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["AppRole"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Not in the example signer. Your code can log in and pass the token."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes, at the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["approle/"]}," mount"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Kubernetes, cloud IAM, JWT/OIDC (OpenID Connect), TLS certificates, and others"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Not in the example signer"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For an auth method the SDK doesn't support, log in to HashiCorp Vault outside the SDK and pass the resulting token as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TOKEN"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"limitations","__idx":11},"children":["Limitations"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Transit only."]}," The SDKs sign only through the Transit secrets engine, with an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ecdsa-p256"]}," key. They don't support other secrets engines or key types."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["HTTPS required."]}," The TypeScript and Python SDKs reject a plain ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["http://"]}," address unless it points to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["localhost"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["127.0.0.1"]}," (TypeScript also accepts ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["::1"]},"). The Java example signer doesn't check the scheme, so add that check if you need it. The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RC_*"]}," TLS settings apply to the ","Ripple Custody"," API, not to HashiCorp Vault:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["TypeScript:"]}," uses the Node.js global ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetch"]},", so the HashiCorp Vault certificate must chain to a CA that Node.js trusts."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Python:"]}," uses the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hvac"]}," client, which honors ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_CACERT"]}," for a private CA."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Java:"]}," the example signer uses a default OkHttp client. If you use a private CA, configure the client to trust it."]}]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["InvalidSignatureError"]},", and login fails. After your administrator registers the new public key, restart the process."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Java:"]}," the example signer keeps using the version it read at startup until the process restarts."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Namespaces."]}," TypeScript and Python support ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_NAMESPACE"]},". The Java example signer sends the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Vault-Namespace"]}," header when you pass a namespace."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Mount point."]}," TypeScript always uses ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transit/"]},". Python reads ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VAULT_TRANSIT_MOUNT_POINT"]},". In Java, the path is in your signer code."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"related-topics","__idx":12},"children":["Related topics"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/developer-experience/sdks/authentication-and-signing"},"children":["Authentication and signing"]},": how the SDKs use the API user's key for login and intents, and the other signers they support."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/developer-experience/sdks/configuration"},"children":["Configure the SDK"]},": the configuration file keys, environment variables, and TLS settings each SDK recognizes."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/identity-and-access/authentication/generate-api-keys-and-register"},"children":["Generate a key pair and register a public key"]},": register the API user's public key."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/identity-and-access/authentication/authenticate-api-requests"},"children":["Authenticate API requests"]},": the underlying token and intent-signing flow of the ","Ripple Custody"," API."]}]}]},"headings":[{"value":"Sign with HashiCorp Vault","id":"sign-with-hashicorp-vault","depth":1},{"value":"SDK support","id":"sdk-support","depth":2},{"value":"Prerequisites","id":"prerequisites","depth":2},{"value":"Set up HashiCorp Vault","id":"set-up-hashicorp-vault","depth":2},{"value":"Step 1: Enable the Transit secrets engine","id":"step-1-enable-the-transit-secrets-engine","depth":3},{"value":"Step 2: Create the signing key","id":"step-2-create-the-signing-key","depth":3},{"value":"Step 3: Create a policy","id":"step-3-create-a-policy","depth":3},{"value":"Step 4: Create credentials for the SDK","id":"step-4-create-credentials-for-the-sdk","depth":3},{"value":"Step 5: Export and register the public key","id":"step-5-export-and-register-the-public-key","depth":3},{"value":"Configure the SDK","id":"configure-the-sdk","depth":2},{"value":"HashiCorp Vault auth methods","id":"hashicorp-vault-auth-methods","depth":2},{"value":"Limitations","id":"limitations","depth":2},{"value":"Related topics","id":"related-topics","depth":2}],"frontmatter":{"seo":{"title":"Sign with HashiCorp Vault"}},"lastModified":"2026-10-26T19:05:12.528Z","pagePropGetterError":{"message":"","name":""}},"slug":"/products/custody/developer-experience/sdks/hashicorp-vault","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}