Skip to content

Authentication and signing

The Ripple Custody SDKs authenticate to the Ripple Custody API and sign intents with one key: the private key of the API user. This page describes how each SDK obtains and renews access tokens, where you can keep that key, and how to plug in your own signer. Read it when you choose a signing key store or connect one the SDK doesn't support.

API user key, not platform keys

The SDK signer holds the API user's key, the key whose public half you register for the user in Ripple Custody. It's not related to the platform keys that protect assets. Ripple Custody manages those keys through its own hardware security module (HSM), key management service (KMS), or multi-party computation (MPC) integration. For the platform side, see Integrate a key management system (KMS).

For the underlying API flow, see Authenticate API requests. To create the key and register its public key, see Generate a key pair and register a public key.

How the SDK authenticates

The SDKs support two OAuth 2.0 grants. You don't call the token endpoint yourself. The client requests a token before the first API call and renews it as described in Token caching and renewal.

Signed challenge

The signed challenge is the password grant described in Authenticate API requests. For each new token, the SDK:

  1. Generates a random challenge with a cryptographically secure random number generator.
  2. Signs the challenge with the configured signer.
  3. Sends the challenge, the signature, and the signer's public key to the token endpoint.

Use this grant for any work that submits, approves, or rejects user-signed intents.

Client credentials

The client credentials grant is the service-caller grant described in Obtain a token for a service caller. With the client_credentials grant, the SDK authenticates with a client ID and client secret instead of a signature.

A client_credentials token identifies a service caller, not a user. The SDKs can use it for read requests, but they don't include a helper for system-signed intents. To submit user-signed intents, the JWT user must match the intent author, so authenticate with the signed challenge.

If you configure client_credentials without a signing key, the client still connects, but any call that signs an intent fails:

  • TypeScript throws a ConfigurationError that says intent signing isn't available.
  • Java throws a CustodyError if you haven't configured an IntentSigner.
  • Python throws a ConfigurationError when it signs an intent.

Grant selection

All three SDKs choose the grant from the credentials they find. Set RC_CLIENT_SECRET as an environment variable, not in the configuration file.

  • Client credentials: the SDK uses it whenever RC_CLIENT_ID and RC_CLIENT_SECRET are both set and not empty, even if you also configure a signer. The signer still signs intents.
  • Signed challenge: the SDK uses it otherwise, with the client ID customer_api. It requires a signer: RC_PRIVATE_KEY_FILE, RC_KMS_PROVIDER, or a custom signer.

Setting a client secret without a client ID is an error. In TypeScript and Python, a client ID without a secret is also an error.

Token caching and renewal

All three SDKs keep the access token in memory, per client instance. The SDKs never write tokens to disk, so a new process always authenticates again. They renew tokens the same way:

  • Proactive renewal: 60 seconds before the token expires. In Python, you can change this margin with token_expiry_threshold_seconds.
  • Refresh token: the SDK uses it if the server returned one, and requests a new grant if the refresh fails.
  • Response 401: the SDK returns the error to the caller and doesn't replay the request. TypeScript throws UnauthorizedError and Python raises CustodyUnauthorizedError.
  • Concurrent callers: one renewal runs at a time.

Java also checks the signer's public key on every request and authenticates again if it changed.

One key for login and intents

The same signer signs both the login challenge and every intent the client proposes, approves, or rejects. One key for both follows the platform rule that the JWT user and the intent author must be the same user. For details, see JWT and intent author identity.

Each SDK wires the key differently:

  • TypeScript and Python: one signer object supplies both readPublicKey/signString (TypeScript) or read_public_key/sign_string (Python). The client uses it for login and for intents.
  • Java: login and intents use two interfaces. ChallengeSigner signs the login challenge and supplies the public key. IntentSigner signs canonical intent JSON. CustodyClient.fromConfig builds both from the same private key file. With a custom signer, back both with the same key.

Supported signers

The following table lists the signers each SDK supports and the key types each signer accepts:

SignerTypeScriptJavaPythonKey types
Local PEM key fileYesYesYesP-256 (secp256r1), secp256k1, Ed25519
AWS KMSYesNo built-in signer. Use a custom signer, as in Example10AwsKms.Yes (kms-aws extra)ECC_NIST_P256 only
Azure Key VaultYesNo built-in signer. Use a custom signer, as in Example09AzureKeyVaultKeys.Yes (kms-azure extra)EC P-256 only
HashiCorp Vault TransitYesNo built-in signer. Use a custom signer, as in Example08HashiCorpVaultTransit.Yes (kms-hashicorp extra)ecdsa-p256 only
Custom signerYesYesYesAny key type the platform accepts

Configure one signer per client. TypeScript and Python reject a configuration that sets both a private key file and a KMS provider. Java rejects RC_PRIVATE_KEY_FILE and RC_KMS_PROVIDER set together.

Local PEM key file

Point RC_PRIVATE_KEY_FILE at the private key you generated for the API user. For the matching configuration file key, see Configure the SDK.

  • The file must be an unencrypted PEM: PKCS#8 (BEGIN PRIVATE KEY) or, for EC keys, SEC1 (BEGIN EC PRIVATE KEY).
  • The SDK derives the public key from the private key, so you don't configure the public key separately.
  • EC keys sign with ECDSA over SHA-256. Ed25519 keys sign with pure Ed25519.

