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 4950ef1f3c..0000000000 --- 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 cce2f57bd5..0000000000 --- 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 75f8e3bccc..0000000000 --- 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 4e371df201..1775a96f07 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,9 @@ 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')), - }; + // TODO: do we want to have it unique per browser context, launch or global? + const { cert, key } = generateSelfSignedCertificate('localhost'); + dummyServerTlsOptions = { key, cert }; } class ALPNCache { diff --git a/packages/playwright-core/src/utils/crypto.ts b/packages/playwright-core/src/utils/crypto.ts index f3e47f6993..4fb05f2a09 100644 --- a/packages/playwright-core/src/utils/crypto.ts +++ b/packages/playwright-core/src/utils/crypto.ts @@ -25,3 +25,171 @@ export function calculateSha1(buffer: Buffer | string): string { hash.update(buffer); return hash.digest('hex'); } + +const encodeBase128 = (value: number) => { + const bytes = new Uint8Array(calculateBase128BytesNeeded(value)); + const lastPos = bytes.byteLength - 1; + let pos = lastPos; + do { + let byte = value & 0x7f; // Take the last 7 bits + value >>>= 7; // Shift right, unsigned + if (pos !== lastPos) { + byte |= 0x80; // Set the continuation bit on all but the first byte + } + bytes[pos--] = byte; // Insert the byte at the start of the array + } while (value > 0); + return bytes; +}; + +const calculateBase128BytesNeeded = (num: number) => { + // Start at 6 and not 0 to account for overflow and to ensure that the + // division below always gives a value equal to or greater than 1. + // For example, consider the following 'real' bits needed: + // 0: 6 (initial value) + 1 (real) => 7 / 7 = 1 + // 7: 6 (initial value) + 7 (real) => 13 / 7 = 1 + // 8: 6 (initial value) + 8 (real) => 14 / 7 = 2 + let bitsNeeded = 6; + + do { + bitsNeeded++; + num >>>= 1; + } while (num > 0); + + return (bitsNeeded / 7) >>> 0; +}; + +class ASN1 { + static toSequence(data: Buffer[]): Buffer { + return this._encode(0x30, Buffer.concat(data)); + } + static toInteger(data: number): Buffer { + return this._encode(0x02, Buffer.from([data])); + } + static toObject(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 toNull(): Buffer { + return Buffer.from([0x05, 0x00]); + } + static toSet(data: Buffer[]): Buffer { + return this._encode(0x31, Buffer.concat(data)); + } + static toContextSpecific(tag: number, data: Buffer): Buffer { + return this._encode(0xa0 + tag, data); + } + static toPrintableString(data: string): Buffer { + return this._encode(0x13, Buffer.from(data)); + } + static toBitString(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 toUtcTime(date: Date): Buffer { + const parts = [ + date.getUTCFullYear().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') + ]; + return this._encode(0x17, Buffer.from(parts.join('') + 'Z')); + } + 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]); + } + } +} + +export function generateSelfSignedCertificate(commonName: string) { + const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); + const publicKeyDer = publicKey.export({ type: 'pkcs1', format: 'der' }); + + const tbsCertificate = ASN1.toSequence([ + ASN1.toContextSpecific(0, ASN1.toInteger(1)), // version + ASN1.toInteger(1), // serialNumber + ASN1.toSequence([ + ASN1.toObject('1.2.840.113549.1.1.11'), + ASN1.toNull() + ]), // signature + ASN1.toSequence([ + ASN1.toSet([ + ASN1.toSequence([ + ASN1.toObject('2.5.4.3'), + ASN1.toPrintableString(commonName) + ]), + ASN1.toSequence([ + ASN1.toObject('2.5.4.10'), + ASN1.toPrintableString('Client Certificate Demo') + ]) + ]) + ]), // issuer + ASN1.toSequence([ + ASN1.toUtcTime(new Date()), + ASN1.toUtcTime(new Date()), + ]), // validity + ASN1.toSequence([ + ASN1.toSet([ + ASN1.toSequence([ + ASN1.toObject('2.5.4.3'), + ASN1.toPrintableString(commonName) + ]), + ASN1.toSequence([ + ASN1.toObject('2.5.4.10'), + ASN1.toPrintableString('Client Certificate Demo') + ]) + ]) + ]), // subject + ASN1.toSequence([ + ASN1.toSequence([ + ASN1.toObject('1.2.840.113549.1.1.1'), + ASN1.toNull() + ]), + ASN1.toBitString(publicKeyDer) + ]), // SubjectPublicKeyInfo + ]); + + const signature = crypto.sign('sha256', tbsCertificate, privateKey); + + const certificate = ASN1.toSequence([ + tbsCertificate, + ASN1.toSequence([ + ASN1.toObject('1.2.840.113549.1.1.11'), + ASN1.toNull() + ]), + ASN1.toBitString(signature) + ]); + + const certPem = [ + '-----BEGIN CERTIFICATE-----', + certificate.toString('base64').match(/.{1,64}/g)!.join('\n'), + '-----END CERTIFICATE-----' + ].join('\n'); + + return { + cert: certPem, + key: privateKey.export({ type: 'pkcs1', format: 'pem' }), + }; +}