paperclipai/paperclip · error

Cloud control assertion is expired or has an invalid…

Error message

Cloud control assertion is expired or has an invalid lifetime

What it means

The final temporal check in verifyCloudControlAssertion enforces the assertion's validity window: exp must be in the future, iat must not be more than MAX_CLOCK_SKEW_SECONDS ahead of now, exp must be after iat, and the total lifetime (exp - iat) must not exceed CLOUD_CONTROL_MAX_LIFETIME_SECONDS. This error means the assertion is expired, not-yet-valid due to clock skew, or declares an unreasonably long lifetime. The request id is only consumed after this passes, so rejected assertions do not burn replay tokens.

Solutions

  1. Mint a fresh assertion immediately before each control request with a short lifetime under CLOUD_CONTROL_MAX_LIFETIME_SECONDS (e.g. 5 minutes)
  2. Synchronize clocks (NTP/chrony) on both the signing client and the verifying instance
  3. Fix the issuer so exp = iat + small delta and iat = now, both epoch-seconds integers
  4. Stop caching/persisting assertions; if retries are needed, mint a new token with a new requestId

Example fix

// before
const iat = 0, exp = iat + 60 * 60 * 24; // 24h lifetime
// after
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 300; // within CLOUD_CONTROL_MAX_LIFETIME_SECONDS
const assertion = mintAssertion({ sub: stackId, action, requestId: crypto.randomUUID(), iat, exp });
Defensive patterns

Strategy: retry

Validate before calling

function lifetimeOk(iat, exp, maxLifetime) {
  const now = Math.floor(Date.now() / 1000);
  return exp > now && iat <= now + 60 && exp > iat && (exp - iat) <= maxLifetime;
}
if (!lifetimeOk(claims.iat, claims.exp, MAX_LIFETIME)) throw new Error("assertion expired or over-long lifetime; re-mint");

Try / catch

async function withFreshAssertion(action) {
  try {
    return await control(action, mintAssertion({ ...claims(), iat: now(), exp: now() + 300 }));
  } catch (e) {
    if (e.message === "Cloud control assertion is expired or has an invalid lifetime" && attempts < 2) {
      // only safe retry path: mint a brand-new assertion (and new requestId) — never resend the old token
      return control(action, mintAssertion({ ...claims(), iat: now(), exp: now() + 300 }));
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: Sending an assertion after its exp has passed (cached or reused token); the verifying server's clock is far behind/ahead of the signer's clock; the issuer set a lifetime longer than CLOUD_CONTROL_MAX_LIFETIME_SECONDS; iat set in the future by a misconfigured signer.

Common situations: Long-lived cached assertions reused across control calls instead of per-request minting; NTP drift or wrong timezone/clock on a VM or container; an issuer hard-coding a 24h expiry that exceeds the max lifetime; a test fixture with frozen timestamps.

Related errors


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

Appendix: source

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

    !(CLOUD_CONTROL_ACTIONS as readonly string[]).includes(payload.action)
    || payload.action !== input.expectedAction
  ) {
    throw new Error("Cloud control assertion does not authorize this action");
  }
  if (
    !payload.requestId
    || payload.requestId.trim() !== payload.requestId
    || payload.requestId.length > 256
  ) {
    throw new Error("Cloud control assertion request id is invalid");
  }
  if (
    payload.exp <= nowSeconds
    || payload.iat > nowSeconds + MAX_CLOCK_SKEW_SECONDS
    || payload.exp <= payload.iat
    || payload.exp - payload.iat > CLOUD_CONTROL_MAX_LIFETIME_SECONDS
  ) {
    throw new Error("Cloud control assertion is expired or has an invalid lifetime");
  }
  // Consumed LAST, only after every other check passed: a rejected
  // assertion must not burn its request id, or an attacker could deny a
  // legitimate call by replaying a mangled copy of it first.
  if (!consumeControlRequestId(payload.requestId, payload.exp + MAX_CLOCK_SKEW_SECONDS, now.getTime())) {
    throw new Error("Cloud control assertion has already been used");
  }
  return payload as CloudControlClaims;
}

View on GitHub (pinned to 3f1d897a7c)