chore: generate self-signed certificates for socks proxy (#32192)

This commit is contained in:
Max Schmitt 2024-08-16 20:21:05 +02:00 committed by GitHub
parent 3e6bba0b79
commit 743565ee3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 171 additions and 65 deletions

View file

@ -1,11 +0,0 @@
# Certfificates for Socks Proxy
These certificates are used when client certificates are used with
Playwright. Playwright then creates a Socks proxy, which sits between
the browser and the actual target server. The Socks proxy uses this certificiate
to talk to the browser and establishes its own secure TLS connection to the server.
The certificates are generated via:
```bash
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout key.pem -out cert.pem -subj "/CN=localhost"
```

View file

@ -1,19 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUTcrzEueVL/OuLHr4LBIPWeS4UL0wDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDcwNDA4NDAzNFoXDTM0MDcw
MjA4NDAzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEApof+SZVN4UGma4xJDVHhMSpmEJoCdMPr+HFadJJK/brF
BNOhA1C5wNk8oD/XYo7enAHQH/EsBnq4MMxv79rXTGnIdXMF+43GdMDh5kh81FQy
Esw8Vt4eif9eZkjUxI2GHhR2ovJewmQa7E+SeUB2RzJTqz8QPLhd74JFfgaci+S2
8L37ScVjcw55T1PcNflzB4vwsQHBT3yND0MLDhm+8MLzmTl4Mw5PgIOaBl5Jh8Tr
wQF4eeeB3FPJoMQhTP8aGBjW1mo+NmSSRAPIAZyhmCAnDeC33yRjAaiHjaL5Pr9f
wt5zoF5+U1xWhGXWzGOE6p/VTj62F9a2fOXNHclYJQIDAQABo1MwUTAdBgNVHQ4E
FgQU9BoVzGtb5x70KqGO/89N1hyqi5kwHwYDVR0jBBgwFoAU9BoVzGtb5x70KqGO
/89N1hyqi5kwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYcbI
wvcfx2p8z0RNN3EA+epKX1SagZyJX4ORIO8kln1sDU+ceHde3n3xnp1dg6HG2qh1
a7CZub/fNUaP9R8+6iiV0wPT7Ybkb2NIJcH1yq+/bfSS5OC5DO0yv9SUADdBoDwa
zOuBAqdcYW1BHYcbAzsQnniRcejHu06ioaS6SwwJ8150rQnLT4Lh9LAl40W6v4nZ
NdTGQETTrbjcgH1ER4IhWTKtVyPOxGF9A/OOawMEdfS8BhUO7YRS4QNFFaQMrJAb
MDhDtjSyDogLr8P43xjjWvQWG9a7zTF0kKEsdJ0cEG5HATpg8bPHmrouxbs2HGeH
kJXzMykrsYyXsInN3w==
-----END CERTIFICATE-----

View file

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmh/5JlU3hQaZr
jEkNUeExKmYQmgJ0w+v4cVp0kkr9usUE06EDULnA2TygP9dijt6cAdAf8SwGergw
zG/v2tdMach1cwX7jcZ0wOHmSHzUVDISzDxW3h6J/15mSNTEjYYeFHai8l7CZBrs
T5J5QHZHMlOrPxA8uF3vgkV+BpyL5LbwvftJxWNzDnlPU9w1+XMHi/CxAcFPfI0P
QwsOGb7wwvOZOXgzDk+Ag5oGXkmHxOvBAXh554HcU8mgxCFM/xoYGNbWaj42ZJJE
A8gBnKGYICcN4LffJGMBqIeNovk+v1/C3nOgXn5TXFaEZdbMY4Tqn9VOPrYX1rZ8
5c0dyVglAgMBAAECggEAB6zX4vNPKhUZAvbtvP/rlZUDLDu05kXLX+F1jk7ZxvTv
NKg+UQVM8l7wxN/8YM3944nP2lEGuuu4BoO9mvvmlV6Avy0EdxITNflX0AHCQxT4
U9Z253gIR0ruQl+T8tUk+8jsqNjr1iC//ukx8oWujdx7b7aR3IKQzcOeyU6rs2TN
lyrVVsEaFVi9+wCw0xyiCmPlobrn+egdigw7Zhp2BRinC6W9eMxuPS2hlhQUhBm/
eiD96YWp0RAv/L5qO93reoXIAzrrLdcUgPEnnq1zN7y2xihU2+B2sTph1m/A26+J
yPcXd7vQrXlRXQU6PaCa+0oJULlpiAzy3HPbnr4BkQKBgQDdmekTX8dQqiEZPX1C
017QRFbx0/x/TDFDSeJbDeauMzzCaGqCO2WVmYmTvFtby2G4/6BYowVtJVHm4uJl
XsYk8dWIQGLPIj1Cw7ZieJvb2EVRxgnY2oMaOTOazHzPHFzZV718zwEeZrryT82J
881E8wgM8V3DjkS4ye3TbwvimQKBgQDAYa/IdnpAg5z1TREi9Tt8fnoGpmSscAak
USgeXVsvoNzXXkE94MiiCOOrX1r68TWYDAzq6MKGDewkWOfLwXWR6D5C2LyE1q9P
1pxstgs/nC3ZUTz0yEH47ahSmhywhGlvXXOQEXUSLiVTOdeMCubMqwQW80F1868n
aBHcj5/lbQKBgQDIojjsWaNT3TTqbUmj30vQtI8jlBLgDlPr4FEYr5VT0wAH5BHK
p4xpzgFJyRfOHG312TuMBM087LUinfjsXsp3WJ1EJ0dO0mk0sY3HyfsTKNRaHTt9
Ixnf/DpExS+bNMq73Tyqa6FPrSNFkAtAA4SuEHwRe9aw33ZI+EpjS/8uwQKBgQCi
9NwqSLlLVnColEw0uVdXH+cLJPzX19i4bQo3lkp8MJ2ATJWk7XflUPRQoGf3ckQ8
c9CpVtoXJUnmi+xkeo21Nu0uQFqHhzZewWIk75rdmdR4ZUjl649+ZQkUVviASNjq
fVU7Lp5k9POm6LL9K+rOaPoA2rKTUAQItC2VD4+YjQKBgB6kgvgN6Mz/u0RE3kkV
2GOoP5sso71Hxwh7o6JEzUMhR+e/T/LLcBwEjLYcf1FYRySHsXLn2Ar/Uw1J7pAZ
ud54/at+7mTDliaT8Ar7S9vcso7ZfmuDX9qB9+c77idPskVBPo2tjJbwvFcB6sww
5Elcfmj6tEP4YLJ6Kv3qTPhT
-----END PRIVATE KEY-----

View file

@ -15,14 +15,12 @@
*/ */
import net from 'net'; import net from 'net';
import path from 'path';
import http2 from 'http2'; import http2 from 'http2';
import type https from 'https'; import type https from 'https';
import fs from 'fs';
import tls from 'tls'; import tls from 'tls';
import stream from 'stream'; import stream from 'stream';
import { createSocket, createTLSSocket } from '../utils/happy-eyeballs'; import { createSocket, createTLSSocket } from '../utils/happy-eyeballs';
import { escapeHTML, ManualPromise, rewriteErrorMessage } from '../utils'; import { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils';
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
import { SocksProxy } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
@ -32,10 +30,8 @@ let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
function loadDummyServerCertsIfNeeded() { function loadDummyServerCertsIfNeeded() {
if (dummyServerTlsOptions) if (dummyServerTlsOptions)
return; return;
dummyServerTlsOptions = { const { cert, key } = generateSelfSignedCertificate();
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')), dummyServerTlsOptions = { key, cert };
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
};
} }
class ALPNCache { class ALPNCache {

View file

@ -15,6 +15,7 @@
*/ */
import crypto from 'crypto'; import crypto from 'crypto';
import { assert } from './debug';
export function createGuid(): string { export function createGuid(): string {
return crypto.randomBytes(16).toString('hex'); return crypto.randomBytes(16).toString('hex');
@ -25,3 +26,170 @@ export function calculateSha1(buffer: Buffer | string): string {
hash.update(buffer); hash.update(buffer);
return hash.digest('hex'); return hash.digest('hex');
} }
// Variable-length quantity encoding aka. base-128 encoding
function encodeBase128(value: number): Buffer {
const bytes = [];
do {
let byte = value & 0x7f;
value >>>= 7;
if (bytes.length > 0) byte |= 0x80;
bytes.push(byte);
} while (value > 0);
return Buffer.from(bytes.reverse());
};
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
class DER {
static encodeSequence(data: Buffer[]): Buffer {
return this._encode(0x30, Buffer.concat(data));
}
static encodeInteger(data: number): Buffer {
assert(data >= -128 && data <= 127);
return this._encode(0x02, Buffer.from([data]));
}
static encodeObjectIdentifier(oid: string): Buffer {
const parts = oid.split('.').map((v) => Number(v));
// Encode the second part, which could be large, using base-128 encoding if necessary
const output = [encodeBase128(40 * parts[0] + parts[1])];
for (let i = 2; i < parts.length; i++) {
output.push(encodeBase128(parts[i]));
}
return this._encode(0x06, Buffer.concat(output));
}
static encodeNull(): Buffer {
return Buffer.from([0x05, 0x00]);
}
static encodeSet(data: Buffer[]): Buffer {
assert(data.length === 1, 'Only one item in the set is supported. We\'d need to sort the data to support more.');
// We expect the data to be already sorted.
return this._encode(0x31, Buffer.concat(data));
}
static encodeExplicitContextDependent(tag: number, data: Buffer): Buffer {
return this._encode(0xa0 + tag, data);
}
static encodePrintableString(data: string): Buffer {
return this._encode(0x13, Buffer.from(data));
}
static encodeBitString(data: Buffer): Buffer {
// The first byte of the content is the number of unused bits at the end
const unusedBits = 0; // Assuming all bits are used
const content = Buffer.concat([Buffer.from([unusedBits]), data]);
return this._encode(0x03, content);
}
static encodeDate(date: Date): Buffer {
const year = date.getUTCFullYear();
const isGeneralizedTime = year >= 2050;
const parts = [
isGeneralizedTime ? year.toString() : year.toString().slice(-2),
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
date.getUTCDate().toString().padStart(2, '0'),
date.getUTCHours().toString().padStart(2, '0'),
date.getUTCMinutes().toString().padStart(2, '0'),
date.getUTCSeconds().toString().padStart(2, '0')
];
const encodedDate = parts.join('') + 'Z';
const tag = isGeneralizedTime ? 0x18 : 0x17; // 0x18 for GeneralizedTime, 0x17 for UTCTime
return this._encode(tag, Buffer.from(encodedDate));
}
private static _encode(tag: number, data: Buffer): Buffer {
const lengthBytes = this._encodeLength(data.length);
return Buffer.concat([Buffer.from([tag]), lengthBytes, data]);
}
private static _encodeLength(length: number): Buffer {
if (length < 128) {
return Buffer.from([length]);
} else {
const lengthBytes = [];
while (length > 0) {
lengthBytes.unshift(length & 0xFF);
length >>= 8;
}
return Buffer.from([0x80 | lengthBytes.length, ...lengthBytes]);
}
}
}
// X.509 Specification: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1
export function generateSelfSignedCertificate() {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const publicKeyDer = publicKey.export({ type: 'pkcs1', format: 'der' });
const oneYearInMilliseconds = 365 * 24 * 60 * 60 * 1_000;
const notBefore = new Date(new Date().getTime() - oneYearInMilliseconds);
const notAfter = new Date(new Date().getTime() + oneYearInMilliseconds);
// List of fields / structure: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1
const tbsCertificate = DER.encodeSequence([
DER.encodeExplicitContextDependent(0, DER.encodeInteger(1)), // version
DER.encodeInteger(1), // serialNumber
DER.encodeSequence([
DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1
DER.encodeNull()
]), // signature
DER.encodeSequence([
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component
DER.encodePrintableString('localhost')
]),
]),
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component
DER.encodePrintableString('Playwright Client Certificate Support')
])
])
]), // issuer
DER.encodeSequence([
DER.encodeDate(notBefore), // notBefore
DER.encodeDate(notAfter), // notAfter
]), // validity
DER.encodeSequence([
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component
DER.encodePrintableString('localhost')
]),
]),
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component
DER.encodePrintableString('Playwright Client Certificate Support')
])
])
]), // subject
DER.encodeSequence([
DER.encodeSequence([
DER.encodeObjectIdentifier('1.2.840.113549.1.1.1'), // rsaEncryption PKCS #1
DER.encodeNull()
]),
DER.encodeBitString(publicKeyDer)
]), // SubjectPublicKeyInfo
]);
const signature = crypto.sign('sha256', tbsCertificate, privateKey);
const certificate = DER.encodeSequence([
tbsCertificate,
DER.encodeSequence([
DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1
DER.encodeNull()
]),
DER.encodeBitString(signature)
]);
const certPem = [
'-----BEGIN CERTIFICATE-----',
// Split the base64 string into lines of 64 characters
certificate.toString('base64').match(/.{1,64}/g)!.join('\n'),
'-----END CERTIFICATE-----'
].join('\n');
return {
cert: certPem,
key: privateKey.export({ type: 'pkcs1', format: 'pem' }),
};
}