From d97567b44a37128ed6aef1bc61553812f10c50f9 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 07:23:47 -0700 Subject: [PATCH] test(proxy): generate TLS test fixtures at runtime (#2389) Generate ephemeral TLS fixtures once per test file. Preserve transport assertions and add certificate validation coverage. --- src/test/tlsFixture.ts | 77 +++++++++++++++++++++++++++++++++ src/utils/proxy.connect.test.ts | 32 +++++++------- 2 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 src/test/tlsFixture.ts diff --git a/src/test/tlsFixture.ts b/src/test/tlsFixture.ts new file mode 100644 index 000000000..e7dc9da4a --- /dev/null +++ b/src/test/tlsFixture.ts @@ -0,0 +1,77 @@ +import { generateKeyPairSync, sign, X509Certificate } from 'node:crypto' + +// Minimal DER encoding for this test certificate, including long-form lengths. +function der(tag: number, ...parts: Buffer[]): Buffer { + const content = Buffer.concat(parts) + const length: number[] = [] + if (content.length < 128) { + length.push(content.length) + } else { + for (let remaining = content.length; remaining > 0; remaining >>>= 8) { + length.unshift(remaining & 0xff) + } + length.unshift(0x80 | length.length) + } + return Buffer.concat([Buffer.from([tag, ...length]), content]) +} + +const sequence = (...parts: Buffer[]) => der(0x30, ...parts) +const oid = (hex: string) => der(0x06, Buffer.from(hex, 'hex')) +const booleanTrue = der(0x01, Buffer.from([0xff])) + +function certificateTime(date: Date): Buffer { + const digits = date.toISOString().replace(/[-:T]/g, '').replace(/\.\d{3}Z$/, 'Z') + // X.509 uses UTCTime for 1950–2049 and GeneralizedTime otherwise. + const utc = date.getUTCFullYear() >= 1950 && date.getUTCFullYear() < 2050 + return der(utc ? 0x17 : 0x18, Buffer.from(utc ? digits.slice(2) : digits)) +} + +function extension(oidHex: string, value: Buffer, critical = false): Buffer { + return sequence( + oid(oidHex), + ...(critical ? [booleanTrue] : []), + der(0x04, value), + ) +} + +export function createTestTlsFixture() { + const { privateKey, publicKey } = generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + }) + // AlgorithmIdentifier for ecdsa-with-SHA256 (1.2.840.10045.4.3.2). + const algorithm = sequence(oid('2a8648ce3d040302')) + const name = sequence(der(0x31, sequence( + oid('550403'), // commonName (2.5.4.3) + der(0x0c, Buffer.from('localhost')), + ))) + const now = Date.now() + const day = 24 * 60 * 60 * 1000 + const tbsCertificate = sequence( + der(0xa0, der(0x02, Buffer.from([2]))), // explicit v3 version + der(0x02, Buffer.from([1])), // positive serial, unique for this fresh issuer + algorithm, + name, // issuer and subject are identical for a self-signed certificate + sequence(certificateTime(new Date(now - day)), certificateTime(new Date(now + day))), + name, + publicKey.export({ type: 'spki', format: 'der' }), + der(0xa3, sequence( + extension('551d13', sequence(booleanTrue), true), // basicConstraints: CA + // keyUsage: digitalSignature + keyCertSign; two unused bits. + extension('551d0f', der(0x03, Buffer.from([2, 0x84])), true), + extension('551d11', sequence( // subjectAltName + der(0x82, Buffer.from('localhost')), + der(0x87, Buffer.from([127, 0, 0, 1])), + )), + )), + ) + const certificate = sequence( + tbsCertificate, + algorithm, + der(0x03, Buffer.from([0]), sign('sha256', tbsCertificate, privateKey)), + ) + return { + key: privateKey.export({ type: 'pkcs8', format: 'pem' }), + cert: new X509Certificate(certificate).toString(), + publicKey, + } +} diff --git a/src/utils/proxy.connect.test.ts b/src/utils/proxy.connect.test.ts index 596acd37b..f45adc93e 100644 --- a/src/utils/proxy.connect.test.ts +++ b/src/utils/proxy.connect.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { X509Certificate } from 'node:crypto' import { once } from 'node:events' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { createServer as createHttpServer, type IncomingMessage } from 'node:http' @@ -13,6 +14,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import type { Duplex } from 'node:stream' import { snapshotEnv } from '../test/envSnapshot.js' +import { createTestTlsFixture } from '../test/tlsFixture.js' import { clearCACertsCache } from './caCerts.js' import { clearMTLSCache, getMTLSAgent, getWebSocketTLSOptions } from './mtls.js' @@ -46,23 +48,19 @@ const { createAxiosInstance, getWebSocketProxyAgent } = await import( `data:text/javascript;base64,${Buffer.from(await proxyBuild.outputs[0].text()).toString('base64')}` ) as typeof import('./proxy.js') -// Self-signed test certificate and key, valid only for the loopback target. -const cert = `-----BEGIN CERTIFICATE----- -MIIBmzCCAUGgAwIBAgIUAtOYg4xV7C9catkY/bwZc7XywMgwCgYIKoZIzj0EAwIw -FDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyNDA4NTUxNloYDzIxMjYwOTMw -MDg1NTE2WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjO -PQMBBwNCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O7bSu0aDJpwdMzOxgOT2jTBmK -2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKWo28wbTAdBgNVHQ4EFgQUu+xqnGfB/gu8 -DbNNHlVonV69Y64wHwYDVR0jBBgwFoAUu+xqnGfB/gu8DbNNHlVonV69Y64wDwYD -VR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI -zj0EAwIDSAAwRQIgPy5jbT1ECH7LFy5r5JrV7xfsOkhwtbuwqVeidDBynFACIQCp -sTyTz0llkGsTStOnpDBTzTUQzcDT8z6ZgqqxbY29nw== ------END CERTIFICATE-----` -const key = `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXc/iw7bBMrZkWHsr -tDvMDJ6qPLsspu6IH8lHZ3ccGvihRANCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O -7bSu0aDJpwdMzOxgOT2jTBmK2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKW ------END PRIVATE KEY-----` +const { key, cert, publicKey } = createTestTlsFixture() + +test('runtime TLS fixture parses, verifies and covers the loopback target', () => { + const certificate = new X509Certificate(cert) + expect(certificate.verify(publicKey)).toBe(true) + expect(certificate.ca).toBe(true) + expect(certificate.subject).toBe('CN=localhost') + expect(certificate.subjectAltName).toBe('DNS:localhost, IP Address:127.0.0.1') + expect(certificate.checkHost('localhost')).toBe('localhost') + expect(certificate.checkIP('127.0.0.1')).toBe('127.0.0.1') + expect(Date.parse(certificate.validFrom)).toBeLessThan(Date.now()) + expect(Date.parse(certificate.validTo)).toBeGreaterThan(Date.now()) +}) describe('HTTPS_PROXY CONNECT transport', () => { let restoreEnv: () => void