/** * A5 default mTLS transport (`defaultMtlsRequest`) — the ONE seam the other nativeRenew tests inject * past, so the real `node:https` transport had zero coverage. These tests mock `node:https` and drive * the actual transport to prove: * - the exact request options (rejectUnauthorized:true + client cert/key + pinned CA + method/body), * - the HIGH fix: a request timeout is armed and a stalled peer REJECTS (never hangs forever), * - the MEDIUM fix: an oversized response body is capped (destroyed + rejected, not buffered). */ import { EventEmitter } from 'node:events' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { requestMock } = vi.hoisted(() => ({ requestMock: vi.fn() })) vi.mock('node:https', () => ({ request: requestMock, default: { request: requestMock } })) import { generateP256Identity } from '../src/keys/identity.js' import { openKeystore } from '../src/keys/keystore.js' import { createMtlsFetch, RENEW_REQUEST_TIMEOUT_MS, MAX_RENEW_RESPONSE_BYTES, } from '../src/certs/nativeRenew.js' const farFuture = (): { validTo: Date } => ({ validTo: new Date(Date.now() + 86_400_000) }) /** Fake `http.ClientRequest`: records setTimeout/write/end and emits 'error' on destroy(err). */ class FakeClientRequest extends EventEmitter { readonly setTimeoutCalls: Array<{ ms: number; cb: () => void }> = [] readonly written: string[] = [] ended = false destroyedWith: Error | undefined setTimeout(ms: number, cb: () => void): this { this.setTimeoutCalls.push({ ms, cb }) return this } write(chunk: string): boolean { this.written.push(chunk) return true } end(): this { this.ended = true return this } destroy(err?: Error): this { this.destroyedWith = err if (err) this.emit('error', err) return this } } /** Fake `http.IncomingMessage`: an EventEmitter with a statusCode and a real destroy(). */ class FakeIncomingMessage extends EventEmitter { statusCode = 200 destroyed = false destroy(): this { this.destroyed = true return this } } type ReqCb = (res: FakeIncomingMessage) => void let dirs: string[] = [] function enrolledKs(): ReturnType { const dir = mkdtempSync(join(tmpdir(), 'wta-nrt-')) dirs.push(dir) const ks = openKeystore(dir) ks.saveIdentity(generateP256Identity()) ks.saveCert('LEAFCERT', 'CACHAIN') return ks } beforeEach(() => { requestMock.mockReset() }) afterEach(() => { for (const d of dirs) rmSync(d, { recursive: true, force: true }) dirs = [] }) describe('defaultMtlsRequest transport (A5 — real node:https)', () => { it('passes rejectUnauthorized:true + the client cert/key/CA + method/body, and arms the timeout', async () => { const ks = enrolledKs() let seenUrl = '' let seenOpts: Record = {} let seenReq: FakeClientRequest | undefined requestMock.mockImplementation((url: string, opts: Record, cb: ReqCb) => { seenUrl = url seenOpts = opts const req = new FakeClientRequest() seenReq = req queueMicrotask(() => { const res = new FakeIncomingMessage() res.statusCode = 200 cb(res) res.emit('data', Buffer.from('{"cert":"NEW",')) res.emit('data', Buffer.from('"caChain":"NEWCA"}')) res.emit('end') }) return req }) const f = createMtlsFetch(ks, { certParser: farFuture }) // NO request seam → real defaultMtlsRequest const res = await f('https://cp.example.com/renew', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"csr":"x"}', }) expect(res.status).toBe(200) expect(await res.json()).toEqual({ cert: 'NEW', caChain: 'NEWCA' }) expect(seenUrl).toBe('https://cp.example.com/renew') expect(seenOpts.rejectUnauthorized).toBe(true) // anti-MITM (INV14) expect(seenOpts.method).toBe('POST') expect(seenOpts.cert).toBe('LEAFCERT') // ca omitted ⇒ verify the LE-fronted control-plane against the system roots (not the private CA). expect(seenOpts.ca).toBeUndefined() expect(String(seenOpts.key)).toContain('PRIVATE KEY') // in-process PKCS#8 key // HIGH fix: a socket timeout is armed with the sane default so a stall can never hang forever. expect(seenReq!.setTimeoutCalls).toHaveLength(1) expect(seenReq!.setTimeoutCalls[0]!.ms).toBe(RENEW_REQUEST_TIMEOUT_MS) expect(seenReq!.written).toEqual(['{"csr":"x"}']) expect(seenReq!.ended).toBe(true) }) it('HIGH: a stalled peer (accepts TLS, never responds) REJECTS via the timeout, not hangs', async () => { const ks = enrolledKs() let seenReq: FakeClientRequest | undefined requestMock.mockImplementation(() => { const req = new FakeClientRequest() seenReq = req return req // never invokes the response callback — a stalled control-plane }) const f = createMtlsFetch(ks, { certParser: farFuture }) const p = f('https://cp.example.com/renew', { method: 'POST', body: '{}' }) // Fire the armed socket-timeout callback (what node does when the socket idles past the limit). expect(seenReq!.setTimeoutCalls).toHaveLength(1) expect(seenReq!.setTimeoutCalls[0]!.ms).toBe(RENEW_REQUEST_TIMEOUT_MS) seenReq!.setTimeoutCalls[0]!.cb() await expect(p).rejects.toThrow(/timed out/i) expect(seenReq!.destroyedWith).toBeInstanceOf(Error) // socket was torn down }) it('MEDIUM: an oversized response body is capped — res destroyed + promise rejected', async () => { const ks = enrolledKs() let seenRes: FakeIncomingMessage | undefined requestMock.mockImplementation((_url: string, _opts: unknown, cb: ReqCb) => { const req = new FakeClientRequest() queueMicrotask(() => { const res = new FakeIncomingMessage() res.statusCode = 200 seenRes = res cb(res) res.emit('data', Buffer.alloc(MAX_RENEW_RESPONSE_BYTES + 1)) // one byte over the cap // deliberately NO 'end' — a capped stream must reject on its own, not wait for end }) return req }) const f = createMtlsFetch(ks, { certParser: farFuture }) await expect(f('https://cp.example.com/renew', { method: 'POST' })).rejects.toThrow(/cap|exceed/i) expect(seenRes!.destroyed).toBe(true) }) })