/** * A3 — REAL capability verifier (boot/verifier.ts) backed by relay-auth's async `verifyCapabilityToken`. * Mints tokens with the paired Ed25519 private key via P5's `issueCapabilityToken`, configures the * matching public key, and asserts: a good token verifies; wrong `aud`, past `exp`, and a token * signed by a NON-matching key all reject (deny-by-default, INV6). */ import { describe, test, expect, beforeAll } from 'vitest' import { issueCapabilityToken } from 'relay-auth' import type { AuthenticatedPrincipal } from 'relay-auth' import { encodeBase64UrlBytes } from 'relay-contracts' import { createCapabilityVerifier, configureCapabilityVerifyKey } from '../src/boot/verifier.js' import type { CapabilityVerifier } from '../src/api/authz.js' const ACCOUNT_A = '11111111-1111-4111-8111-111111111111' const AUD = 'term.example.com' const NOW = 1_000_000 // A base64url SHA-256 JWK thumbprint is exactly 43 chars of [A-Za-z0-9_-]; 32 bytes → 43 base64url chars. const CNF_JKT = encodeBase64UrlBytes(new Uint8Array(32).fill(7)) const principal: AuthenticatedPrincipal = { kind: 'human', accountId: ACCOUNT_A, principalId: 'cred-1', amr: ['passkey'], authAt: NOW - 10, stepUpAt: null, } type KeyPair = { publicKey: CryptoKey; privateKey: CryptoKey } async function genKeyPair(): Promise { // generateKey's named-algorithm overload is typed as CryptoKey; Ed25519 yields a pair at runtime. return (await globalThis.crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])) as unknown as KeyPair } function mint(signingKey: CryptoKey, now: number): Promise { return issueCapabilityToken( { principal, aud: AUD, host: 'host-1', rights: ['manage'], ttlSeconds: 60, cnfJkt: CNF_JKT }, signingKey, now, ) } describe('A3 real capability verifier (relay-auth verifyCapabilityToken)', () => { let signingKey: CryptoKey let verifier: CapabilityVerifier beforeAll(async () => { const pair = await genKeyPair() signingKey = pair.privateKey const rawPub = new Uint8Array(await globalThis.crypto.subtle.exportKey('raw', pair.publicKey)) await configureCapabilityVerifyKey(rawPub) verifier = createCapabilityVerifier() }) test('verifies a token signed by the configured key and returns the account principal', async () => { const raw = await mint(signingKey, NOW) const token = await verifier.verify(raw, AUD, NOW) expect(token.sub).toBe(ACCOUNT_A) expect(token.aud).toBe(AUD) expect(token.rights).toContain('manage') }) test('rejects a token minted for a different aud (Host-confusion guard)', async () => { const raw = await mint(signingKey, NOW) await expect(verifier.verify(raw, 'evil.example.com', NOW)).rejects.toThrow() }) test('rejects an expired token', async () => { const raw = await mint(signingKey, NOW) // exp = NOW + 60 await expect(verifier.verify(raw, AUD, NOW + 120)).rejects.toThrow() }) test('rejects a token signed by a NON-matching key (bad signature)', async () => { const other = await genKeyPair() const raw = await mint(other.privateKey, NOW) await expect(verifier.verify(raw, AUD, NOW)).rejects.toThrow() }) })