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.
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.
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.
The signed challenge is the password grant described in Authenticate API requests. For each new token, the SDK:
- Generates a random challenge with a cryptographically secure random number generator.
- Signs the challenge with the configured signer.
- 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.
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
ConfigurationErrorthat says intent signing isn't available. - Java throws a
CustodyErrorif you haven't configured anIntentSigner. - Python throws a
ConfigurationErrorwhen it signs an intent.
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_IDandRC_CLIENT_SECRETare 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.
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 throwsUnauthorizedErrorand Python raisesCustodyUnauthorizedError. - 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.
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) orread_public_key/sign_string(Python). The client uses it for login and for intents. - Java: login and intents use two interfaces.
ChallengeSignersigns the login challenge and supplies the public key.IntentSignersigns canonical intent JSON.CustodyClient.fromConfigbuilds both from the same private key file. With a custom signer, back both with the same key.
The following table lists the signers each SDK supports and the key types each signer accepts:
| Signer | TypeScript | Java | Python | Key types |
|---|---|---|---|---|
| Local PEM key file | Yes | Yes | Yes | P-256 (secp256r1), secp256k1, Ed25519 |
| AWS KMS | Yes | No built-in signer. Use a custom signer, as in Example10AwsKms. | Yes (kms-aws extra) | ECC_NIST_P256 only |
| Azure Key Vault | Yes | No built-in signer. Use a custom signer, as in Example09AzureKeyVaultKeys. | Yes (kms-azure extra) | EC P-256 only |
| HashiCorp Vault Transit | Yes | No built-in signer. Use a custom signer, as in Example08HashiCorpVaultTransit. | Yes (kms-hashicorp extra) | ecdsa-p256 only |
| Custom signer | Yes | Yes | Yes | Any 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.
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.
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 usageSIGN_VERIFY. The SDK signs withECDSA_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 (
ECorEC-HSM) with thesignoperation. The SDK signs withES256and 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:
| Setting | TypeScript | Python |
|---|---|---|
| AWS key ID, ARN, or alias | RC_KMS_KEY_ID (or KMS_KEY_ID) | AWS_KMS_KEY_ID |
| AWS region | AWS_REGION (default us-east-1) | AWS_REGION (default us-east-1) |
| AWS credentials | Default AWS SDK credential chain | Default boto3 credential chain, with optional AWS_PROFILE |
| Azure Key Vault URL | AZURE_VAULT_URL | AZURE_KEY_VAULT_URL (must be HTTPS) |
| Azure key name | AZURE_KEY_NAME | AZURE_KEY_NAME |
| Azure credentials | DefaultAzureCredential | For 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-keysand@azure/identity. - Python: install the
kms-awsextra (requires Python 3.10 or later) or thekms-azureextra, for examplepip 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.
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.
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.
- Sign with HashiCorp Vault: keep the API user's key in the HashiCorp Vault Transit secrets engine.
- Configure the SDK: the configuration file keys, environment variables, and TLS settings each SDK recognizes.
- Work with intents: track, approve, and reject the intents the SDK signs.
- Authenticate API requests: the underlying token and intent-signing flow of the Ripple Custody API.
- Generate a key pair and register a public key: create the API user's key and register its public key.