paperclipai/paperclip · error
Cloud control signature is invalid
Error message
Cloud control signature is invalid
What it means
verifyCloudControlAssertion verifies the Ed25519 signature over signingInput ("header.payload" ASCII) using the public key selected by the header's kid from the PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS env configuration. This error means node:crypto's verify() returned false: the signature bytes do not match that key over that exact signing input.
Solutions
- Confirm the private key used to sign matches the public JWK published under the same kid in PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS
- Sign the exact bytes `${encodedHeader}.${encodedPayload}` of the final compact token, and never mutate header/payload after signing
- Regenerate/rotate the JWKS entry so it contains the public half of the currently deployed signing key, and restart the instance
- Verify signature encoding is base64url (no '+', '/', or '=') and that signing used Ed25519/EdDSA
Example fix
// before
const sig = Buffer.from(crypto.sign(null, data, privateKey)).toString("base64");
// after
const sig = Buffer.from(crypto.sign(null, Buffer.from(`${encodedHeader}.${encodedPayload}`, "ascii"), privateKey))
.toString("base64url");
const jws = `${encodedHeader}.${encodedPayload}.${sig.toString()}`; Defensive patterns
Strategy: try-catch
Try / catch
try {
return verifyCloudControlAssertion({ compactJws: token, expectedAction });
} catch (e) {
if (e.message === "Cloud control signature is invalid") {
// signature mismatch: token was modified or signed with an unknown key — fail closed, never retry same token
logger.warn("cloud control signature verification failed", { kid: peekKid(token) });
return respond(401, "invalid signature");
}
throw e;
} Prevention
- Keep PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS in sync with the deployed signing private key; rotate both together
- Sign the exact final "header.payload" bytes; never re-serialize claims after signing
- Use base64url encoding end-to-end; avoid base64 in signature output
- Add a self-test: sign then verify a canary assertion at deploy time
When it happens
Trigger: The assertion was signed with a private key whose public half is not the JWKS entry for the given kid; the payload or header was modified after signing; the signature was re-encoded (e.g. base64 vs base64url) or truncated; signing happened over a different serialization (pretty-printed JSON, different key order).
Common situations: JWKS env var points at a rotated key set while clients still sign with the old private key; the kid matches but the key material was copy-pasted incorrectly; a debug build re-serialized the payload after signing; clock-shifted test harness reuses a captured token with edited claims.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Cloud control assertion is not a compact JWS
- Cloud control protected header is invalid
- Cloud control assertion does not authorize this action
- Cloud control assertion is expired or has an invalid…
- Cloud control assertion request id is invalid
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/84ab0de872ace629.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/cloud-runtime-identity.ts:528
const parts = input.compactJws.split(".");
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
throw new Error("Cloud control assertion is not a compact JWS");
}
const [encodedHeader, encodedPayload, encodedSignature] = parts;
const header = decodeJsonPart(encodedHeader, "protected header");
if (
header.alg !== "EdDSA"
|| header.typ !== CLOUD_CONTROL_JWS_TYPE
|| typeof header.kid !== "string"
|| !header.kid
) {
throw new Error("Cloud control protected header is invalid");
}
const key = publicKeyForKid(env, header.kid);
const signature = Buffer.from(encodedSignature, "base64url");
const signingInput = Buffer.from(`${encodedHeader}.${encodedPayload}`, "ascii");
if (!verify(null, signingInput, key, signature)) {
throw new Error("Cloud control signature is invalid");
}
const payload = decodeJsonPart(encodedPayload, "payload");
const configuredStackId = nonEmpty(env.PAPERCLIP_CLOUD_STACK_ID);
const nowSeconds = Math.floor(now.getTime() / 1000);
if (
payload.v !== 1
|| payload.iss !== CLOUD_RUNTIME_IDENTITY_ISSUER
|| payload.aud !== CLOUD_CONTROL_AUDIENCE
|| typeof payload.sub !== "string"
|| typeof payload.action !== "string"
|| typeof payload.requestId !== "string"
|| typeof payload.iat !== "number"
|| !Number.isInteger(payload.iat)
|| typeof payload.exp !== "number"
|| !Number.isInteger(payload.exp)
) {
throw new Error("Cloud control claims are incomplete");View on GitHub (pinned to 3f1d897a7c)