Skip to main content

Rotate Signing Keys

Replace the active signing key without losing the ability to verify existing checkpoints and exports created under the old key.

Before you start

Confirm the ledger is clean before rotating:

php artisan chronicle:verify

Fix any failures before continuing. Rotation on a broken chain will not repair it.

1. Generate the new keypair

php artisan chronicle:key:generate --id=my-key-2026

The command prints a base64-encoded private key, a public key, and a ready-to-paste signing.keys config entry. Store the private key in your secret manager immediately — it is not saved anywhere by the command.

2. Add the new key to signing.keys (without activating it)

Copy the printed entry into config/chronicle.php. Keep the old key exactly as-is — do not remove it yet:

'signing' => [
'active' => env('CHRONICLE_ACTIVE_KEY', 'chronicle-dev-key'),
'keys' => [
// Existing key stays unchanged
'chronicle-dev-key' => [
'provider' => \Chronicle\Signing\Ed25519SigningProvider::class,
'algorithm' => 'ed25519',
'private_key' => env('CHRONICLE_PRIVATE_KEY'),
'public_key' => env('CHRONICLE_PUBLIC_KEY'),
],
// New key added — not yet active
'my-key-2026' => [
'provider' => \Chronicle\Signing\Ed25519SigningProvider::class,
'algorithm' => 'ed25519',
'private_key' => env('CHRONICLE_NEW_PRIVATE_KEY'),
'public_key' => env('CHRONICLE_NEW_PUBLIC_KEY'),
],
],
],

Set CHRONICLE_NEW_PRIVATE_KEY and CHRONICLE_NEW_PUBLIC_KEY in your secrets manager and deploy this config change. The active key has not changed — this deploy is safe.

3. Create the boundary checkpoint and get the activation instruction

php artisan chronicle:key:rotate my-key-2026

Output:

Creating boundary checkpoint before rotation...
✓ Boundary checkpoint created
ID: 01JXX...
Algorithm: ed25519
Key: chronicle-dev-key

Rotation ready. To activate my-key-2026, update your environment:

CHRONICLE_ACTIVE_KEY=my-key-2026

After deploying the updated environment:
php artisan chronicle:key:list
php artisan chronicle:verify

The checkpoint anchors the current ledger head under the old key. All future checkpoints and exports will be signed by my-key-2026.

4. Activate the new key

Set CHRONICLE_ACTIVE_KEY=my-key-2026 in your secrets manager and deploy.

5. Verify the full ledger

php artisan chronicle:verify
php artisan chronicle:key:list

The full ledger — including checkpoints created before rotation under chronicle-dev-key — should verify cleanly because the old public key is still in the ring.

6. Retire the old private key

Once you have confirmed integrity, remove private_key from the old key's config entry:

// Retired — private key removed, public key retained for historic verification
'chronicle-dev-key' => [
'provider' => \Chronicle\Signing\Ed25519SigningProvider::class,
'algorithm' => 'ed25519',
'public_key' => env('CHRONICLE_OLD_PUBLIC_KEY'),
// no private_key
],
warning

Keep public_key in the ring forever. Removing it makes any artifact signed by that key permanently unverifiable.

See also