From 743565ee3e1ef47e06abd60100ec0552ba51f077 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 16 Aug 2024 20:21:05 +0200 Subject: [PATCH] chore: generate self-signed certificates for socks proxy (#32192) --- .../playwright-core/bin/socks-certs/README.md | 11 -- .../playwright-core/bin/socks-certs/cert.pem | 19 -- .../playwright-core/bin/socks-certs/key.pem | 28 --- .../socksClientCertificatesInterceptor.ts | 10 +- packages/playwright-core/src/utils/crypto.ts | 168 ++++++++++++++++++ 5 files changed, 171 insertions(+), 65 deletions(-) delete mode 100644 packages/playwright-core/bin/socks-certs/README.md delete mode 100644 packages/playwright-core/bin/socks-certs/cert.pem delete mode 100644 packages/playwright-core/bin/socks-certs/key.pem diff --git a/packages/playwright-core/bin/socks-certs/README.md b/packages/playwright-core/bin/socks-certs/README.md deleted file mode 100644 index 4950ef1f3cc31..0000000000000 --- a/packages/playwright-core/bin/socks-certs/README.md +++ /dev/null @@ -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" -``` diff --git a/packages/playwright-core/bin/socks-certs/cert.pem b/packages/playwright-core/bin/socks-certs/cert.pem deleted file mode 100644 index cce2f57bd5237..0000000000000 --- a/packages/playwright-core/bin/socks-certs/cert.pem +++ /dev/null @@ -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----- diff --git a/packages/playwright-core/bin/socks-certs/key.pem b/packages/playwright-core/bin/socks-certs/key.pem deleted file mode 100644 index 75f8e3bccc3c8..0000000000000 --- a/packages/playwright-core/bin/socks-certs/key.pem +++ /dev/null @@ -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----- diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 4e371df201efc..5510303882962 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -15,14 +15,12 @@ */ import net from 'net'; -import path from 'path'; import http2 from 'http2'; import type https from 'https'; -import fs from 'fs'; import tls from 'tls'; import stream from 'stream'; 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 { SocksProxy } from '../common/socksProxy'; import type * as channels from '@protocol/channels'; @@ -32,10 +30,8 @@ let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined; function loadDummyServerCertsIfNeeded() { if (dummyServerTlsOptions) return; - dummyServerTlsOptions = { - key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')), - cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')), - }; + const { cert, key } = generateSelfSignedCertificate(); + dummyServerTlsOptions = { key, cert }; } class ALPNCache { diff --git a/packages/playwright-core/src/utils/crypto.ts b/packages/playwright-core/src/utils/crypto.ts index f3e47f69933a5..5da56d4e9b695 100644 --- a/packages/playwright-core/src/utils/crypto.ts +++ b/packages/playwright-core/src/utils/crypto.ts @@ -15,6 +15,7 @@ */ import crypto from 'crypto'; +import { assert } from './debug'; export function createGuid(): string { return crypto.randomBytes(16).toString('hex'); @@ -25,3 +26,170 @@ export function calculateSha1(buffer: Buffer | string): string { hash.update(buffer); 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' }), + }; +}