Browse the library
Documentation librarySecurity
Signing keys and rotation
Your app's signing key is how your software knows that every answer really comes from your app's server, not from a fake server or a proxy that rewrites responses. This guide covers the public key and its `kid`, how to embed keys in a build, and how to move to a new key in two steps without breaking anyone who already has your software installed.
8 min read
What the app signing key is
Every app is created with its own Ed25519 key pair:
- The secret half stays on the server, encrypted at rest (the Encryption at Rest method), and is never shown in the dashboard. The server uses it to sign every answer it sends your software, error answers included, plus offline tokens and update manifests.
- The public half is what you embed in your software. It can only verify, never sign, so shipping it inside a build is safe.
Each public key has an identifier, its kid: the first 16 hex characters of the SHA-256 of the public key. Under CLV1.1, every answer names the key that signed it in the x-clavis-kid header, and offline tokens and update manifests carry a kid too. The SDK looks the kid up in the list of keys the build trusts. If it is not there, the answer is refused with unknown_key; if it is there but the signature does not match, the answer is refused with invalid_response_signature. Because of the kid, one build can trust several keys at once, which is what makes rotation possible. The exact signed strings and verification rules are in the protocol specification.
Where to copy the public key
The public key appears in three places, each with a copy button:
- The Overview page in the block titled with your app's name, on the Public key card next to the App ID. It shows only the key that is signing now.
- Integration › Connect software: the settings for your software and the downloadable starter, with the public key filled in with the signing key.
- App security › Protection, tab Methods, in the Signing keys panel. It shows in Advanced Protection only. This is the complete view: every key with its
kidand a short note.
| Row | Meaning | Note shown |
|---|---|---|
| Current | The key that signs every answer | signing since the day it started signing |
| Next | A prepared key that signs nothing yet | prepared on the day it was created |
| Previous | The key that was just replaced, still in its grace period | trusted until the end of the grace period |
Under the panel you will find the line Keys for your next build, paste into ClavisClient and a code block titled ClavisClient. It lists every key currently shown (current, next, previous) in the exact appPublicKeys format. Use the copy button in the block's corner and paste it straight into your source.
Every workspace member can view and copy keys. Preparing, activating and discarding keys requires the Admin or Owner role; a Member who tries gets an error message.
Embedding keys in your software
ClavisClient takes keys through either of two options, or both (the SDK merges them into one list):
appPublicKey: a single key, exactly as on the Connect software page.appPublicKeys: a list of{ kid, key }. Thekidis optional; when it is missing, the SDK computes it from the key.
With neither, the constructor throws ClavisClient needs appPublicKey or appPublicKeys. We recommend appPublicKeys from day one, so each future rotation is just pasting a new block:
import { ClavisClient, hashComponent } from "@clavisauh/sdk";
const clavis = new ClavisClient({
baseUrl: "https://api.example.com",
appId: "app_…",
// Paste the whole block from the Signing keys panel
appPublicKeys: [
{ kid: "<current kid>", key: "<current public key>" },
{ kid: "<next kid>", key: "<next public key>" },
],
device: { fingerprint: hashComponent(machineId) },
deviceKey: await loadDeviceKey(),
});
Rotating in two steps
If the key changed in one go, every installed build that only knows the old key would refuse every answer from the server. So rotation is split into two actions in the dashboard, with a release in between. The panel shows the three milestones next to the key list: Prepare, Ship a build, Activate.
Step 1: Prepare a new key
- Open App security › Protection, tab Methods, and scroll down to the Signing keys panel. In Standard Protection, turn on Advanced Protection first.
- Under Prepare, click Prepare a new key.
- A Next row appears with the new
kid, and the ClavisClient block now holds both the current and the next key.
Nothing changes for your software yet: the server still signs with the current key. An app has at most one next key, so Prepare a new key stays hidden until you activate or discard it.
Step 2: Ship a build with both keys
Copy the ClavisClient block, replace appPublicKeys in your source, then build and release. This build trusts both the old and the new key, so it works before and after the switch. You can distribute it through signed releases.
Then wait until most of your users have updated. The dashboard does not count how many machines still run older builds, so when to activate is your call.
Step 3: Activate the new key
- On the Next row, click Activate the new key.
- The Activate the new key? dialog spells out what happens. Click Activate the new key to confirm, or Not yet to back out.
Right away:
- The server signs every answer, offline token and update manifest with the new key. The change applies almost at once; when the API runs on several instances, the others catch up within about 30 seconds.
- The old key moves to the Previous row, trusted until 14 days from now.
- The Overview and Connect software pages show the new key.
- Builds that only know the old key get
unknown_keyon every request.
There is no undo. After activation the server keeps only the public half of the old key; its secret half is gone. If you need another change, prepare a fresh key and start again from step 1.
The 14-day grace period
For 14 days after activation, the old key stays in the panel and in the ClavisClient block. The server no longer signs anything with it. The point is that a build you ship during this window still trusts the old key, so it still accepts offline tokens that the old key signed before the switch. After 14 days, the Previous row disappears and the block lists only the current key (plus a next key, if you prepared one).
If you activate again before the 14 days are up, the key you just replaced becomes Previous and the older one drops off the list immediately.
Discarding a prepared key
Click Discard this key on the Next row when you no longer need the prepared key, for example because you changed your mind before shipping any build with it. It happens immediately, with no confirmation dialog, and cannot be undone because the secret half is deleted. The current key is not affected.
If you already shipped a build containing the discarded key, that build keeps working, since it also trusts the current key. But your next prepare creates a different key, so you will need to ship another build carrying that one before you activate.
Rotation reminder
Signing Key Rotation sits in the Key and Release Integrity group of Advanced Protection, on the same Methods tab. Turn on its switch, then set Rotate every to anywhere from 30 to 730 days; the value saves when you leave the field. The default is 180 days. The Signing keys link next to the field jumps down to the panel.
With it on, the top of the Signing keys panel shows Rotation due and a date: the day the current key started signing (your last activation, or the day the app was created if you have never rotated) plus the number of days you set. Once that date has passed, the label turns into Rotation overdue in a warning color.
Handling an unknown key in your software
An older build that meets a key it does not know gets a ClavisVerificationError with code unknown_key. Since no answer can be verified, your software cannot read a download link or notice from the server, so handle this code with content that ships inside the build:
import { ClavisVerificationError } from "@clavisauh/sdk";
try {
await clavis.init();
} catch (err) {
if (err instanceof ClavisVerificationError && err.code === "unknown_key") {
// This build no longer trusts the key the server signs with
showMessage("Please download the latest version from our website.");
return;
}
throw err;
}
Edge cases
- Offline tokens: a build that only has the new key refuses tokens signed by the old one, so the user needs to go online once to get a new token. See Offline licenses.
- Custom clients: only CLV1.1 (header
x-clavis-proto: 1.1) carries akid. A client that speaks plain CLV1 gets answers without one, so move it to CLV1.1 before you rotate. - Error codes when calling the dashboard API directly: preparing while a next key exists returns
next_key_exists; activating before preparing returnsno_next_key. The panel hides those buttons in both cases. - 2FA: if the app has Team MFA Enforcement on, you need 2FA or a passkey on your own account to work with that app at all, the Signing keys panel included. See Dashboard account security.