paperclipai/paperclip · error

Cloud control protected header is invalid

Error message

Cloud control protected header is invalid

What it means

After the JWS splits into three parts, verifyCloudControlAssertion decodes the protected header and requires alg === "EdDSA", typ equal to the expected cloud-control JWS type, and a non-empty string kid. This error means the header parsed as JSON but failed one of those constraints, so the assertion cannot be bound to a known Ed25519 signing key.

Solutions

  1. Inspect the decoded protected header of the token (base64url-decode part 1) and confirm alg is exactly "EdDSA", typ matches CLOUD_CONTROL_JWS_TYPE, and kid is a non-empty string
  2. Fix the token issuer to set { alg: "EdDSA", typ: <CLOUD_CONTROL_JWS_TYPE>, kid: <keyId> } in the protected header
  3. Verify the signer uses an Ed25519 (OKP) key, per the JWKS requirements in publicKeyForKid
  4. Ensure the kid used to sign corresponds to an entry in PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS, otherwise the next check will also fail

Example fix

// before
const header = { alg: "RS256", kid };
// after
const header = { alg: "EdDSA", typ: CLOUD_CONTROL_JWS_TYPE, kid };
const jws = `${b64url(JSON.stringify(header))}.${b64url(payload)}.${sig}`;
Defensive patterns

Strategy: validation

Validate before calling

function headerLooksValid(token) {
  try {
    const h = JSON.parse(Buffer.from(token.split(".")[0], "base64url").toString("utf8"));
    return h.alg === "EdDSA" && typeof h.kid === "string" && h.kid.length > 0 && typeof h.typ === "string" && h.typ.length > 0;
  } catch { return false; }
}
if (!headerLooksValid(assertion)) throw new Error("assertion protected header fails local pre-check");

Type guard

const isCloudControlHeader = (h: unknown): h is { alg: "EdDSA"; typ: string; kid: string } =>
  !!h && typeof h === "object" && (h as any).alg === "EdDSA" && typeof (h as any).kid === "string" && !!(h as any).kid;

Try / catch

try {
  return verifyCloudControlAssertion({ compactJws: token, expectedAction });
} catch (e) {
  if (e.message === "Cloud control protected header is invalid") {
    // decode header locally (without verifying) to diagnose alg/typ/kid, then re-mint
    logger.warn("assertion rejected: bad protected header");
    return respond(401, "invalid assertion header");
  }
  throw e;
}

Prevention

When it happens

Trigger: A token signed with a different algorithm (e.g. RS256, HS256) or missing/wrong typ header; a header without a kid or with an empty kid; a hand-rolled signer omitting required header fields.

Common situations: A client library defaults to a different JWT algorithm; the signing side was updated to a new token type without updating the assertion issuer; a generic JWT library was used instead of the cloud control assertion signer; token was issued by an older/other environment with different header conventions.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/03dae5f8c8ab0097. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/cloud-runtime-identity.ts:522

  expectedAction: CloudControlAction;
  env?: NodeJS.ProcessEnv;
  now?: Date;
}): CloudControlClaims {
  const env = input.env ?? process.env;
  const now = input.now ?? new Date();
  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"

View on GitHub (pinned to 3f1d897a7c)