Signing keys and rotation
Each merchant has an Ed25519 signing keypair. The service signs every offline token with the private key. Your shipped product embeds the matching public key and verifies tokens offline against it. The public key is not secret: it is shown on the Integration page and is safe to ship in your binary.
The public key is locked to your builds
Section titled “The public key is locked to your builds”If the private key and the key baked into a shipped build ever diverge, activation succeeds on the server but the product rejects the signed token it gets back, showing a generic error. The device looks activated everywhere except the one place that matters.
Guard against drift automatically with the build-time key check.
Discovering the current key
Section titled “Discovering the current key”GET /v1/products/{productId}/pubkey is public and read-only:
{ "productId": "…", "publicKeyRawB64": "<active key>", "publicKeys": ["<active key>", "<keys still verifying live leases>"] }publicKeyRawB64 is always the active signing key. publicKeys is the full set the service may currently sign with or
still has valid leases under, active first. Bake the whole set during a rotation window.
Why rotation is possible
Section titled “Why rotation is possible”Every token carries a kid: a short ID derived from the signing key (first 16 base64url characters of the SHA-256 of the raw
key). A build can hold several keys and use kid to pick the right one. kid is a routing hint, never a trust decision: a
forged kid can only select the wrong key, and the signature then fails.
Rotating without bricking anyone
Section titled “Rotating without bricking anyone”Rotation is carried out by the service operator at the merchant’s request. Your part is the order of operations, which matters more than any code:
-
Ship acceptance first. Release a build whose keyset contains both the current key and the incoming one. In the SDK, pass an array:
evaluateOffline({ publicKeyRawB64: [CURRENT_KEY, NEXT_KEY] /* …other fields */ });In the JUCE kit, set
cfg.publicKeyB64to the current key and add the next one withcfg.publicKeysB64.add(...). Nothing changes for users yet. -
Wait for that build to reach the field. This is the long pole. Measure it (installed-base telemetry, download counts, support signal) and treat it as the gate, not the plan. The SDK and kit report the keys they accept on activate and heartbeat so the operator can see adoption.
-
Then the operator switches signing to the new key. New tokens carry the new
kid; builds from step 1 accept them, and every token already issued under the old key stays valid until its lease lapses. -
Retire the old key only once no supported build depends on it and no live lease still carries its
kid. A lease lives for its term plus grace: 44 days on defaults, longer for products on thedeactivateseat policy (up to the product’slongLeaseDaysplus grace).
Doing step 3 before step 1 is the failure mode: every installed copy rejects every new token with unknown-key, and the only
fix is another release cycle.