Skip to content

Commit b22cb6c

Browse files
add mTLS mixed mode support
1 parent 03b8c1c commit b22cb6c

File tree

17 files changed

+455
-184
lines changed

17 files changed

+455
-184
lines changed

.changeset/famous-candies-start.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
add workerName option to startMixedModeSession API

.changeset/lemon-laws-mate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"miniflare": patch
3+
"wrangler": patch
4+
---
5+
6+
add mixed-mode support for mtls bindings

packages/miniflare/src/plugins/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { EMAIL_PLUGIN, EMAIL_PLUGIN_NAME } from "./email";
2424
import { HYPERDRIVE_PLUGIN, HYPERDRIVE_PLUGIN_NAME } from "./hyperdrive";
2525
import { IMAGES_PLUGIN, IMAGES_PLUGIN_NAME } from "./images";
2626
import { KV_PLUGIN, KV_PLUGIN_NAME } from "./kv";
27+
import { MTLS_PLUGIN, MTLS_PLUGIN_NAME } from "./mtls";
2728
import { PIPELINE_PLUGIN, PIPELINES_PLUGIN_NAME } from "./pipelines";
2829
import { QUEUES_PLUGIN, QUEUES_PLUGIN_NAME } from "./queues";
2930
import { R2_PLUGIN, R2_PLUGIN_NAME } from "./r2";
@@ -54,6 +55,7 @@ export const PLUGINS = {
5455
[IMAGES_PLUGIN_NAME]: IMAGES_PLUGIN,
5556
[VECTORIZE_PLUGIN_NAME]: VECTORIZE_PLUGIN,
5657
[CONTAINER_PLUGIN_NAME]: CONTAINER_PLUGIN,
58+
[MTLS_PLUGIN_NAME]: MTLS_PLUGIN,
5759
};
5860
export type Plugins = typeof PLUGINS;
5961

@@ -112,7 +114,8 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
112114
z.input<typeof DISPATCH_NAMESPACE_PLUGIN.options> &
113115
z.input<typeof IMAGES_PLUGIN.options> &
114116
z.input<typeof VECTORIZE_PLUGIN.options> &
115-
z.input<typeof CONTAINER_PLUGIN.options>;
117+
z.input<typeof CONTAINER_PLUGIN.options> &
118+
z.input<typeof MTLS_PLUGIN.options>;
116119

117120
export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
118121
z.input<typeof CACHE_PLUGIN.sharedOptions> &
@@ -183,3 +186,4 @@ export * from "./images";
183186
export * from "./vectorize";
184187
export * from "./containers";
185188
export { ContainerService } from "./containers/service";
189+
export * from "./mtls";
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import assert from "node:assert";
2+
import { z } from "zod";
3+
import {
4+
mixedModeClientWorker,
5+
MixedModeConnectionString,
6+
Plugin,
7+
ProxyNodeBinding,
8+
} from "../shared";
9+
10+
const MtlsSchema = z.object({
11+
certificate_id: z.string(),
12+
mixedModeConnectionString: z.custom<MixedModeConnectionString>(),
13+
});
14+
15+
export const MtlsOptionsSchema = z.object({
16+
mtlsCertificates: z.record(MtlsSchema).optional(),
17+
});
18+
19+
export const MTLS_PLUGIN_NAME = "mtls";
20+
21+
export const MTLS_PLUGIN: Plugin<typeof MtlsOptionsSchema> = {
22+
options: MtlsOptionsSchema,
23+
async getBindings(options) {
24+
if (!options.mtlsCertificates) {
25+
return [];
26+
}
27+
28+
return Object.entries(options.mtlsCertificates).map(
29+
([name, { certificate_id, mixedModeConnectionString }]) => {
30+
assert(mixedModeConnectionString, "MTLS only supports Mixed Mode");
31+
32+
return {
33+
name,
34+
35+
service: {
36+
name: `${MTLS_PLUGIN_NAME}:${certificate_id}`,
37+
},
38+
};
39+
}
40+
);
41+
},
42+
getNodeBindings(options: z.infer<typeof MtlsOptionsSchema>) {
43+
if (!options.mtlsCertificates) {
44+
return {};
45+
}
46+
return Object.fromEntries(
47+
Object.keys(options.mtlsCertificates).map((name) => [
48+
name,
49+
new ProxyNodeBinding(),
50+
])
51+
);
52+
},
53+
async getServices({ options }) {
54+
if (!options.mtlsCertificates) {
55+
return [];
56+
}
57+
58+
return Object.entries(options.mtlsCertificates).map(
59+
([name, { certificate_id, mixedModeConnectionString }]) => {
60+
assert(mixedModeConnectionString, "MTLS only supports Mixed Mode");
61+
62+
return {
63+
name: `${MTLS_PLUGIN_NAME}:${certificate_id}`,
64+
worker: mixedModeClientWorker(mixedModeConnectionString, name),
65+
};
66+
}
67+
);
68+
},
69+
};

