# Verify response signatures

Ripple Custody protects critical entities, such as domains, users, accounts, and intents, with a data integrity protocol. The API returns each protected entity in a signed envelope with three fields: `data`, `signature`, and `signingKey`. The notary signs the envelope with its API-signing key.

The SDKs verify supported signed envelopes before they return data from successful responses. Signature verification detects changes to signed data, even when TLS is in place. The SDKs don't verify every object, even when you enable response verification. See [Verification limits](#verification-limits) for the exceptions. Use this page to pin the notary key, handle verification failures, and verify stored payloads.

## How verification works

Response verification is on by default in all three SDKs. For each successful (2xx) JSON response, the SDK checks supported envelopes:

1. Finds the signed envelopes in the body, either a single envelope or the items of a collection.
2. Checks that each envelope's `signingKey` matches the notary API public key you pinned in the configuration.
3. Verifies the `signature` over the canonical form of `data`.


If a signature or pinned-key check fails, the call throws `ResponseSignatureError` and doesn't return the data. Responses that the platform doesn't sign, such as balances or the `requestId` returned when you submit an intent, pass through unchanged.

The SDK never fetches the notary key on its own. Reading the key from the same API whose responses it verifies would let a compromised server supply its own key. You must pin the key from a source you trust.

### Verification limits

- **TypeScript:** the SDK returns objects that contain `data` and `signature` but no `signingKey` **without verifying their signatures**. It logs a warning instead of throwing an error. Some platform responses, such as tickers, use this format. Don't treat these objects as verified data. The manual verifier returns `0` if it doesn't verify any envelopes.
- **Python:** the SDK skips signature verification for `CoreAccountAddress` envelopes when both `signature` and `signingKey` are null. It also skips these envelopes in `CoreAddressesCollection` and emits a `CustodyWarning`. Newly generated external account addresses can use this format. The SDK doesn't verify the signature on an address it skips.
- **All SDKs:** the SDKs don't verify unsigned responses or objects whose format the verifier doesn't recognize. The manual verifier counts the envelopes it verifies. The count doesn't confirm that the verifier checked the entire payload.


If an operation requires verified data, check how many envelopes the verifier checked. Stop if it skips an object you need. For a single signed envelope, require a count of `1`. For collections, compare the count with the number of items you expect to carry signatures. Investigate verification warnings. Don't suppress them or disable verification.

## Behavior without a pinned key

If verification is on and you haven't configured a notary API public key, the SDK refuses to build a client instead of running without verification:

| SDK | Where it fails | Error |
|  --- | --- | --- |
| TypeScript | `loadConfiguration()` | `CustodyError` |
| Java | `CustodyClient.fromConfig(config)` or `new CustodyClient(config)` | `CustodyError` |
| Python | `load_configuration()` | `ConfigurationError` |


