chore: generate self-signed certificates for socks proxy (#32192)
This commit is contained in:
parent
3e6bba0b79
commit
743565ee3e
|
|
@ -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"
|
|
||||||
```
|
|
||||||
|
|
@ -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-----
|
|
||||||
|
|
@ -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-----
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue