From dd6099105789eb2c8a9e2b7a0fb41d614d4a7eae Mon Sep 17 00:00:00 2001 From: John Skilbeck Date: Sat, 15 Mar 2025 14:46:54 -0700 Subject: [PATCH 1/4] fix(https): add option to disable TLS 1.0 cxns resolves https://github.com/httptoolkit/mockttp/issues/187 --- src/mockttp.ts | 13 ++++++ src/server/http-combo-server.ts | 1 + test/integration/https.spec.ts | 71 ++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/mockttp.ts b/src/mockttp.ts index 9e32e7b78..905f4448d 100644 --- a/src/mockttp.ts +++ b/src/mockttp.ts @@ -723,6 +723,19 @@ export type MockttpHttpsOptions = CAOptions & { * here for additional configuration of this behaviour. */ tlsInterceptOnly?: Array<{ hostname: string }>; + + /** + * Set the minimum TLS version to be used for incoming TLS connections. + * If not set, all TLS versions are supported. + * + * If set, and an incoming connection uses a lower version, the connection + * will be rejected. + * + * Currently, only a minVersion of 'TLSv1.2' is supported. + * In the future, other versions may be supported. + * The full list of versions can be found: https://nodejs.org/api/tls.html#tlssocketgetprotocol + */ + tlsServerOptions?: { minVersion: 'TLSv1.2' }; }; export interface MockttpOptions { diff --git a/src/server/http-combo-server.ts b/src/server/http-combo-server.ts index 8e54a318e..a7cd3eb2a 100644 --- a/src/server/http-combo-server.ts +++ b/src/server/http-combo-server.ts @@ -184,6 +184,7 @@ export async function createComboServer( cert: defaultCert.cert, ca: [defaultCert.ca], ...ALPNOption, + ...(options.https?.tlsServerOptions || {}), SNICallback: (domain: string, cb: Function) => { if (options.debug) console.log(`Generating certificate for ${domain}`); diff --git a/test/integration/https.spec.ts b/test/integration/https.spec.ts index 502aaa390..14dcc6570 100644 --- a/test/integration/https.spec.ts +++ b/test/integration/https.spec.ts @@ -419,4 +419,73 @@ describe("When configured for HTTPS", () => { }); }); }); -}); \ No newline at end of file + + describe.only("with TLS version restrictions", () => { + const server = getLocal({ + https: { + keyPath: './test/fixtures/test-ca.key', + certPath: './test/fixtures/test-ca.pem', + tlsServerOptions: { + minVersion: 'TLSv1.2' + } + } + }); + + beforeEach(async () => { + await server.start(); + await server.forAnyRequest().thenReply(200, "Mock response"); + }); + + afterEach(async () => { + await server.stop(); + }); + + it("should accept TLS 1.2 connections", async () => { + const tlsSocket = await openRawTlsSocket(server, { + rejectUnauthorized: false, + minVersion: 'TLSv1.2', + maxVersion: 'TLSv1.2' + }); + + expect(tlsSocket.getProtocol()).to.equal('TLSv1.2'); + tlsSocket.destroy(); + }); + + it("should reject TLS 1.0 connections", async () => { + try { + await openRawTlsSocket(server, { + rejectUnauthorized: false, + minVersion: 'TLSv1', + maxVersion: 'TLSv1' + }); + throw new Error('Expected connection to fail'); + } catch (e: any) { + expect(e.code).to.include('ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + } + }); + + it("should reject TLS 1.1 connections", async () => { + try { + await openRawTlsSocket(server, { + rejectUnauthorized: false, + minVersion: 'TLSv1.1', + maxVersion: 'TLSv1.1' + }); + throw new Error('Expected connection to fail'); + } catch (e: any) { + expect(e.code).to.include('ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + } + }); + + it("should accept TLS 1.3 connections when TLS 1.2 is minimum", async () => { + const tlsSocket = await openRawTlsSocket(server, { + rejectUnauthorized: false, + minVersion: 'TLSv1.3', + maxVersion: 'TLSv1.3' + }); + + expect(tlsSocket.getProtocol()).to.equal('TLSv1.3'); + tlsSocket.destroy(); + }); + }); +}); From 98bde8d52b754a54bfa7d1a561ebb6618dd27434 Mon Sep 17 00:00:00 2001 From: John Skilbeck Date: Mon, 17 Mar 2025 20:24:06 -0700 Subject: [PATCH 2/4] address review feedback --- test/integration/https.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/https.spec.ts b/test/integration/https.spec.ts index 14dcc6570..c0ac71744 100644 --- a/test/integration/https.spec.ts +++ b/test/integration/https.spec.ts @@ -420,15 +420,15 @@ describe("When configured for HTTPS", () => { }); }); - describe.only("with TLS version restrictions", () => { + describe("with TLS version restrictions", () => { const server = getLocal({ - https: { - keyPath: './test/fixtures/test-ca.key', - certPath: './test/fixtures/test-ca.pem', - tlsServerOptions: { + https: { + keyPath: './test/fixtures/test-ca.key', + certPath: './test/fixtures/test-ca.pem', + tlsServerOptions: { minVersion: 'TLSv1.2' - } } + } }); beforeEach(async () => { From aa78e8ec07894bc9559a007070cc13c14d8d34a1 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 18 Mar 2025 12:43:36 +0100 Subject: [PATCH 3/4] Tweak tlsServerOptions type & docs --- src/mockttp.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/mockttp.ts b/src/mockttp.ts index 905f4448d..8e25d872a 100644 --- a/src/mockttp.ts +++ b/src/mockttp.ts @@ -725,17 +725,16 @@ export type MockttpHttpsOptions = CAOptions & { tlsInterceptOnly?: Array<{ hostname: string }>; /** - * Set the minimum TLS version to be used for incoming TLS connections. - * If not set, all TLS versions are supported. + * Set the TLS server options, used for incoming TLS connections. * - * If set, and an incoming connection uses a lower version, the connection - * will be rejected. - * - * Currently, only a minVersion of 'TLSv1.2' is supported. - * In the future, other versions may be supported. - * The full list of versions can be found: https://nodejs.org/api/tls.html#tlssocketgetprotocol + * The only officially supported option for now is the minimum TLS version, which can + * be used to relax/tighten TLS requirements on clients. If not set, this defaults + * to your Node version's default TLS configuration. The full list of versions can be + * found at https://nodejs.org/api/tls.html#tlssocketgetprotocol. */ - tlsServerOptions?: { minVersion: 'TLSv1.2' }; + tlsServerOptions?: { + minVersion?: 'TLSv1.3' | 'TLSv1.2' | 'TLSv1.1' | 'TLSv1'; + }; }; export interface MockttpOptions { From 437babbb6695925469bd4982d16f129a8ab985f1 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 18 Mar 2025 13:19:05 +0100 Subject: [PATCH 4/4] Fix TLS options tests for old Node & browsers --- test/integration/https.spec.ts | 122 ++++++++++++++++++--------------- test/test-utils.ts | 1 + 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/test/integration/https.spec.ts b/test/integration/https.spec.ts index c0ac71744..b4b38fd17 100644 --- a/test/integration/https.spec.ts +++ b/test/integration/https.spec.ts @@ -2,6 +2,7 @@ import * as http from 'http'; import * as tls from 'tls'; import * as https from 'https'; import * as fs from 'fs/promises'; +import * as semver from 'semver'; import { getLocal } from "../.."; import { @@ -11,7 +12,8 @@ import { delay, openRawSocket, openRawTlsSocket, - http2ProxyRequest + http2ProxyRequest, + DETAILED_TLS_ERROR_CODES } from "../test-utils"; import { streamToBuffer } from '../../src/util/buffer-utils'; @@ -418,74 +420,82 @@ describe("When configured for HTTPS", () => { ); }); }); - }); - describe("with TLS version restrictions", () => { - const server = getLocal({ - https: { - keyPath: './test/fixtures/test-ca.key', - certPath: './test/fixtures/test-ca.pem', - tlsServerOptions: { - minVersion: 'TLSv1.2' + describe("with TLS version restrictions", () => { + const server = getLocal({ + https: { + keyPath: './test/fixtures/test-ca.key', + certPath: './test/fixtures/test-ca.pem', + tlsServerOptions: { + minVersion: 'TLSv1.2' + } as any } - } - }); - - beforeEach(async () => { - await server.start(); - await server.forAnyRequest().thenReply(200, "Mock response"); - }); - - afterEach(async () => { - await server.stop(); - }); + }); - it("should accept TLS 1.2 connections", async () => { - const tlsSocket = await openRawTlsSocket(server, { - rejectUnauthorized: false, - minVersion: 'TLSv1.2', - maxVersion: 'TLSv1.2' + beforeEach(async () => { + await server.start(); + await server.forAnyRequest().thenReply(200, "Mock response"); }); - expect(tlsSocket.getProtocol()).to.equal('TLSv1.2'); - tlsSocket.destroy(); - }); + afterEach(async () => { + await server.stop(); + }); - it("should reject TLS 1.0 connections", async () => { - try { - await openRawTlsSocket(server, { + it("should accept TLS 1.2 connections", async () => { + const tlsSocket = await openRawTlsSocket(server, { rejectUnauthorized: false, - minVersion: 'TLSv1', - maxVersion: 'TLSv1' + minVersion: 'TLSv1.2', + maxVersion: 'TLSv1.2' }); - throw new Error('Expected connection to fail'); - } catch (e: any) { - expect(e.code).to.include('ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); - } - }); - it("should reject TLS 1.1 connections", async () => { - try { - await openRawTlsSocket(server, { + expect(tlsSocket.getProtocol()).to.equal('TLSv1.2'); + tlsSocket.destroy(); + }); + + it("should reject TLS 1.0 connections", async () => { + try { + await openRawTlsSocket(server, { + rejectUnauthorized: false, + minVersion: 'TLSv1', + maxVersion: 'TLSv1' + }); + throw new Error('Expected connection to fail'); + } catch (e: any) { + expect(e.code).to.equal( + semver.satisfies(process.version, DETAILED_TLS_ERROR_CODES) + ? 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION' + : 'ECONNRESET' + ); + } + }); + + it("should reject TLS 1.1 connections", async () => { + try { + await openRawTlsSocket(server, { + rejectUnauthorized: false, + minVersion: 'TLSv1.1', + maxVersion: 'TLSv1.1' + }); + throw new Error('Expected connection to fail'); + } catch (e: any) { + expect(e.code).to.equal( + semver.satisfies(process.version, DETAILED_TLS_ERROR_CODES) + ? 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION' + : 'ECONNRESET' + ); + } + }); + + it("should accept TLS 1.3 connections when TLS 1.2 is minimum", async () => { + const tlsSocket = await openRawTlsSocket(server, { rejectUnauthorized: false, - minVersion: 'TLSv1.1', - maxVersion: 'TLSv1.1' + minVersion: 'TLSv1.3', + maxVersion: 'TLSv1.3' }); - throw new Error('Expected connection to fail'); - } catch (e: any) { - expect(e.code).to.include('ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); - } - }); - it("should accept TLS 1.3 connections when TLS 1.2 is minimum", async () => { - const tlsSocket = await openRawTlsSocket(server, { - rejectUnauthorized: false, - minVersion: 'TLSv1.3', - maxVersion: 'TLSv1.3' + expect(tlsSocket.getProtocol()).to.equal('TLSv1.3'); + tlsSocket.destroy(); }); - - expect(tlsSocket.getProtocol()).to.equal('TLSv1.3'); - tlsSocket.destroy(); }); }); }); diff --git a/test/test-utils.ts b/test/test-utils.ts index d4b3fd0d7..34025b254 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -257,6 +257,7 @@ export async function startDnsServer(callback: (question: dns2.DnsQuestion) => s export const H2_TLS_ON_TLS_SUPPORTED = ">=12.17"; export const HTTP_ABORTSIGNAL_SUPPORTED = ">=14.17"; +export const DETAILED_TLS_ERROR_CODES = ">=18"; export const NATIVE_FETCH_SUPPORTED = ">=18"; export const SOCKET_RESET_SUPPORTED = "^16.17 || >=18.3"; export const BROKEN_H1_OVER_H2_TUNNELLING = "^18.8";