To fix the error, pin the key as described in [Configure the key](#configure-the-key). To run without verification, see [Disable verification](#disable-verification).

## Get the notary API public key

You need the notary's **API-signing** public key, encoded as a base64 DER (Distinguished Encoding Rules) SubjectPublicKeyInfo value. The notary has other keys, such as the messaging key and the collections key. They don't verify API responses, and pinning one of them causes every protected response to fail.

Get the key out of band from a source you trust:

- If your organization operates Ripple Custody, ask the team that deployed the notary. The notary generates its API key during setup, and again during each key rotation. See [Notary key rotation and migration](/products/custody/operations-and-maintenance/security-maintenance/notary-key-rotation-and-migration).
- If Ripple hosts your deployment, contact the [Ripple support team](/products/custody/support/get-support).


To cross-check the value, call [List API public keys](/products/custody/reference/api/openapi/trustedpublickeys/gettrustedpublickeysapi) and compare `items[].data.publicKey` with the key you received. Treat this only as a check: don't build a client that discovers its pinned key from the API at startup.

## Configure the key

The `RC_NOTARY_API_PUBLIC_KEY` environment variable works the same way in all three SDKs:

```sh
export RC_NOTARY_API_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
```

The `config.toml` key is `notary_api_public_key` in all three SDKs. The accepted key formats and the programmatic setter differ by SDK, as the following tabs show. Environment variables override the file.

TypeScript
```toml
notary_api_public_key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
```

The SDK accepts a PEM (Privacy-Enhanced Mail) or base64 DER key and normalizes it. If you build the configuration in code with `generateNewConfiguration`, pass the key as `notaryApiPublicKey`.

Java
```toml
notary_api_public_key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
```

```java
CustodyClientConfig config = CustodyClientConfig.loadConfiguration()
    .notaryApiPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...");
CustodyClient client = CustodyClient.fromConfig(config);
```

The Java SDK compares the configured value with each envelope's `signingKey` as a string. Use the base64 DER value exactly as the API returns it, without PEM headers or line breaks.

Python
```toml
notary_api_public_key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
```

`load_configuration()` accepts a PEM or base64 DER key and normalizes it. If you construct a `Configuration` in code instead, pass the base64 DER value as `notary_api_public_key`.

## Rotate the notary key

After a notary key rotation, responses arrive signed with the new API-signing key.

Each SDK pins one key. Update the configured key when the new notary becomes active and restart your application. Until you do, protected responses fail with `ResponseSignatureError`.

For the rotation procedure, see [Notary key rotation and migration](/products/custody/operations-and-maintenance/security-maintenance/notary-key-rotation-and-migration).

## Handle ResponseSignatureError

`ResponseSignatureError` is a subclass of `CustodyError`. It means one of the following:

- A key other than the one you pinned signed the response. The usual cause is a notary key rotation, or a pinned key that isn't the API-signing key.
- The signature doesn't match the data, or the envelope isn't well formed. This can mean that something altered the response between the notary and your application, for example a proxy that rewrites response bodies.


Don't retry the call or turn off verification to get past the error. Confirm the pinned key with your operator first. If the key is correct, treat the response as untrusted and investigate the network path.

The following example shows where to handle the error:

TypeScript
```typescript
import { ResponseSignatureError } from '@ripple/ripple-custody-client-lib-typescript';

try {
  await client.governance.domains.list({ limit: 10 });
} catch (err) {
  if (err instanceof ResponseSignatureError) {
    // Don't use the data. Check the pinned notary key, then alert.
  }
  throw err;
}
```

Java
```java
import com.ripple.custody.errors.ResponseSignatureError;

try {
    client.governance().domains().list(10, null);
} catch (ResponseSignatureError e) {
    // Don't use the data. Check the pinned notary key, then alert.
    throw e;
}
```

Python
```python
from ripple_custody_client_lib_python import ResponseSignatureError

try:
    client.governance.domains.list(limit=10)
except ResponseSignatureError:
    # Don't use the data. Check the pinned notary key, then alert.
    raise
```

## Verify a payload manually

The client checks supported response envelopes automatically, with the exceptions in [Verification limits](#verification-limits). To check a payload from another source or a response body you saved earlier, use the client's manual verification method. It uses the configured key unless you pass another key. It returns the number of envelopes it verified.

Preserve the raw JSON response. Converting it to SDK models and back to JSON can change signed fields and cause verification to fail. Parse the original JSON as shown in these examples instead.

The following examples require exactly one verified envelope in a stored response body, `responseText` (`response_text` in Python). For a collection, use the expected number of signed items instead:

TypeScript
```typescript
const count = client.verifySigned(JSON.parse(responseText));
if (count !== 1) {
  throw new Error(`Expected one verified envelope, got ${count}`);
}
```

Java
```java
int count = client.verifySigned(responseText);
if (count != 1) {
    throw new IllegalStateException("Expected one verified envelope, got " + count);
}
```

Python
```python
import json

count = client.verify_signed(json.loads(response_text))
if count != 1:
    raise RuntimeError(f"Expected one verified envelope, got {count}")
```

## Disable verification

Turn verification off only for local development or a test cluster where no notary API key is available. Don't disable it in production: without it, your application trusts API responses on the strength of TLS alone. The TypeScript and Python SDKs treat the opt-out as deprecated and log a warning when you use it.

To opt out in any SDK, set `RC_VERIFY_RESPONSES=false`. Each SDK also has its own options:

TypeScript
```toml
verify_responses = false
```

If you build the configuration in code with `generateNewConfiguration`, pass `verifyResponses: false`.

Java
```toml
verify_responses = false
```

```java
CustodyClientConfig config = CustodyClientConfig.loadConfiguration().verifyResponses(false);
```

Python
```toml
verify_responses = false
```

```python
config = load_configuration(verify_responses=False)
```

## Related topics

- [Configure the SDK](/products/custody/developer-experience/sdks/configuration): configuration keys and environment variables for each SDK.
- [Errors, pagination, and HTTP settings](/products/custody/developer-experience/sdks/errors-and-http): the `CustodyError` hierarchy and HTTP transport settings.
- [Notary key rotation and migration](/products/custody/operations-and-maintenance/security-maintenance/notary-key-rotation-and-migration): the procedure that changes the API-signing key.