The SDK loads the private key into process memory for the lifetime of the client. If the key must not leave a dedicated key store, use a KMS-backed or custom signer instead.

AWS KMS and Azure Key Vault

The TypeScript and Python SDKs include signers for AWS KMS and Azure Key Vault. The private key stays in the key service. The SDK sends a SHA-256 digest for the service to sign and reads the public key from the service.

Key requirements:

  • AWS KMS: key spec ECC_NIST_P256, key usage SIGN_VERIFY. The SDK signs with ECDSA_SHA_256. The Python signer checks the key spec, usage, and enabled state when it starts.
  • Azure Key Vault: an EC key on curve P-256 (EC or EC-HSM) with the sign operation. The SDK signs with ES256 and converts Azure's raw signature to the ASN.1 DER form the API verifies.

To select a provider, set RC_KMS_PROVIDER to aws or azure (or kms_provider in the configuration file), then set the provider variables. The SDKs read the provider variables from the environment only, even when you set kms_provider in the configuration file. The two SDKs use different variable names:

SettingTypeScriptPython
AWS key ID, ARN, or aliasRC_KMS_KEY_ID (or KMS_KEY_ID)AWS_KMS_KEY_ID
AWS regionAWS_REGION (default us-east-1)AWS_REGION (default us-east-1)
AWS credentialsDefault AWS SDK credential chainDefault boto3 credential chain, with optional AWS_PROFILE
Azure Key Vault URLAZURE_VAULT_URLAZURE_KEY_VAULT_URL (must be HTTPS)
Azure key nameAZURE_KEY_NAMEAZURE_KEY_NAME
Azure credentialsDefaultAzureCredentialFor managed identity, AZURE_USE_MANAGED_IDENTITY=true. For a service principal, AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET. Otherwise, DefaultAzureCredential.

The provider SDKs are optional dependencies:

  • TypeScript: install @aws-sdk/client-kms, or @azure/keyvault-keys and @azure/identity.
  • Python: install the kms-aws extra (requires Python 3.10 or later) or the kms-azure extra, for example pip install "./ripple_custody_client_lib_python-{version}-py3-none-any.whl[kms-aws]". See Install the SDK.

The Java SDK has no built-in KMS signer. CustodyClient.fromConfig throws UnsupportedOperationException if you set RC_KMS_PROVIDER, and the error names the matching example. Wrap your KMS client in a custom signer instead. The Java examples Example10AwsKms and Example09AzureKeyVaultKeys show how. They're in the SDK examples, not in the JAR. To get them, see Examples.

HashiCorp Vault Transit

The TypeScript and Python SDKs can sign with a non-exportable ecdsa-p256 key in the HashiCorp Vault Transit secrets engine. Java can do the same through a custom signer, as in Example08HashiCorpVaultTransit. For setup, policy, configuration, and limitations, see Sign with HashiCorp Vault.

Custom signer

Use a custom signer for any key store the SDK doesn't support directly, such as an on-premises HSM or a signing service. Your signer must supply two functions backed by the API user's key:

  • Public key: the base64-encoded DER SubjectPublicKeyInfo, the same value you registered for the user.
  • Sign: sign the UTF-8 bytes of the string the SDK passes in and return the base64-encoded signature. For ECDSA keys, hash with SHA-256 and return the ASN.1 DER signature.

The following examples connect a custom signer to each SDK:

import {
  type Configuration,
  CustodyClient,
  generateNewConfiguration,
  type KmsSigner,
} from '@ripple/ripple-custody-client-lib-typescript';

// yourKeyStore is your own client for the HSM or signing service.
const signer: KmsSigner = {
  // Return the base64-DER SubjectPublicKeyInfo of the API user's key.
  readPublicKey: () => yourKeyStore.publicKeyBase64Der(),
  // Sign the UTF-8 bytes of data; return a base64 ASN.1 DER ECDSA signature.
  signString: (data) => yourKeyStore.sign(data),
};

const apiBaseUrl = process.env.RC_API_BASE_URL!;
const oidcIssuerUrl = process.env.RC_OIDC_ISSUER_URL!;
const notaryApiPublicKey = process.env.RC_NOTARY_API_PUBLIC_KEY;

const config: Configuration = {
  apiBaseUrl,
  oidcIssuerUrl,
  rcDomainId: process.env.RC_DOMAIN_ID,
  readPublicKey: signer.readPublicKey,
  signString: signer.signString,
  rcValidateAddresses: true,
  verifyResponses: true,
  rcEnforceImmutableEndpoints: true,
  rcVerifyDerivedAddresses: true,
  notaryApiPublicKey,
  _sdkConfig: generateNewConfiguration({
    apiBaseUrl,
    oidcIssuerUrl,
    readPublicKey: signer.readPublicKey,
    signString: signer.signString,
    notaryApiPublicKey,
  }),
};

const client = new CustodyClient(config);

loadConfiguration() doesn't accept a signer object, so build the Configuration directly. Pass oidcIssuerUrl so the SDK discovers the token endpoint, or pass the full URL as tokenEndpoint. Pass the same functions at both levels: the inner configuration uses them for login, and the client uses them for intents. The boolean values in the example match the loadConfiguration() defaults.