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:
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import type { ClientRegistryEntry, ClientRegistryPort, NonceStorePort } from './context-attestation';
|
||||
|
||||
/** Client registry backed by Postgres (IiosClientRegistry). */
|
||||
@Injectable()
|
||||
export class PrismaClientRegistry implements ClientRegistryPort {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async find(clientId: string): Promise<ClientRegistryEntry | null> {
|
||||
const r = await this.prisma.iiosClientRegistry.findUnique({ where: { clientId } });
|
||||
if (!r) return null;
|
||||
return {
|
||||
clientId: r.clientId,
|
||||
clientType: r.clientType,
|
||||
allowedAppIds: r.allowedAppIds,
|
||||
attestSecret: r.attestSecret ?? undefined,
|
||||
jwksUri: r.jwksUri ?? undefined,
|
||||
status: r.status === 'ACTIVE' ? 'ACTIVE' : 'DISABLED',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nonce ledger backed by Postgres (IiosAttestationNonce). Reserve-if-absent is atomic via the
|
||||
* unique primary key: a duplicate insert (P2002) means the nonce was already used → replay.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PrismaNonceStore implements NonceStorePort {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async reserve(input: { nonce: string; clientId: string; expiresAt: Date }): Promise<boolean> {
|
||||
try {
|
||||
await this.prisma.iiosAttestationNonce.create({ data: input });
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') return false; // already seen
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user