import { type CanActivate, type ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; import type { Request } from 'express'; import { SessionVerifier } from './session.verifier'; import { ContextAttestationVerifier } from './context-attestation'; /** * The July 12 three-proof gate on the request path. Proof #1 (a valid actor token) is done by * the controllers; proof #2 (workload/mTLS) is the mesh's job in prod. This guard adds proof #3: * a signed CONTEXT ATTESTATION from an authorized parent (AppShell etc.). * * Behaviour (safe rollout): * • attestation header present → verify it; the attestation's app_id must match the actor * token's app_id. Any failure → 403. * • attestation header absent → allowed ONLY when not required (dev / a zero-trust standalone * caller that IIOS checks fully itself). With `IIOS_REQUIRE_ATTESTATION=1`, absence is a 403. * * The requirement is read per-request so it can be toggled without restarts and stays OFF by * default — existing callers that don't send an attestation keep working until the rollout flips. */ @Injectable() export class ContextAttestationGuard implements CanActivate { constructor( private readonly session: SessionVerifier, private readonly attest: ContextAttestationVerifier, ) {} async canActivate(ctx: ExecutionContext): Promise { const required = process.env.IIOS_REQUIRE_ATTESTATION === '1'; const req = ctx.switchToHttp().getRequest(); const raw = req.headers['x-context-attestation']; const attestation = Array.isArray(raw) ? raw[0] : raw; if (!attestation) { if (required) throw new ForbiddenException('context attestation required'); return true; // dev / zero-trust: the actor token is still enforced downstream } // Bind the attestation to the token's app_id, so a stolen attestation for another app fails. const auth = req.headers['authorization'] ?? ''; const token = String(auth).replace(/^Bearer\s+/i, ''); let appId: string; try { appId = this.session.verify(token).appId ?? ''; } catch { throw new ForbiddenException('invalid actor token'); } const result = await this.attest.verify(attestation, appId); if (!result.ok) throw new ForbiddenException(`context attestation rejected: ${result.reason}`); return true; } }