> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign Changes with Signing Keys

> Register signing keys, sign API requests with FORMAL-SIG, and make Connectors verify signed configuration

## Overview

Change authorization proves which key approved each change to your Formal configuration:

1. You register the **public key** of a key pair as a signing key.
2. You sign API requests with the private key, using the `FORMAL-SIG` authorization scheme instead of an API key.
3. Formal verifies the signature and records it with the change.
4. Optionally, Connectors verify those recorded signatures before they load configuration.

Use it to require that configuration changes come from a reviewed pipeline, such as CI with a key in a hardware security module. With Connector verification, a change made without your key never reaches your data plane.

Signing is opt-in. Requests authenticated with an API key keep working, unless Connectors run in `enforce` mode.

## Supported Algorithms

| Algorithm | `Algorithm` value | Public key format | Signature format |
| - | - | - | - |
| Ed25519 | `ed25519` | Raw 32-byte key, base64 | Raw 64-byte signature, base64 |
| ECDSA P-384 | `ecdsa-p384` | DER SubjectPublicKeyInfo, or uncompressed point, base64 | ASN.1 DER signature over the SHA-384 digest, base64 |
| ML-DSA-87 | `ml-dsa-87` | Encoded ML-DSA-87 public key (2,592 bytes), base64 | ML-DSA-87 signature with an empty context, base64 |

All values use standard base64 with padding.

## 1. Generate a Key Pair

Keep the private key in your signing system. Only the public key goes to Formal.

<Tabs>
  <Tab title="Ed25519">
    ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    openssl genpkey -algorithm ed25519 -out signing-key.pem
    openssl pkey -in signing-key.pem -pubout -outform DER | tail -c 32 | base64 | tr -d '\n'
    ```
  </Tab>

  <Tab title="ECDSA P-384">
    ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    openssl ecparam -name secp384r1 -genkey -noout -out signing-key.pem
    openssl ec -in signing-key.pem -pubout -outform DER | base64 | tr -d '\n'
    ```
  </Tab>

  <Tab title="ML-DSA-87">
    Generate the key pair with a library or tool that supports ML-DSA-87 (FIPS 204), such as OpenSSL 3.5 or later. Register the encoded public key, not a PEM or DER wrapper.
  </Tab>
</Tabs>

The last command prints the base64 public key to register.

## 2. Register the Signing Key

<Steps>
  <Step title="Open Signing Keys">
    Go to [Signing Keys](https://app.formal.ai/api-keys?tab=signing-keys), the **Signing Keys** tab of **API Keys & Signing Keys**. You can also search for **Signing Keys** in the command menu. You need the **Developer** permission.
  </Step>

  <Step title="Create the key">
    Click **Create Signing Key**. Enter a **Name**, choose the **Algorithm**, and paste the **Public Key (Base64)**. Click **Create**.
  </Step>

  <Step title="Copy the ID">
    Copy the key's **ID** from the table. You use it as `KeyId` when you sign.
  </Step>
</Steps>

The key belongs to you. Requests signed with it run as you, with your permissions. To sign as an automation identity, create the key while signed in as that identity, for example with its API key and `CreateSigningKey`.

**Verify:** The key appears with status `active`.

## 3. Sign a Request

Send the signature in the `Authorization` header instead of `X-API-KEY`:

```text theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
Authorization: FORMAL-SIG Algorithm=<ALGORITHM>, KeyId=<SIGNING_KEY_ID>, Timestamp=<TIMESTAMP>, SignedFields=, Signature=<BASE64_SIGNATURE>
```

| Component | Value |
| - | - |
| `Algorithm` | The key's algorithm. It must match the registered key. |
| `KeyId` | The signing key ID |
| `Timestamp` | The current time in RFC 3339, in UTC, such as `2026-10-04T22:00:00Z` |
| `SignedFields` | Leave empty to sign every field of the body. If you list fields, separate them with `;`, and list every top-level field of the body. |
| `Signature` | The base64 signature of the canonical payload |

### Build the Canonical Payload

Sign these four parts, joined by newlines (`\n`), with no trailing newline:

```text theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
POST
/core.v1.PoliciesService/UpdatePolicy
2026-10-04T22:00:00Z
<CANONICAL_BODY>
```

1. The HTTP method, `POST` for every Formal API call
2. The request path
3. The `Timestamp` value, character for character
4. The canonical body

To build the canonical body, sort the top-level JSON fields of the request body by name. For each field, write its name on one line and its value as compact JSON on the next. Join everything with newlines. Nested objects keep their key order, so sign the values exactly as you send them. An empty body or `{}` gives an empty canonical body.

For the body `{"status":"active","name":"prod-db"}`, the canonical body is:

```text theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
name
"prod-db"
status
"active"
```

### Example: Sign with OpenSSL

This script signs and sends a request with an Ed25519 key:

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
API_PATH="/core.v1.PoliciesService/UpdatePolicy"
BODY='{"id":"<POLICY_ID>","status":"active"}'
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)