packages/vite-plugin-cloudflare/src/miniflare-options.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,8 +719,12 @@ async function maybeStartOrUpdateMixedModeSession(
719719
// same we can just leave the mixedModeSession untouched
720720
if (mixedModeSession === undefined) {
721721
if (Object.keys(workerRemoteBindings).length > 0) {
722-
mixedModeSession =
723-
await experimental_startMixedModeSession(workerRemoteBindings);
722+
mixedModeSession = await experimental_startMixedModeSession(
723+
workerRemoteBindings,
724+
{
725+
workerName: workerConfig.name,
726+
}
727+
);
724728
mixedModeSessionsMap.set(workerConfig.name, mixedModeSession);
725729
}
726730
} else {

packages/wrangler/e2e/cert.test.ts

Lines changed: 9 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,14 @@
1-
import { randomUUID } from "node:crypto";
2-
import * as forge from "node-forge";
31
import { describe, expect, it } from "vitest";
2+
import {
3+
generateCaCertName,
4+
generateLeafCertificate,
5+
generateMtlsCertName,
6+
generateRootCaCert,
7+
generateRootCertificate,
8+
} from "./helpers/cert";
49
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
510
import { normalizeOutput } from "./helpers/normalize";
611

7-
// Generate X509 self signed root key pair and certificate
8-
function generateRootCertificate() {
9-
const rootKeys = forge.pki.rsa.generateKeyPair(2048);
10-
const rootCert = forge.pki.createCertificate();
11-
rootCert.publicKey = rootKeys.publicKey;
12-
rootCert.serialNumber = "01";
13-
rootCert.validity.notBefore = new Date();
14-
rootCert.validity.notAfter = new Date();
15-
rootCert.validity.notAfter.setFullYear(
16-
rootCert.validity.notBefore.getFullYear() + 10
17-
); // 10 years validity
18-
19-
const rootAttrs = [
20-
{ name: "commonName", value: "Root CA" },
21-
{ name: "countryName", value: "US" },
22-
{ shortName: "ST", value: "California" },
23-
{ name: "organizationName", value: "Localhost Root CA" },
24-
];
25-
rootCert.setSubject(rootAttrs);
26-
rootCert.setIssuer(rootAttrs); // Self-signed
27-
28-
rootCert.sign(rootKeys.privateKey, forge.md.sha256.create());
29-
30-
return { certificate: rootCert, privateKey: rootKeys.privateKey };
31-
}
32-
33-
// Generate X509 leaf certificate signed by the root
34-
function generateLeafCertificate(
35-
rootCert: forge.pki.Certificate,
36-
rootKey: forge.pki.PrivateKey
37-
) {
38-
const leafKeys = forge.pki.rsa.generateKeyPair(2048);
39-
const leafCert = forge.pki.createCertificate();
40-
leafCert.publicKey = leafKeys.publicKey;
41-
leafCert.serialNumber = "02";
42-
leafCert.validity.notBefore = new Date();
43-
leafCert.validity.notAfter = new Date();
44-
leafCert.validity.notAfter.setFullYear(2034, 10, 18);
45-
46-
const leafAttrs = [
47-
{ name: "commonName", value: "example.org" },
48-
{ name: "countryName", value: "US" },
49-
{ shortName: "ST", value: "California" },
50-
{ name: "organizationName", value: "Example Inc" },
51-
];
52-
leafCert.setSubject(leafAttrs);
53-
leafCert.setIssuer(rootCert.subject.attributes); // Signed by root
54-
55-
leafCert.sign(rootKey, forge.md.sha256.create()); // Signed using root's private key
56-
57-
const pemLeafCert = forge.pki.certificateToPem(leafCert);
58-
const pemLeafKey = forge.pki.privateKeyToPem(leafKeys.privateKey);
59-
60-
return { certificate: pemLeafCert, privateKey: pemLeafKey };
61-
}
62-
63-
// Generate self signed X509 CA root certificate
64-
function generateRootCaCert() {
65-
// Create a key pair (private and public keys)
66-
const keyPair = forge.pki.rsa.generateKeyPair(2048);
67-
68-
// Create a new X.509 certificate
69-
const cert = forge.pki.createCertificate();
70-
71-
// Set certificate fields
72-
cert.publicKey = keyPair.publicKey;
73-
cert.serialNumber = "01";
74-
cert.validity.notBefore = new Date();
75-
cert.validity.notAfter = new Date();
76-
cert.validity.notAfter.setFullYear(2034, 10, 18);
77-
78-
// Add issuer and subject fields (for a root CA, they are the same)
79-
const attrs = [
80-
{ name: "commonName", value: "Localhost CA" },
81-
{ name: "countryName", value: "US" },
82-
{ shortName: "ST", value: "California" },
83-
{ name: "localityName", value: "San Francisco" },
84-
{ name: "organizationName", value: "Localhost" },
85-
{ shortName: "OU", value: "SSL Department" },
86-
];
87-
cert.setSubject(attrs);
88-
cert.setIssuer(attrs);
89-
90-
// Add basic constraints and key usage extensions
91-
cert.setExtensions([
92-
{
93-
name: "basicConstraints",
94-
cA: true,
95-
},
96-
{
97-
name: "keyUsage",
98-
keyCertSign: true,
99-
digitalSignature: true,
100-
cRLSign: true,
101-
},
102-
]);
103-
104-
// Self-sign the certificate with the private key
105-
cert.sign(keyPair.privateKey, forge.md.sha256.create());
106-
107-
// Convert the certificate and private key to PEM format
108-
const pemCert = forge.pki.certificateToPem(cert);
109-
const pemPrivateKey = forge.pki.privateKeyToPem(keyPair.privateKey);
110-
111-
return { certificate: pemCert, privateKey: pemPrivateKey };
112-
}
113-
11412
describe("cert", () => {
11513
const normalize = (str: string) =>
11614
normalizeOutput(str, {
@@ -125,8 +23,8 @@ describe("cert", () => {
12523
const { certificate: caCert } = generateRootCaCert();
12624

12725
// Generate filenames for concurrent e2e test environment
128-
const mtlsCertName = `tmp-e2e-mtls-cert-${randomUUID()}`;
129-
const caCertName = `tmp-e2e-ca-cert-${randomUUID()}`;
26+
const mtlsCertName = generateMtlsCertName();
27+
const caCertName = generateCaCertName();
13028

13129
it("upload mtls-certificate", async () => {
13230
// locally generated certs/key

packages/wrangler/e2e/helpers/cert.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { randomUUID } from "node:crypto";
2+
import * as forge from "node-forge";
3+
4+
// Generate X509 self signed root key pair and certificate
5+
export function generateRootCertificate() {
6+
const rootKeys = forge.pki.rsa.generateKeyPair(2048);
7+
const rootCert = forge.pki.createCertificate();
8+
rootCert.publicKey = rootKeys.publicKey;
9+
rootCert.serialNumber = "01";
10+
rootCert.validity.notBefore = new Date();
11+
rootCert.validity.notAfter = new Date();
12+
rootCert.validity.notAfter.setFullYear(
13+
rootCert.validity.notBefore.getFullYear() + 10
14+
); // 10 years validity
15+
16+
const rootAttrs = [
17+
{ name: "commonName", value: "Root CA" },
18+
{ name: "countryName", value: "US" },
19+
{ shortName: "ST", value: "California" },
20+
{ name: "organizationName", value: "Localhost Root CA" },
21+
];
22+
rootCert.setSubject(rootAttrs);
23+
rootCert.setIssuer(rootAttrs); // Self-signed
24+
25+
rootCert.sign(rootKeys.privateKey, forge.md.sha256.create());
26+
27+
return { certificate: rootCert, privateKey: rootKeys.privateKey };
28+
}
29+
30+
// Generate X509 leaf certificate signed by the root
31+
export function generateLeafCertificate(
32+
rootCert: forge.pki.Certificate,
33+
rootKey: forge.pki.PrivateKey
34+
) {
35+
const leafKeys = forge.pki.rsa.generateKeyPair(2048);
36+
const leafCert = forge.pki.createCertificate();
37+
leafCert.publicKey = leafKeys.publicKey;
38+
leafCert.serialNumber = "02";
39+
leafCert.validity.notBefore = new Date();
40+
leafCert.validity.notAfter = new Date();
41+
leafCert.validity.notAfter.setFullYear(2034, 10, 18);
42+
43+
const leafAttrs = [
44+
{ name: "commonName", value: "example.org" },
45+
{ name: "countryName", value: "US" },
46+
{ shortName: "ST", value: "California" },
47+
{ name: "organizationName", value: "Example Inc" },
48+
];
49+
leafCert.setSubject(leafAttrs);
50+
leafCert.setIssuer(rootCert.subject.attributes); // Signed by root
51+
52+
leafCert.sign(rootKey, forge.md.sha256.create()); // Signed using root's private key
53+
54+
const pemLeafCert = forge.pki.certificateToPem(leafCert);
55+
const pemLeafKey = forge.pki.privateKeyToPem(leafKeys.privateKey);
56+
57+
return { certificate: pemLeafCert, privateKey: pemLeafKey };
58+
}
59+
60+
// Generate self signed X509 CA root certificate
61+
export function generateRootCaCert() {
62+
// Create a key pair (private and public keys)
63+
const keyPair = forge.pki.rsa.generateKeyPair(2048);
64+
65+
// Create a new X.509 certificate
66+
const cert = forge.pki.createCertificate();
67+
68+
// Set certificate fields
69+
cert.publicKey = keyPair.publicKey;
70+
cert.serialNumber = "01";
71+
cert.validity.notBefore = new Date();
72+
cert.validity.notAfter = new Date();
73+
cert.validity.notAfter.setFullYear(2034, 10, 18);
74+
75+
// Add issuer and subject fields (for a root CA, they are the same)
76+
const attrs = [
77+
{ name: "commonName", value: "Localhost CA" },
78+
{ name: "countryName", value: "US" },
79+
{ shortName: "ST", value: "California" },
80+
{ name: "localityName", value: "San Francisco" },
81+
{ name: "organizationName", value: "Localhost" },
82+
{ shortName: "OU", value: "SSL Department" },
83+
];
84+
cert.setSubject(attrs);
85+
cert.setIssuer(attrs);
86+
87+
// Add basic constraints and key usage extensions
88+
cert.setExtensions([
89+
{
90+
name: "basicConstraints",
91+
cA: true,
92+
},
93+
{
94+
name: "keyUsage",
95+
keyCertSign: true,
96+
digitalSignature: true,
97+
cRLSign: true,
98+
},
99+
]);
100+
101+
// Self-sign the certificate with the private key
102+
cert.sign(keyPair.privateKey, forge.md.sha256.create());
103+
104+
// Convert the certificate and private key to PEM format
105+
const pemCert = forge.pki.certificateToPem(cert);
106+
const pemPrivateKey = forge.pki.privateKeyToPem(keyPair.privateKey);
107+
108+
return { certificate: pemCert, privateKey: pemPrivateKey };
109+
}
110+
111+
export function generateMtlsCertName() {
112+
return `tmp-e2e-mtls-cert-${randomUUID()}`;
113+
}
114+
115+
export function generateCaCertName() {
116+
return `tmp-e2e-ca-cert-${randomUUID()}`;
117+
}

0 commit comments

Comments
 (0)