/** * Native-tunnel wiring E2E (fastify inject against the WIRED control-plane app from `main.ts`). * * Proves the full DEVICE path through the real composition root: * mint a device:enroll token (auth/session, verified by the REAL §4.3 verifier the boot wires) → * POST /device/enroll {csr:, subdomain:} → 201 with a cert that RE-PARSES, * CHAINS to the wired dev device-CA, and carries dNSName .terminal.yaojia.wang → present that * cert to POST /device/:id/renew → 201 fresh cert for the SAME subdomain. * Plus the A4 ownership gate end-to-end: enroll for an UNOWNED (and a foreign-owned) subdomain → 403. * * Also proves the HOST /renew path through the wired app: the initial frp-client leaf is minted via * the WIRED host signer (native host HTTP /enroll is a documented follow-up — the existing pairing * `/enroll` relay route is left untouched), then presented to /renew → 201 for the SAME subdomain. * * The verifier is the SAME seam the admin API uses (`boot/verifier.ts`), keyed off the boot env's * capability pubkey — so the enroll token is minted with the matching private key and travels the * exact verify path production uses. */ import 'reflect-metadata' import { describe, test, expect, beforeEach } from 'vitest' import * as x509 from '@peculiar/x509' import { webcrypto } from 'node:crypto' import type { FastifyInstance } from 'fastify' import { generateEd25519KeyPair, exportEd25519PublicRaw } from 'relay-auth/src/crypto/ed25519.js' import { createMemoryStores } from '../src/store/memory.js' import { createHostRegistry } from '../src/registry/hosts.js' import { createPairingIssuer } from '../src/pairing/issue.js' import { fingerprint } from '../src/ca/fingerprint.js' import { generateEd25519 } from '../src/util/crypto.js' import { buildCsr } from '../src/ca/csr.js' import { buildCsrEc } from '../src/ca/csr-ec.js' import { buildControlPlane, type BuiltControlPlane } from '../src/main.js' import { createCapabilityVerifier } from '../src/boot/verifier.js' import { mintDeviceEnrollToken } from '../src/auth/session.js' import { loadEnv, type ControlPlaneEnv } from '../src/env.js' import { bytesToBase64 } from '../src/util/bytes.js' import type { Stores } from '../src/store/ports.js' x509.cryptoProvider.set(webcrypto) const ACCOUNT_A = '11111111-1111-4111-8111-111111111111' const ACCOUNT_B = '22222222-2222-4222-8222-222222222222' const DNS_ZONE = 'terminal.yaojia.wang' let signingKey: CryptoKey let env: ControlPlaneEnv beforeEach(async () => { // Ed25519 capability keypair: the raw public key becomes the boot env's verify key, so the // device:enroll token minted with the private key verifies through the wired real §4.3 verifier. const pair = await generateEd25519KeyPair() signingKey = pair.privateKey const rawPub = await exportEd25519PublicRaw(pair.publicKey) env = loadEnv({ PG_URL: 'postgres://u:p@localhost:5432/cp', REDIS_URL: 'redis://localhost:6379', CAPABILITY_SIGN_PUBKEY_B64: bytesToBase64(rawPub), CA_INTERMEDIATE_KMS_KEY_REF: 'kms://key/intermediate', CA_INTERMEDIATE_CERT_PATH: '/etc/cp/int.pem', NODE_MTLS_TRUST_BUNDLE_PATH: '/etc/cp/node-ca.pem', RELAY_TRUST_DOMAIN: DNS_ZONE, BASE_DOMAIN: 'term.example.com', }) }) async function buildApp(): Promise<{ app: FastifyInstance; stores: Stores; built: BuiltControlPlane }> { const stores = createMemoryStores() const built = await buildControlPlane(env, { stores, verifier: createCapabilityVerifier() }) await built.app.ready() return { app: built.app, stores, built } } /** Onboard a host so `accountId` OWNS `subdomain` in the ownership source of truth (registry). */ async function ownSubdomain(stores: Stores, accountId: string, subdomain: string): Promise { const hosts = createHostRegistry({ hosts: stores.hosts }) const { publicKeyRaw } = generateEd25519() await hosts.bindHost({ accountId, subdomain, agentPubkey: publicKeyRaw, enrollFpr: fingerprint(publicKeyRaw) }) } /** Mint a single-use pairing code bound to `accountId` (the enroll gate reads accountId from the row). */ async function mintPairingCode(stores: Stores, accountId: string): Promise { const issuer = createPairingIssuer({ pairing: stores.pairing, pairingTtlSec: 3600 }) const { code } = await issuer.issuePairingCode(accountId) return code } /** A fresh P-256 host identity: its real PKCS#10 CSR + the SPKI DER the agent presents as agentPubkey. */ async function ecHostIdentity(subject = 'CN=web-terminal-host'): Promise<{ csr: Uint8Array; spki: Uint8Array }> { const { der: csr, keys } = await buildCsrEc(subject) const spki = new Uint8Array(await webcrypto.subtle.exportKey('spki', keys.publicKey)) return { csr, spki } } /** Bind a host whose frp-client identity is a P-256 key (so the wired host signer can issue for it). */ async function bindP256Host( stores: Stores, accountId: string, subdomain: string, ): Promise<{ hostId: string; csr: Uint8Array; spki: Uint8Array }> { const hosts = createHostRegistry({ hosts: stores.hosts }) const { der: csr, keys } = await buildCsrEc(`CN=${subdomain}`) const spki = new Uint8Array(await webcrypto.subtle.exportKey('spki', keys.publicKey)) const host = await hosts.bindHost({ accountId, subdomain, agentPubkey: spki, enrollFpr: fingerprint(spki) }) return { hostId: host.hostId, csr, spki } } function b64(bytes: Uint8Array): string { return Buffer.from(bytes).toString('base64') } function sanNames(der: Uint8Array): ReturnType { const leaf = new x509.X509Certificate(der) return leaf.getExtension(x509.SubjectAlternativeNameExtension)!.names.toJSON() } describe('native-tunnel wiring — DEVICE e2e (enroll → renew)', () => { test('enroll an owned subdomain → 201 real leaf chaining to the dev device-CA → renew → 201 fresh cert, same subdomain', async () => { const { app, stores, built } = await buildApp() await ownSubdomain(stores, ACCOUNT_A, 'alice') const token = await mintDeviceEnrollToken(ACCOUNT_A, { signingKey }) const { der: csr } = await buildCsrEc('CN=web-terminal-device') // ── ENROLL ─────────────────────────────────────────────────────────────────────────────────── const enroll = await app.inject({ method: 'POST', url: '/device/enroll', headers: { authorization: `Bearer ${token}` }, payload: { csr: b64(csr), keyAlg: 'ec-p256', subdomain: 'alice', deviceName: 'iphone' }, }) expect(enroll.statusCode).toBe(201) const enrolled = JSON.parse(enroll.body) expect(typeof enrolled.deviceId).toBe('string') expect(enrolled.deviceId.length).toBeGreaterThan(0) // the returned cert re-parses as a real X.509 leaf … const leafDer = new Uint8Array(Buffer.from(enrolled.cert, 'base64')) const leaf = new x509.X509Certificate(leafDer) // … chains to the CA the app returned … const caDer = new Uint8Array(Buffer.from(enrolled.caChain[0], 'base64')) const caCert = new x509.X509Certificate(caDer) expect(await leaf.verify({ publicKey: caCert.publicKey, signatureOnly: true })).toBe(true) // … which IS the wired dev device-CA anchor … expect(b64(caDer)).toBe(b64(built.nativeTunnel.nativeCas.deviceCa.caCertDer)) // … and carries the tenant-isolation dNSName SAN. expect(sanNames(leafDer)).toContainEqual({ type: 'dns', value: `alice.${DNS_ZONE}` }) // ── RENEW (present the enroll-issued cert as the current mTLS client cert) ────────────────────── const renew = await app.inject({ method: 'POST', url: `/device/${enrolled.deviceId}/renew`, headers: { 'x-client-cert': b64(leafDer) }, payload: { csr: b64(csr) }, // same-key CSR }) expect(renew.statusCode).toBe(201) const renewed = JSON.parse(renew.body) const renewedDer = new Uint8Array(Buffer.from(renewed.cert, 'base64')) expect(() => new x509.X509Certificate(renewedDer)).not.toThrow() // same subdomain — the renew stamps the registry scope, never client input. expect(sanNames(renewedDer)).toContainEqual({ type: 'dns', value: `alice.${DNS_ZONE}` }) // and the fresh cert still chains to the device-CA anchor. expect(await new x509.X509Certificate(renewedDer).verify({ publicKey: caCert.publicKey, signatureOnly: true })).toBe(true) }) test('enroll for an UNOWNED subdomain → 403, no cert issued (A4 ownership gate, end-to-end)', async () => { const { app, stores } = await buildApp() await ownSubdomain(stores, ACCOUNT_A, 'alice') const token = await mintDeviceEnrollToken(ACCOUNT_A, { signingKey }) const { der: csr } = await buildCsrEc('CN=web-terminal-device') const res = await app.inject({ method: 'POST', url: '/device/enroll', headers: { authorization: `Bearer ${token}` }, payload: { csr: b64(csr), keyAlg: 'ec-p256', subdomain: 'charlie', deviceName: 'x' }, }) expect(res.statusCode).toBe(403) expect(JSON.parse(res.body).cert).toBeUndefined() }) test('account B enrolling account A\'s subdomain → 403 (cross-tenant, no cert)', async () => { const { app, stores } = await buildApp() await ownSubdomain(stores, ACCOUNT_A, 'alice') const tokenB = await mintDeviceEnrollToken(ACCOUNT_B, { signingKey }) const { der: csr } = await buildCsrEc('CN=web-terminal-device') const res = await app.inject({ method: 'POST', url: '/device/enroll', headers: { authorization: `Bearer ${tokenB}` }, payload: { csr: b64(csr), keyAlg: 'ec-p256', subdomain: 'alice', deviceName: 'x' }, }) expect(res.statusCode).toBe(403) expect(JSON.parse(res.body).cert).toBeUndefined() }) test('missing enroll token → 401 (deny-by-default through the wired verifier seam)', async () => { const { app, stores } = await buildApp() await ownSubdomain(stores, ACCOUNT_A, 'alice') const { der: csr } = await buildCsrEc('CN=web-terminal-device') const res = await app.inject({ method: 'POST', url: '/device/enroll', payload: { csr: b64(csr), keyAlg: 'ec-p256', subdomain: 'alice', deviceName: 'x' }, }) expect(res.statusCode).toBe(401) }) }) describe('native-tunnel wiring — HOST /renew e2e', () => { test('initial frp-client leaf (wired host signer) → /renew → 201 fresh cert, SAME subdomain, chains to frp-client-CA', async () => { const { app, stores, built } = await buildApp() const { hostId, csr, spki } = await bindP256Host(stores, ACCOUNT_A, 'alice') // Initial leaf, minted by the WIRED host signer (chains to the wired frp-client-CA anchor). const { cert: currentCert } = await built.nativeTunnel.hostSigner.signHostLeaf(hostId, spki, csr) const res = await app.inject({ method: 'POST', url: '/renew', headers: { 'x-client-cert': b64(currentCert) }, payload: { csr: b64(csr) }, }) expect(res.statusCode).toBe(201) const renewedDer = new Uint8Array(Buffer.from(JSON.parse(res.body).cert, 'base64')) expect(sanNames(renewedDer)).toContainEqual({ type: 'dns', value: `alice.${DNS_ZONE}` }) const caCert = new x509.X509Certificate(built.nativeTunnel.nativeCas.frpClientCa.caCertDer) expect(await new x509.X509Certificate(renewedDer).verify({ publicKey: caCert.publicKey, signatureOnly: true })).toBe(true) }) test('a current cert from an UNTRUSTED (different-instance) frp-client-CA is rejected by the wired anchor set → 401', async () => { const { app } = await buildApp() // A second, independent wired control-plane = a DIFFERENT (untrusted) frp-client-CA. Bind the same // host+identity there and mint a leaf under ITS CA, then present it to the REAL app's /renew. const rogueStores = createMemoryStores() const rogue = await buildControlPlane(env, { stores: rogueStores, verifier: createCapabilityVerifier() }) const rogueBound = await bindP256Host(rogueStores, ACCOUNT_A, 'alice') const { cert: rogueCert } = await rogue.nativeTunnel.hostSigner.signHostLeaf( rogueBound.hostId, rogueBound.spki, rogueBound.csr, ) const res = await app.inject({ method: 'POST', url: '/renew', headers: { 'x-client-cert': b64(rogueCert) }, payload: { csr: b64(rogueBound.csr) }, }) expect(res.statusCode).toBe(401) }) }) describe('native-tunnel wiring — HOST /enroll HTTP arm (P-256 frp-client leaf)', () => { test('full host onboard: mint code → POST /enroll (EC CSR) → 201 frp-client leaf (chains to frp-client-CA + dNSName SAN, hostContentSecret null) → /renew → 201 SAME subdomain', async () => { const { app, stores, built } = await buildApp() const code = await mintPairingCode(stores, ACCOUNT_A) const { csr, spki } = await ecHostIdentity() // ── ENROLL (native P-256 arm — routed by CSR key algorithm) ──────────────────────────────────── const enroll = await app.inject({ method: 'POST', url: '/enroll', payload: { code, machineId: 'MB-A1B2C3', agentPubkey: b64(spki), csr: b64(csr) }, }) expect(enroll.statusCode).toBe(201) const body = JSON.parse(enroll.body) expect(typeof body.hostId).toBe('string') expect(body.hostId.length).toBeGreaterThan(0) expect(typeof body.subdomain).toBe('string') expect(body.subdomain.length).toBeGreaterThan(0) // native tunnel has no E2E-relay content secret (L-host-hcs). expect(body.hostContentSecret).toBeNull() // the returned cert re-parses as a real X.509 leaf … const leafDer = new Uint8Array(Buffer.from(body.cert, 'base64')) const leaf = new x509.X509Certificate(leafDer) // … chains to the wired frp-client-CA anchor … const caCert = new x509.X509Certificate(built.nativeTunnel.nativeCas.frpClientCa.caCertDer) expect(await leaf.verify({ publicKey: caCert.publicKey, signatureOnly: true })).toBe(true) const caDer = new Uint8Array(Buffer.from(body.caChain[0], 'base64')) expect(b64(caDer)).toBe(b64(built.nativeTunnel.nativeCas.frpClientCa.caCertDer)) // … and carries the AUTHORITATIVE (server-assigned) dNSName SAN .. expect(sanNames(leafDer)).toContainEqual({ type: 'dns', value: `${body.subdomain}.${DNS_ZONE}` }) // the host binding persisted the SAME authoritative subdomain (not client input). const host = await stores.hosts.get(body.hostId) expect(host?.subdomain).toBe(body.subdomain) expect(host?.accountId).toBe(ACCOUNT_A) // ── RENEW (present the enroll-issued cert as the current mTLS client cert) ────────────────────── const renew = await app.inject({ method: 'POST', url: '/renew', headers: { 'x-client-cert': b64(leafDer) }, payload: { csr: b64(csr) }, // same-key CSR }) expect(renew.statusCode).toBe(201) const renewedDer = new Uint8Array(Buffer.from(JSON.parse(renew.body).cert, 'base64')) // the renew stamps the registry scope — SAME subdomain, never client input. expect(sanNames(renewedDer)).toContainEqual({ type: 'dns', value: `${body.subdomain}.${DNS_ZONE}` }) expect(await new x509.X509Certificate(renewedDer).verify({ publicKey: caCert.publicKey, signatureOnly: true })).toBe(true) }) test('anti-smuggling: a client body requesting a DIFFERENT subdomain does NOT change the issued SAN', async () => { const { app, stores } = await buildApp() const code = await mintPairingCode(stores, ACCOUNT_A) const { csr, spki } = await ecHostIdentity() const enroll = await app.inject({ method: 'POST', url: '/enroll', // the attacker tries to steer the SAN to a subdomain it was never assigned: payload: { code, machineId: 'x', agentPubkey: b64(spki), csr: b64(csr), subdomain: 'victim' }, }) expect(enroll.statusCode).toBe(201) const body = JSON.parse(enroll.body) // the SAN is the SERVER-assigned subdomain, never the client-supplied 'victim'. expect(body.subdomain).not.toBe('victim') const leafDer = new Uint8Array(Buffer.from(body.cert, 'base64')) expect(sanNames(leafDer)).toContainEqual({ type: 'dns', value: `${body.subdomain}.${DNS_ZONE}` }) expect(sanNames(leafDer)).not.toContainEqual({ type: 'dns', value: `victim.${DNS_ZONE}` }) // and the host registry bound the server-assigned subdomain, not 'victim'. const host = await stores.hosts.get(body.hostId) expect(host?.subdomain).toBe(body.subdomain) expect(host?.subdomain).not.toBe('victim') }) test('single-use: replaying an already-redeemed pairing code → NOT 201 (double-spend guard)', async () => { const { app, stores } = await buildApp() const code = await mintPairingCode(stores, ACCOUNT_A) const first = await ecHostIdentity('CN=host-1') const ok = await app.inject({ method: 'POST', url: '/enroll', payload: { code, machineId: 'a', agentPubkey: b64(first.spki), csr: b64(first.csr) }, }) expect(ok.statusCode).toBe(201) const second = await ecHostIdentity('CN=host-2') const replay = await app.inject({ method: 'POST', url: '/enroll', payload: { code, machineId: 'a', agentPubkey: b64(second.spki), csr: b64(second.csr) }, }) expect(replay.statusCode).not.toBe(201) }) test('no-substitution: agentPubkey that does not match the CSR key → NOT 201', async () => { const { app, stores } = await buildApp() const code = await mintPairingCode(stores, ACCOUNT_A) const { csr } = await ecHostIdentity('CN=host-real') const other = await ecHostIdentity('CN=host-other') const res = await app.inject({ method: 'POST', url: '/enroll', // present a DIFFERENT key than the CSR embeds — the gate's no-substitution check must reject. payload: { code, machineId: 'a', agentPubkey: b64(other.spki), csr: b64(csr) }, }) expect(res.statusCode).not.toBe(201) }) test('non-regression: an Ed25519 relay /enroll still issues the PEM relay leaf + wrapped secret (native arm does not hijack it)', async () => { const { app, stores } = await buildApp() const code = await mintPairingCode(stores, ACCOUNT_A) const { publicKeyRaw, privateKey } = generateEd25519() const res = await app.inject({ method: 'POST', url: '/enroll', payload: { code, agentPubkey: bytesToBase64(publicKeyRaw), csr: bytesToBase64(buildCsr(privateKey, publicKeyRaw)) }, }) expect(res.statusCode).toBe(201) const body = JSON.parse(res.body) expect(body.cert).toContain('BEGIN CERTIFICATE') // PEM relay leaf, unchanged expect(typeof body.hostContentSecret).toBe('string') // base64url wrapped secret, NOT null }) })