feat(iios): context-attestation verifier — the July 12 stolen-token proof (§1)

A valid actor token is no longer enough. This adds the trust-layer core so IIOS
can require a signed CONTEXT ATTESTATION from an authorized parent (AppShell etc.)
before a privileged request — defeating a hacked app replaying a stolen token.

- prisma: IiosClientRegistry (registered clients allowed to attest, per app) +
  IiosAttestationNonce (single-use replay ledger). Migration applied.
- ContextAttestationVerifier + ports (ClientRegistryPort, NonceStorePort) + a dev
  signer. Verifies: registered+active client, signature, aud=iios, app_id matches
  the token AND is allowed for the client, freshness, single-use nonce. Fail-closed.
- Prisma-backed stores; nonce reserve is atomic via the unique PK (P2002 = replay).
- Tests (9 pass): happy path + the five rejection cases the CEO named
  (unknown client, wrong audience, expired, replayed nonce, app-mismatch) +
  forged-signature + disabled-client; a DB atomicity test (skips if engine offline).

Decoupled from the request path on purpose — wiring it into the messaging guard
(the three-proof gate) + demoting be-crm to an attestation forwarder is the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 20:11:04 +05:30
parent 77dd5bac82
commit dd6a4cd4fa
5 changed files with 356 additions and 0 deletions
@@ -0,0 +1,24 @@
-- Trust plane (July 12): context attestation client registry + nonce replay ledger.
CREATE TABLE "IiosClientRegistry" (
"clientId" TEXT NOT NULL,
"clientType" TEXT NOT NULL,
"ownerService" TEXT,
"allowedAppIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
"attestSecret" TEXT,
"jwksUri" TEXT,
"spiffeId" TEXT,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "IiosClientRegistry_pkey" PRIMARY KEY ("clientId")
);
CREATE TABLE "IiosAttestationNonce" (
"nonce" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "IiosAttestationNonce_pkey" PRIMARY KEY ("nonce")
);
CREATE INDEX "IiosAttestationNonce_expiresAt_idx" ON "IiosAttestationNonce"("expiresAt");
@@ -1321,3 +1321,31 @@ model IiosDlqItem {
@@unique([consumerName, sourceId])
@@index([scopeId, status])
}
// ─── Trust plane (July 12) — context attestation ──────────────────
// Registered clients/BFFs/adapters allowed to call IIOS and attest context on
// behalf of an app. A valid actor token is NOT enough; the caller must present a
// context attestation signed by one of these registered clients.
model IiosClientRegistry {
clientId String @id
clientType String // APPSHELL | SUPPORT_BFF | ADAPTER | CALENDAR | SERVICE
ownerService String?
allowedAppIds String[] // which app_ids this client may attest for
attestSecret String? // dev: shared HS256 signing key (AppShell's key stand-in)
jwksUri String? // prod: verify the attestation signature via JWKS
spiffeId String? // prod: workload identity for mTLS proof
status String @default("ACTIVE") // ACTIVE | DISABLED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Single-use nonce ledger: once a context attestation's nonce is seen it can
// never be replayed. Reserve-if-absent (unique PK) makes the check atomic.
model IiosAttestationNonce {
nonce String @id
clientId String
expiresAt DateTime
firstSeenAt DateTime @default(now())
@@index([expiresAt])
}