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

  1. Confirm the private key used to sign matches the public JWK published under the same kid in PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS
  2. Sign the exact bytes `${encodedHeader}.${encodedPayload}` of the final compact token, and never mutate header/payload after signing
  3. Regenerate/rotate the JWKS entry so it contains the public half of the currently deployed signing key, and restart the instance
  4. 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

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


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)