# 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)](/products/custody/deployment/integrate-kms/overview).

For the underlying API flow, see [Authenticate API requests](/products/custody/identity-and-access/authentication/authenticate-api-requests). To create the key and register its public key, see [Generate a key pair and register a public key](/products/custody/identity-and-access/authentication/generate-api-keys-and-register).

## 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](#token-caching-and-renewal).

### Signed challenge

The signed challenge is the `password` grant described in [Authenticate API requests](/products/custody/identity-and-access/authentication/authenticate-api-requests#obtaining-a-jwt-token). 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](/products/custody/identity-and-access/authentication/authenticate-api-requests#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](/products/custody/identity-and-access/authentication/authenticate-api-requests#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:

| 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.

### 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](/products/custody/developer-experience/sdks/configuration).

- 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:

| 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-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](/products/custody/get-started/integrate-via-sdk#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](/products/custody/developer-experience/sdks#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](/products/custody/developer-experience/sdks/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:

TypeScript
```typescript
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.

Java
```java
import com.ripple.custody.CustodyClient;
import com.ripple.custody.CustodyClientConfig;
import com.ripple.custody.auth.ChallengeSigner;
import com.ripple.custody.invoker.ApiClient;
import java.util.function.Function;

CustodyClientConfig config = CustodyClientConfig.loadConfiguration();

// yourKeyStore is your own client for the HSM or signing service.
// sign: sign the UTF-8 bytes of the input; return a base64 ASN.1 DER ECDSA signature.
Function<String, String> sign = yourKeyStore::sign;
// The base64-DER SubjectPublicKeyInfo of the same key, read once.
String publicKey = yourKeyStore.publicKeyBase64Der();

ApiClient apiClient = ApiClient.newBuilder()
    .tokenEndpoint(config.getEffectiveTokenEndpoint())
    .clientId(config.getClientId() != null ? config.getClientId() : "customer_api")
    .grantType("password")
    .signer(new ChallengeSigner(sign, unused -> publicKey))
    .verifySsl(config.isVerifySsl())
    .sslCaCert(config.getSslCaCert())
    .caCertData(config.getCaCertData())
    .certFile(config.getCertFile())
    .keyFile(config.getKeyFile())
    .build();
apiClient.setBasePath(config.getApiBaseUrl());

// The same key signs intents.
config.apiClient(apiClient).signer(sign::apply);
CustodyClient client = new CustodyClient(config);
```

This code replaces `CustodyClient.fromConfig`, which only resolves a private key file. Leave `RC_PRIVATE_KEY_FILE` and `RC_KMS_PROVIDER` unset. `ChallengeSigner` signs the login challenge. `config.signer(...)` takes an `IntentSigner`, a single method that signs canonical intent JSON. The Java SDK calls the public key function on every request, so return a cached value rather than calling your key store each time.

Python
```python
from ripple_custody_client_lib_python import CustodyClient, load_configuration


def sign(message: str) -> str:
    # Sign the UTF-8 bytes of message; return a base64 ASN.1 DER ECDSA signature.
    ...


def public_key() -> str:
    # Return the base64-DER SubjectPublicKeyInfo of the same key.
    ...


config = load_configuration()
config.sign_string = sign
config.read_public_key = public_key

with CustodyClient(config) as client:
    ...
```

Leave `RC_PRIVATE_KEY_FILE`, `RC_KMS_PROVIDER`, and `RC_CLIENT_SECRET` unset so that `load_configuration()` doesn't install another signer or switch to `client_credentials`. You can also subclass `KmsSigner` from `ripple_custody_client_lib_python.custody.kms`, implement `sign_message` and `read_public_key`, and assign those methods.

## Related topics

- [Sign with HashiCorp Vault](/products/custody/developer-experience/sdks/hashicorp-vault): keep the API user's key in the HashiCorp Vault Transit secrets engine.
- [Configure the SDK](/products/custody/developer-experience/sdks/configuration): the configuration file keys, environment variables, and TLS settings each SDK recognizes.
- [Work with intents](/products/custody/developer-experience/sdks/intents): track, approve, and reject the intents the SDK signs.
- [Authenticate API requests](/products/custody/identity-and-access/authentication/authenticate-api-requests): the underlying token and intent-signing flow of the Ripple Custody API.
- [Generate a key pair and register a public key](/products/custody/identity-and-access/authentication/generate-api-keys-and-register): create the API user's key and register its public key.