# Canonical body for this BODY: fields sorted by name, values as compact JSON
printf 'POST\n%s\n%s\n%s\n%s\n%s\n%s' "$API_PATH" "$TIMESTAMP" \
  "id" '"<POLICY_ID>"' "status" '"active"' > payload.txt

SIGNATURE=$(openssl pkeyutl -sign -rawin -inkey signing-key.pem -in payload.txt | base64 | tr -d '\n')

curl -X POST "https://api.joinformal.com$API_PATH" \
  -H "Content-Type: application/json" \
  -H "Authorization: FORMAL-SIG Algorithm=ed25519, KeyId=<SIGNING_KEY_ID>, Timestamp=$TIMESTAMP, SignedFields=, Signature=$SIGNATURE" \
  -d "$BODY"
```

For ECDSA P-384, sign with `openssl dgst -sha384 -sign signing-key.pem payload.txt` instead.

**Verify:** The API returns `200`. A wrong signature, an unknown or revoked key, or a timestamp outside the window returns an authentication error.

<Note>
  The timestamp must be at most 5 minutes old and at most 1 minute in the
  future. Keep your signing system's clock in sync.
</Note>

## 4. Verify Signatures on Connectors (Optional)

Connectors can check the signature recorded with each configuration entity before they load it. This covers most configuration a Connector loads, such as Resources, policies, listeners, Native Users, users, and groups.

Set these environment variables on the Connector:

| Variable | Value |
| - | - |
| `FORMAL_CHANGE_AUTH_VERIFICATION_MODE` | `off` (default), `audit`, or `enforce` |
| `FORMAL_CHANGE_AUTH_PUBLIC_KEYS` | JSON array of the public keys to trust |

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
FORMAL_CHANGE_AUTH_VERIFICATION_MODE=audit
FORMAL_CHANGE_AUTH_PUBLIC_KEYS='[{"id":"<SIGNING_KEY_ID>","algorithm":"ed25519","public_key":"<BASE64_PUBLIC_KEY>"}]'
```

* **`audit`:** The Connector loads every entity, and logs `Change auth verification failure (audit mode)` for each unsigned or invalid one.
* **`enforce`:** The Connector rejects any response that contains an unsigned or invalid entity. It needs at least one key in `FORMAL_CHANGE_AUTH_PUBLIC_KEYS`.

<Warning>
  In `enforce` mode, any change made without a trusted signature stops the
  Connector from loading that configuration. This includes changes from the
  Control Plane UI, Terraform with an API key, and directory sync. Run in
  `audit` mode first, and switch only when the logs show no failures.
</Warning>

## Rotate a Key

1. Generate a new key pair and register it.
2. Add the new public key to `FORMAL_CHANGE_AUTH_PUBLIC_KEYS` on your Connectors.
3. Switch your signing system to the new key.
4. Revoke the old key with `UpdateSigningKey` by setting its status to `revoked`, or delete it.

A revoked key can no longer authenticate requests. Remove it from `FORMAL_CHANGE_AUTH_PUBLIC_KEYS` once no configuration still carries its signature.

## API

Manage signing keys with `core.v1.ChangeAuthorizationService`: `CreateSigningKey`, `ListSigningKeys`, `UpdateSigningKey`, and `DeleteSigningKey`. See the **Change Authorization** section of the API reference.

## Next Steps

<CardGroup cols={2}>
  <Card title="API Introduction" icon="code" href="/docs/api/introduction">
    Authenticate and call the Formal API
  </Card>

  <Card title="Permissions" icon="key" href="/docs/guides/core-concepts/permissions">
    Control who can manage signing keys
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.