# Notary key rotation and migration

This tutorial describes how to rotate the notary signing and messaging keys by migrating from your current notary service (the **old notary**) to a replacement notary service (the **new notary**). After following these steps, the **new notary** operates as the active notary service with new collections, messaging, and API keys, while keeping the chain of trust with the data that the **old notary** created.

The old notary must remain operational
This procedure expects the **old notary** to be fully functional with its existing KMS backend throughout the migration. If you rotate keys on the HSM as part of this work, make sure the rotation does not overwrite the HSM master key that the **old notary** currently uses to operate.

## When to do notary key rotation

Rotate the notary keys, or migrate to a new notary service, when you need to:

* Satisfy compliance and regulatory requirements, such as the Digital Operational Resilience Act (DORA).
* Comply with cybersecurity policies within your institution.
* Migrate from one HSM to another, or from an on-premise notary to a Ripple-hosted managed notary service.


The procedure uses a *chain-of-trust* process: you configure the **new notary** so that it recognizes the public key of the **old notary**, which lets it read the Merkle-tree data that the **old notary** created.

## Prerequisites

* **The state review authority public key is registered with Ripple Custody.** The final migration step recovers the anti-rewind file (ARF) — see [Recover your anti-rewind file](/products/custody/v1.37/operations-and-maintenance/backup-and-restore/recover-anti-rewind-file) — which requires a manifest signed by the state review authority. If this key is not yet registered, register it first — see [Register the state review authority public key](/products/custody/v1.37/operations-and-maintenance/backup-and-restore/register-a-key).
* **Operator access to both notary deployments.** You must be able to set environment variables for the **old notary** and the **new notary**, start and stop each service, and read the **new notary**'s log files.
* **A Ripple Custody API user who can propose, approve, and sign intents**, and who can read system properties. See [Manage intents and approvals](/products/custody/v1.37/governance/intents/manage-intents-and-approvals).
* **A current backup of the Ripple Custody database.** Take the backup before you begin. The recovery-manifest step runs against a copy of the database, and the backup lets you restore the system state if the migration fails. Follow PostgreSQL backup best practices, and enable point-in-time recovery (PITR) so that you can restore the database to a moment just before the migration.


Disruption notice
Steps 1–3 are non-disruptive: the **old notary** remains active and continues to sign intents. From Step 4 onward, the **old notary** stops processing intents, and the **new notary** does not process intents until it completes recovery in Step 6 — the system runs without a notary until that point. Wait until any in-progress intents have executed successfully, then do Steps 4–7 in quick succession.

## Rotate the notary keys

The following sections describe the steps to migrate from the **old notary** to the **new notary**.

### Step 1: Retrieve the old notary collections public key

Use the Ripple Custody API to retrieve the **old notary** collections public key from the system properties, so that you can register this key with the **new notary**:

1. Call the [List system properties](/products/custody/v1.37/reference/api/openapi/systemproperties/getsystemproperties) operation, with query parameter `id=NOTARY_COLLECTIONS_KEY`.
2. Copy the `items[0].data.value.publicKey.value`.


The response body is similar to the following:

```json
{
    "items": [
        {
            "data": {
                "id": "NOTARY_COLLECTIONS_KEY",
                "value": {
                    "publicKey": {
                        "value": "<OLD_NOTARY_COLLECTIONS_PUBLIC_KEY>",
                        "type": "PublicKey"
                    }
                },
                "metadata": {
                    "description": null,
                    "revision": 1,
                    "createdAt": "2025-07-14T12:23:44.653Z",
                    "createdBy": {
                        "id": "f0ca2fb4-369f-4617-aaa3-6a2f3370e931",
                        "domainId": "5cd224fe-193e-8bce-c94c-c6c05245e2d1"
                    },
                    "lastModifiedAt": "2025-07-14T12:23:44.653Z",
                    "lastModifiedBy": {
                        "id": "f0ca2fb4-369f-4617-aaa3-6a2f3370e931",
                        "domainId": "5cd224fe-193e-8bce-c94c-c6c05245e2d1"
                    },
                    "customProperties": {}
                },
                "type": "NotaryCollectionsKeySystemProperty",
                "access": "ReadOnly"
            },
            "signature": "<SIGNATURE>",
            "signingKey": "<SIGNINGKEY>"
        }
    ],
    "count": 1
}
```

### Step 2: Configure the new notary and launch it in standby mode

Register the **old notary** key with the **new notary**, and launch the **new notary** in standby mode. This generates all the new public keys — for the collections, messaging, and API calls — that you need in the next step.

In standby mode, the **new notary** starts without taking over as the active notary. It reads the **old notary** collections public key to establish the chain of trust, generates its new key material, and writes that material to its logs — it does not process intents. The **old notary** remains the active notary until the final migration step.

You control both behaviors through two environment variables in the **new notary**'s configuration:

| Variable | Value |
|  --- | --- |
| `MIGRATION_PREVIOUS_NOTARY_COLLECTIONS_PUBLIC_KEY` | The value of the `NOTARY_COLLECTIONS_KEY` system property that you retrieved in [Step 1](#step-1-retrieve-the-old-notary-collections-public-key). This is how you insert the old key into the **new notary**. |
| `MIGRATION_STANDBY_MODE` | `"true"` — starts the notary in standby mode. |


Keep the new notary disconnected from the notary bridge
Make sure the **new notary** is not connected to the notary bridge while it runs in standby mode. If the notary uses HTTP mode, make sure `HMZ_NOTARY_BRIDGE_HTTP_URI` does not point to the bridge.

To launch in standby mode:

1. Set the two environment variables in the **new notary**'s configuration, and start the service:

```yaml
MIGRATION_PREVIOUS_NOTARY_COLLECTIONS_PUBLIC_KEY: "<OLD_NOTARY_COLLECTIONS_PUBLIC_KEY>"
MIGRATION_STANDBY_MODE: "true"
```
Replace `<OLD_NOTARY_COLLECTIONS_PUBLIC_KEY>` with the value that you retrieved in [Step 1](#step-1-retrieve-the-old-notary-collections-public-key).
2. Check the log files for the **new notary**. The logs contain the following six items, labeled as follows:
  * `Migration key material generated`
  * `New trusted collections public key`
  * `New messaging public key`
  * `New API public key`
  * `Previous notary public key`
  * `Previous notary public key signature`
Record all six values. You need them for the key-migration intent in the next step. The log labels differ from the intent payload field names, so use the mapping table in [Step 3](#step-3-create-the-key-migration-intent) when building the payload.
At this point, the **new notary** is not running — standby mode generates the key material, writes it to the logs, and stops. The **old notary** remains the active notary.


If the **new notary** reports that it cannot create the new keys file, see [Troubleshooting](#troubleshooting).

### Step 3: Create the key-migration intent

Submit an intent to execute the key migration, using the information that the **new notary** generated. When you approve and sign this intent, it registers both the old and new public keys with Ripple Custody in preparation for bringing up the **new notary**.

1. In the Custody API, call the [Propose an intent](/products/custody/v1.37/reference/api/openapi/intents/createintent) operation with the `v0_AddTrustedPublicKeysForMigration` intent.  (For more information about requesting intents, see [Manage intents and approvals](/products/custody/v1.37/governance/intents/manage-intents-and-approvals).)
The intent payload has the following structure:

```json
{
    "newCollectionSigningPublicKey":             "",
    "newMessageSigningPublicKey":                "",
    "newApiSigningPublicKey":                    "",
    "newKeyMaterial":                            "",
    "currentCollectionSigningPublicKey":         "",
    "currentCollectionSigningPublicKeySignature": "",
    "type": "v0_AddTrustedPublicKeysForMigration"
}
```
2. Copy the six values from the **new notary** logs into the payload fields. The payload field names differ from the labels the notary writes to its logs, so use this mapping:
| Payload field | Notary log label |
|  --- | --- |
| `newCollectionSigningPublicKey` | `New trusted collections public key` |
| `newMessageSigningPublicKey` | `New messaging public key` |
| `newApiSigningPublicKey` | `New API public key` |
| `newKeyMaterial` | `Migration key material generated` |
| `currentCollectionSigningPublicKey` | `Previous notary public key` |
| `currentCollectionSigningPublicKeySignature` | `Previous notary public key signature` |
3. Approve and sign the intent.


To verify that the keys are registered, call the following API operations:

* [List trusted-collection public keys](/products/custody/v1.37/reference/api/openapi/trustedpublickeys/gettrustedpublickeystrustedcollection)
* [List api public keys](/products/custody/v1.37/reference/api/openapi/trustedpublickeys/gettrustedpublickeysapi)
* [List messaging public keys](/products/custody/v1.37/reference/api/openapi/trustedpublickeys/gettrustedpublickeysmessages)


Note
At this point the **old notary** is still active, and continues to process intents using the current (old) public keys, until you are ready to migrate to the **new notary**.

### Step 4: Scale down the old notary

From this step, the **old notary** stops processing intents, and the **new notary** does not process intents until it completes recovery in [Step 6](#step-6-launch-the-new-notary-in-recovery-mode) — the system runs without a notary until that point.

1. Wait until any in-progress intents have executed successfully.
2. Scale down the **old notary** instance.


### Step 5: Generate the recovery manifest for the anti-rewind file

The **new notary** needs the anti-rewind file (ARF) to load the current Ripple Custody system state before it can sign intents. To rebuild the ARF, generate a signed manifest of the system state. (For more information about the ARF, see [Anti-rewind file](/products/custody/v1.37/overview/security/data-integrity-and-audit-trail#anti-rewind-file-arf).)

This step requires the state review authority key described in [Prerequisites](#prerequisites): the state review authority must sign the manifest.

Follow the disaster recovery procedure described in [Recover your anti-rewind file](/products/custody/v1.37/operations-and-maintenance/backup-and-restore/recover-anti-rewind-file) to generate the signed manifest.

Before you continue, confirm that both the manifest JSON file and the signature file are present.

### Step 6: Launch the new notary in recovery mode

Reconfigure the **new notary** so that it starts in recovery mode instead of standby mode:

1. Remove all of the `MIGRATION_*` environment variables that you set for standby mode:
  * `MIGRATION_PREVIOUS_NOTARY_COLLECTIONS_PUBLIC_KEY`
  * `MIGRATION_STANDBY_MODE`
  * `MIGRATION_KEY_MATERIAL_DIRECTORY`, if you set it
2. Set the recovery environment variables with the manifest and signature that you generated in Step 5:
| Variable | Value |
|  --- | --- |
| `ANTI_REWIND_RECOVERY_STATE_MANIFEST` | The manifest, as a serialized JSON string. |
| `ANTI_REWIND_RECOVERY_STATE_MANIFEST_SIGNATURE` | The manifest signature, encoded in base64. |
3. Start the **new notary**. The service runs in anti-rewind-file recovery mode and rebuilds the ARF. While in recovery mode, the notary does not serve requests, so no intents can be executed.
4. Wait until the **new notary** logs show the following message, which confirms that the recovery is complete:
`Recovery completed, remember to unset ANTI_REWIND_RECOVERY_* env variables before running it again`
Do not continue to the next step until this message appears.
5. To verify, call any of the following API operations, and check the `publicKey` value and that the `revision` number is updated:
  * [List system properties](/products/custody/v1.37/reference/api/openapi/systemproperties/getsystemproperties) with query parameter `id=NOTARY_COLLECTIONS_KEY`, as in [Step 1](#step-1-retrieve-the-old-notary-collections-public-key)
  * [List trusted-collection public keys](/products/custody/v1.37/reference/api/openapi/trustedpublickeys/gettrustedpublickeystrustedcollection)
  * [List api public keys](/products/custody/v1.37/reference/api/openapi/trustedpublickeys/gettrustedpublickeysapi)
  * [List messaging public keys](/products/custody/v1.37/reference/api/openapi/trustedpublickeys/gettrustedpublickeysmessages)


### Step 7: Remove the recovery environment variables

After the recovery completes, remove the recovery environment variables and restart the **new notary** one more time:

1. Remove `ANTI_REWIND_RECOVERY_STATE_MANIFEST` and `ANTI_REWIND_RECOVERY_STATE_MANIFEST_SIGNATURE`.
2. Restart the **new notary** service.


The **new notary** is now out of recovery mode and operates as the active notary service.

## Verification

Before you return the system to normal operations, verify that the **new notary** processes governed operations end to end:

1. Check the **new notary** service logs for a normal startup, and confirm that no `MIGRATION_*` or `ANTI_REWIND_RECOVERY_*` environment variables remain in its configuration.
2. Call the [List system properties](/products/custody/v1.37/reference/api/openapi/systemproperties/getsystemproperties) operation with query parameter `id=NOTARY_COLLECTIONS_KEY`, as in Step 1, and confirm that the returned public key is now the **new notary** collections public key, with an updated `revision`.
3. Submit a test intent, approve and sign it, and verify that it reaches an executed state. Any governed intent confirms that the **new notary** attests intents end to end — for example, create a user, create an account (a `v0_CreateAccount` intent — see [Manage accounts with the API](/products/custody/v1.37/accounts-and-assets/accounts/manage-accounts-api#create-an-account-with-the-api)), or send a transaction.
4. Send a low-value test transaction from an existing account, and verify that it completes signing and broadcasts. See [Send assets with the API](/products/custody/v1.37/transactions/send-and-receive/send-assets-api).
5. Confirm that intents submitted before the migration window, if any remain, complete or fail cleanly rather than staying stuck in a pending state.


If any check fails, stop and contact your Customer Partner Engineer (CPE) or the [Technical Services team](https://metaco.atlassian.net/servicedesk/customer/portal/1) before processing production operations.

## Troubleshooting

* **The new notary reports that it cannot create the new keys file:** Set a third environment variable, `MIGRATION_KEY_MATERIAL_DIRECTORY: /tmp`, in the **new notary**'s configuration and restart the service. Remove this variable, together with the other `MIGRATION_*` variables, when you launch recovery mode in Step 6.
* **The new notary does not write the six key items to its logs:** Make sure `MIGRATION_STANDBY_MODE` is set to `"true"` and that `MIGRATION_PREVIOUS_NOTARY_COLLECTIONS_PUBLIC_KEY` contains the value from Step 1. Also make sure the **new notary** is not connected to the notary bridge — in HTTP mode, `HMZ_NOTARY_BRIDGE_HTTP_URI` must not point to the bridge.
* **The key-migration intent is rejected or the values don't match:** The notary log labels differ from the intent payload field names. Check each value against the mapping table in Step 3.
* **The public keys or revision numbers don't update after recovery:** Confirm that the key-migration intent from Step 3 was approved and signed before you scaled down the **old notary**, and that you removed all `MIGRATION_*` variables before starting recovery mode.