diff --git a/docs/src/api/class-apirequest.md b/docs/src/api/class-apirequest.md index 164de27b2e..2f832e1dd8 100644 --- a/docs/src/api/class-apirequest.md +++ b/docs/src/api/class-apirequest.md @@ -12,6 +12,9 @@ see [APIRequestContext]. Creates new instances of [APIRequestContext]. +### option: APIRequest.newContext.clientCertificates = %%-context-option-clientCertificates-%% +* since: 1.46 + ### option: APIRequest.newContext.useragent = %%-context-option-useragent-%% * since: v1.16 diff --git a/docs/src/api/class-browser.md b/docs/src/api/class-browser.md index abbf4fd96b..9ae59571fc 100644 --- a/docs/src/api/class-browser.md +++ b/docs/src/api/class-browser.md @@ -256,6 +256,9 @@ await browser.CloseAsync(); ### option: Browser.newContext.proxy = %%-context-option-proxy-%% * since: v1.8 +### option: Browser.newContext.clientCertificates = %%-context-option-clientCertificates-%% +* since: 1.46 + ### option: Browser.newContext.storageState = %%-js-python-context-option-storage-state-%% * since: v1.8 @@ -281,6 +284,9 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo ### option: Browser.newPage.proxy = %%-context-option-proxy-%% * since: v1.8 +### option: Browser.newPage.clientCertificates = %%-context-option-clientCertificates-%% +* since: 1.46 + ### option: Browser.newPage.storageState = %%-js-python-context-option-storage-state-%% * since: v1.8 diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index 176f696e1d..0d41bb1aa7 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -343,6 +343,9 @@ use a temporary directory instead. ### option: BrowserType.launchPersistentContext.firefoxUserPrefs2 = %%-csharp-java-browser-option-firefoxuserprefs-%% * since: v1.40 +### option: BrowserType.launchPersistentContext.clientCertificates = %%-context-option-clientCertificates-%% +* since: 1.46 + ## async method: BrowserType.launchServer * since: v1.8 * langs: js diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 253ca3a1fd..4e2d554ed1 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -514,6 +514,25 @@ Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_ Does not enforce fixed viewport, allows resizing window in the headed mode. +## context-option-clientCertificates +- `clientCertificates` <[Array]<[Object]>> + - `url` <[string]> Glob pattern to match the URLs that the certificate is valid for. + - `certs` <[Array]<[Object]>> List of client certificates to be used. + - `certPath` ?<[string]> Path to the file with the certificate in PEM format. + - `keyPath` ?<[string]> Path to the file with the private key in PEM format. + - `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain. + - `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX). + +An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided with a glob pattern to match the URLs that the certificate is valid for. + +:::note +Using Client Certificates in combination with Proxy Servers is not supported. +::: + +:::note +When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. +::: + ## context-option-useragent - `userAgent` <[string]> diff --git a/docs/src/test-api/class-testoptions.md b/docs/src/test-api/class-testoptions.md index 6a4dfd55c7..ac6b29473c 100644 --- a/docs/src/test-api/class-testoptions.md +++ b/docs/src/test-api/class-testoptions.md @@ -138,6 +138,35 @@ export default defineConfig({ ] }); ``` + +## property: TestOptions.clientCertificates = %%-context-option-clientCertificates-%% +* since: 1.46 + +**Usage** + +```js title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + projects: [ + { + name: 'Microsoft Edge', + use: { + ...devices['Desktop Edge'], + clientCertificates: [{ + url: 'https://example.com/**', + certs: [{ + certPath: './cert.pem', + keyPath: './key.pem', + passphase: 'mysecretpassword', + }], + }], + }, + }, + ] +}); +``` + ## property: TestOptions.colorScheme = %%-context-option-colorscheme-%% * since: v1.10 diff --git a/packages/playwright-core/bin/socks-certs/README.md b/packages/playwright-core/bin/socks-certs/README.md new file mode 100644 index 0000000000..4950ef1f3c --- /dev/null +++ b/packages/playwright-core/bin/socks-certs/README.md @@ -0,0 +1,11 @@ +# 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 new file mode 100644 index 0000000000..cce2f57bd5 --- /dev/null +++ b/packages/playwright-core/bin/socks-certs/cert.pem @@ -0,0 +1,19 @@ +-----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 new file mode 100644 index 0000000000..75f8e3bccc --- /dev/null +++ b/packages/playwright-core/bin/socks-certs/key.pem @@ -0,0 +1,28 @@ +-----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/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index fcc17c4231..5100b0db25 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -529,6 +529,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion, forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors, acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), + clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), }; if (!contextParams.recordVideo && options.videosPath) { contextParams.recordVideo = { @@ -548,3 +549,21 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) { return 'accept'; return 'deny'; } + +export async function toClientCertificatesProtocol(clientCertificates?: BrowserContextOptions['clientCertificates']): Promise { + if (!clientCertificates) + return undefined; + return await Promise.all(clientCertificates.map(async clientCertificate => { + return { + url: clientCertificate.url, + certs: await Promise.all(clientCertificate.certs.map(async cert => { + return { + cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined, + key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined, + pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined, + passphrase: cert.passphrase, + }; + })) + }; + })); +} diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 44314c6fa3..63f74ef26e 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -25,10 +25,11 @@ import { assert, headersObjectToArray, isString } from '../utils'; import { mkdirIfNeeded } from '../utils/fileUtils'; import { ChannelOwner } from './channelOwner'; import { RawHeaders } from './network'; -import type { FilePayload, Headers, StorageState } from './types'; +import type { ClientCertificate, FilePayload, Headers, StorageState } from './types'; import type { Playwright } from './playwright'; import { Tracing } from './tracing'; import { TargetClosedError, isTargetClosedError } from './errors'; +import { toClientCertificatesProtocol } from './browserContext'; export type FetchOptions = { params?: { [key: string]: string; }, @@ -44,9 +45,10 @@ export type FetchOptions = { maxRetries?: number, }; -type NewContextOptions = Omit & { +type NewContextOptions = Omit & { extraHTTPHeaders?: Headers, storageState?: string | StorageState, + clientCertificates?: ClientCertificate[]; }; type RequestWithBodyOptions = Omit; @@ -74,6 +76,7 @@ export class APIRequest implements api.APIRequest { extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, storageState, tracesDir, + clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), })).request); this._contexts.add(context); context._request = this; @@ -175,7 +178,7 @@ export class APIRequestContext extends ChannelOwner = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']); -export type BrowserContextOptions = Omit & { +export type ClientCertificate = { + url: string; + certs: { + certPath?: string; + keyPath?: string; + pfxPath?: string; + passphrase?: string; + }[]; +}; + +export type BrowserContextOptions = Omit & { viewport?: Size | null; extraHTTPHeaders?: Headers; logger?: Logger; @@ -70,6 +80,7 @@ export type BrowserContextOptions = Omit { validateBrowserContextOptions(options, this.options); - const context = await this.doCreateNewContext(options); + const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options); + let context; + try { + context = await this.doCreateNewContext(options); + } catch (error) { + await clientCertificatesProxy?.close(); + throw error; + } + context._clientCertificatesProxy = clientCertificatesProxy; if (options.storageState) await context.setStorageState(metadata, options.storageState); return context; diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 84673f533d..b67771fe9a 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -43,6 +43,7 @@ import * as consoleApiSource from '../generated/consoleApiSource'; import { BrowserContextAPIRequestContext } from './fetch'; import type { Artifact } from './artifact'; import { Clock } from './clock'; +import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; export abstract class BrowserContext extends SdkObject { static Events = { @@ -90,6 +91,7 @@ export abstract class BrowserContext extends SdkObject { private _debugger!: Debugger; _closeReason: string | undefined; readonly clock: Clock; + _clientCertificatesProxy: ClientCertificatesProxy | undefined; constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) { super(browser, 'browser-context'); @@ -245,6 +247,7 @@ export abstract class BrowserContext extends SdkObject { // at the same time. return; } + this._clientCertificatesProxy?.close().catch(() => {}); this.tracing.abort(); if (this._isPersistentContext) this.onClosePersistent(); @@ -655,6 +658,18 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) { } } +export async function createClientCertificatesProxyIfNeeded(options: channels.BrowserNewContextOptions, browserOptions?: BrowserOptions) { + if (!options.clientCertificates?.length) + return; + if (options.proxy?.server || browserOptions?.proxy?.server) + throw new Error('Cannot specify both proxy and clientCertificates'); + verifyClientCertificates(options.clientCertificates); + const clientCertificatesProxy = new ClientCertificatesProxy(options); + options.proxy = { server: await clientCertificatesProxy.listen() }; + options.ignoreHTTPSErrors = true; + return clientCertificatesProxy; +} + export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) { if (options.noDefaultViewport && options.deviceScaleFactor !== undefined) throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`); @@ -707,6 +722,27 @@ export function verifyGeolocation(geolocation?: types.Geolocation) { throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`); } +export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) { + if (!clientCertificates) + return; + for (const { url, certs } of clientCertificates) { + if (!url) + throw new Error(`clientCertificates.url is required`); + if (!certs.length) + throw new Error('No certs specified for url: ' + url); + for (const cert of certs) { + if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx) + throw new Error('None of cert, key, passphrase or pfx is specified'); + if (cert.cert && !cert.key) + throw new Error('cert is specified without key'); + if (!cert.cert && cert.key) + throw new Error('key is specified without cert'); + if (cert.pfx && (cert.cert || cert.key)) + throw new Error('pfx is specified together with cert, key or passphrase'); + } + } +} + export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxySettings { let { server, bypass } = proxy; let url; diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index cfa8862b84..24a057ebf9 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import * as os from 'os'; import path from 'path'; import type { BrowserContext } from './browserContext'; -import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; +import { createClientCertificatesProxyIfNeeded, normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; import type { BrowserName } from './registry'; import { registry } from './registry'; import type { ConnectionTransport } from './transport'; @@ -77,10 +77,17 @@ export abstract class BrowserType extends SdkObject { async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise { options = this._validateLaunchOptions(options); const controller = new ProgressController(metadata, this); - const persistent: channels.BrowserNewContextParams = options; + const persistent: channels.BrowserNewContextParams = { ...options }; controller.setLogName('browser'); - const browser = await controller.run(progress => { - return this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); + const browser = await controller.run(async progress => { + // Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors. + const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistent); + if (clientCertificatesProxy) + options.proxy = persistent.proxy; + progress.cleanupWhenAborted(() => clientCertificatesProxy?.close()); + const browser = await this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); + browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy; + return browser; }, TimeoutSettings.launchTimeout(options)); return browser._defaultContext!; } diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index c02781f748..f5dad1d656 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -16,8 +16,9 @@ import type * as channels from '@protocol/channels'; import type { LookupAddress } from 'dns'; -import * as http from 'http'; -import * as https from 'https'; +import http from 'http'; +import fs from 'fs'; +import https from 'https'; import type { Readable, TransformCallback } from 'stream'; import { pipeline, Transform } from 'stream'; import url from 'url'; @@ -27,7 +28,7 @@ import { TimeoutSettings } from '../common/timeoutSettings'; import { getUserAgent } from '../utils/userAgent'; import { assert, createGuid, monotonicTime } from '../utils'; import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; -import { BrowserContext } from './browserContext'; +import { BrowserContext, verifyClientCertificates } from './browserContext'; import { CookieStore, domainMatches } from './cookieStore'; import { MultipartFormData } from './formData'; import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs'; @@ -40,6 +41,7 @@ import { Tracing } from './trace/recorder/tracing'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; import { kMaxCookieExpiresDateInSeconds } from './network'; +import { clientCertificatesToTLSOptions } from './socksClientCertificatesInterceptor'; type FetchRequestOptions = { userAgent: string; @@ -49,6 +51,7 @@ type FetchRequestOptions = { timeoutSettings: TimeoutSettings; ignoreHTTPSErrors?: boolean; baseURL?: string; + clientCertificates?: channels.BrowserNewContextOptions['clientCertificates']; }; type HeadersObject = Readonly<{ [name: string]: string }>; @@ -190,9 +193,12 @@ export abstract class APIRequestContext extends SdkObject { maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects, timeout, deadline, + ...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.toString()), __testHookLookup: (params as any).__testHookLookup, }; - // rejectUnauthorized = undefined is treated as true in node 12. + if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA) + options.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)]; + // rejectUnauthorized = undefined is treated as true in Node.js 12. if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors) options.rejectUnauthorized = false; @@ -351,6 +357,7 @@ export abstract class APIRequestContext extends SdkObject { maxRedirects: options.maxRedirects - 1, timeout: options.timeout, deadline: options.deadline, + ...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.toString()), __testHookLookup: options.__testHookLookup, }; // rejectUnauthorized = undefined is treated as true in node 12. @@ -522,6 +529,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext { timeoutSettings: this._context._timeoutSettings, ignoreHTTPSErrors: this._context._options.ignoreHTTPSErrors, baseURL: this._context._options.baseURL, + clientCertificates: this._context._options.clientCertificates, }; } @@ -557,17 +565,21 @@ export class GlobalAPIRequestContext extends APIRequestContext { if (!/^\w+:\/\//.test(url)) url = 'http://' + url; proxy.server = url; + if (options.clientCertificates) + throw new Error('Cannot specify both proxy and clientCertificates'); } if (options.storageState) { this._origins = options.storageState.origins; this._cookieStore.addCookies(options.storageState.cookies || []); } + verifyClientCertificates(options.clientCertificates); this._options = { baseURL: options.baseURL, userAgent: options.userAgent || getUserAgent(), extraHTTPHeaders: options.extraHTTPHeaders, ignoreHTTPSErrors: !!options.ignoreHTTPSErrors, httpCredentials: options.httpCredentials, + clientCertificates: options.clientCertificates, proxy, timeoutSettings, }; diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts new file mode 100644 index 0000000000..5c483e7c30 --- /dev/null +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -0,0 +1,203 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type net from 'net'; +import path from 'path'; +import type https from 'https'; +import fs from 'fs'; +import tls from 'tls'; +import stream from 'stream'; +import { createSocket } from '../utils/happy-eyeballs'; +import { globToRegex } from '../utils'; +import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy'; +import { SocksProxy } from '../common/socksProxy'; +import type * as channels from '@protocol/channels'; + +class SocksConnectionDuplex extends stream.Duplex { + constructor(private readonly writeCallback: (data: Buffer) => void) { + super(); + } + override _read(): void { } + override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void): void { + this.writeCallback(chunk); + callback(); + } +} + +class SocksProxyConnection { + private readonly socksProxy: ClientCertificatesProxy; + private readonly uid: string; + private readonly host: string; + private readonly port: number; + firstPackageReceived: boolean = false; + target!: net.Socket; + // In case of http, we just pipe data to the target socket and they are |undefined|. + internal: stream.Duplex | undefined; + internalTLS: tls.TLSSocket | undefined; + + constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) { + this.socksProxy = socksProxy; + this.uid = uid; + this.host = host; + this.port = port; + } + + async connect() { + this.target = await createSocket(this.host === 'local.playwright' ? 'localhost' : this.host, this.port); + this.target.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid })); + this.target.on('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message })); + this.socksProxy._socksProxy.socketConnected({ + uid: this.uid, + host: this.target.localAddress!, + port: this.target.localPort!, + }); + } + + public onClose() { + this.internal?.destroy(); + this.target.destroy(); + } + + public onData(data: Buffer) { + // HTTP / TLS are client-hello based protocols. This allows us to detect + // the protocol on the first package and attach appropriate listeners. + if (!this.firstPackageReceived) { + this.firstPackageReceived = true; + // 0x16 is SSLv3/TLS "handshake" content type: https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_record + if (data[0] === 0x16) + this._attachTLSListeners(); + else + this.target.on('data', data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data })); + } + if (this.internal) + this.internal.push(data); + else + this.target.write(data); + } + + private _attachTLSListeners() { + this.internal = new SocksConnectionDuplex(data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data })); + const internalTLS = new tls.TLSSocket(this.internal, { + isServer: true, + key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')), + cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')), + }); + this.internalTLS = internalTLS; + internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid })); + + const targetTLS = tls.connect({ + socket: this.target, + rejectUnauthorized: this.socksProxy.contextOptions.ignoreHTTPSErrors === true ? false : true, + ...clientCertificatesToTLSOptions(this.socksProxy.contextOptions.clientCertificates, `https://${this.host}:${this.port}/`), + }); + + internalTLS.pipe(targetTLS); + targetTLS.pipe(internalTLS); + + // Handle close and errors + const closeBothSockets = () => { + internalTLS.end(); + targetTLS.end(); + }; + + internalTLS.on('end', () => closeBothSockets()); + targetTLS.on('end', () => closeBothSockets()); + + internalTLS.on('error', () => closeBothSockets()); + targetTLS.on('error', error => { + internalTLS.write('HTTP/1.1 503 Internal Server Error\r\n'); + internalTLS.write('Content-Type: text/html; charset=utf-8\r\n'); + const responseBody = 'Playwright client-certificate error: ' + error.message; + internalTLS.write('Content-Length: ' + Buffer.byteLength(responseBody) + '\r\n'); + internalTLS.write('\r\n'); + internalTLS.write(responseBody); + internalTLS.end(); + closeBothSockets(); + }); + } +} + +export class ClientCertificatesProxy { + _socksProxy: SocksProxy; + private _connections: Map = new Map(); + + constructor( + public readonly contextOptions: Pick + ) { + this._socksProxy = new SocksProxy(); + this._socksProxy.setPattern('*'); + this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { + try { + const connection = new SocksProxyConnection(this, payload.uid, payload.host, payload.port); + await connection.connect(); + this._connections.set(payload.uid, connection); + } catch (error) { + this._socksProxy.socketFailed({ uid: payload.uid, errorCode: error.code }); + } + }); + this._socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => { + this._connections.get(payload.uid)?.onData(payload.data); + }); + this._socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => { + this._connections.get(payload.uid)?.onClose(); + this._connections.delete(payload.uid); + }); + } + + public async listen(): Promise { + const port = await this._socksProxy.listen(0, '127.0.0.1'); + return `socks5://127.0.0.1:${port}`; + } + + public async close() { + await this._socksProxy.close(); + } +} + +const kClientCertificatesGlobRegex = Symbol('kClientCertificatesGlobRegex'); + +export function clientCertificatesToTLSOptions( + clientCertificates: channels.BrowserNewContextOptions['clientCertificates'], + requestURL: string +): Pick | undefined { + const matchingCerts = clientCertificates?.filter(c => { + let regex: RegExp | undefined = (c as any)[kClientCertificatesGlobRegex]; + if (!regex) { + regex = globToRegex(c.url); + (c as any)[kClientCertificatesGlobRegex] = regex; + } + regex.lastIndex = 0; + return regex.test(requestURL); + }); + if (!matchingCerts || !matchingCerts.length) + return; + const tlsOptions = { + pfx: [] as { buf: Buffer, passphrase?: string }[], + key: [] as { pem: Buffer, passphrase?: string }[], + cert: [] as Buffer[], + }; + for (const { certs } of matchingCerts) { + for (const cert of certs) { + if (cert.cert) + tlsOptions.cert.push(cert.cert); + if (cert.key) + tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase }); + if (cert.pfx) + tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase }); + } + } + return tlsOptions; +} diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d1dfa46df9..f9e25c8c40 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -13221,6 +13221,49 @@ export interface BrowserType { */ chromiumSandbox?: boolean; + /** + * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a + * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the + * private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided + * with a glob pattern to match the URLs that the certificate is valid for. + * + * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. + * + * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it + * work by replacing `localhost` with `local.playwright`. + */ + clientCertificates?: Array<{ + /** + * Glob pattern to match the URLs that the certificate is valid for. + */ + url: string; + + /** + * List of client certificates to be used. + */ + certs: Array<{ + /** + * Path to the file with the certificate in PEM format. + */ + certPath?: string; + + /** + * Path to the file with the private key in PEM format. + */ + keyPath?: string; + + /** + * Path to the PFX or PKCS12 encoded private key and certificate chain. + */ + pfxPath?: string; + + /** + * Passphrase for the private key (PEM or PFX). + */ + passphrase?: string; + }>; + }>; + /** * Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. @@ -15590,6 +15633,49 @@ export interface APIRequest { */ baseURL?: string; + /** + * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a + * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the + * private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided + * with a glob pattern to match the URLs that the certificate is valid for. + * + * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. + * + * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it + * work by replacing `localhost` with `local.playwright`. + */ + clientCertificates?: Array<{ + /** + * Glob pattern to match the URLs that the certificate is valid for. + */ + url: string; + + /** + * List of client certificates to be used. + */ + certs: Array<{ + /** + * Path to the file with the certificate in PEM format. + */ + certPath?: string; + + /** + * Path to the file with the private key in PEM format. + */ + keyPath?: string; + + /** + * Path to the PFX or PKCS12 encoded private key and certificate chain. + */ + pfxPath?: string; + + /** + * Passphrase for the private key (PEM or PFX). + */ + passphrase?: string; + }>; + }>; + /** * An object containing additional HTTP headers to be sent with every request. Defaults to none. */ @@ -16741,6 +16827,49 @@ export interface Browser extends EventEmitter { */ bypassCSP?: boolean; + /** + * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a + * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the + * private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided + * with a glob pattern to match the URLs that the certificate is valid for. + * + * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. + * + * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it + * work by replacing `localhost` with `local.playwright`. + */ + clientCertificates?: Array<{ + /** + * Glob pattern to match the URLs that the certificate is valid for. + */ + url: string; + + /** + * List of client certificates to be used. + */ + certs: Array<{ + /** + * Path to the file with the certificate in PEM format. + */ + certPath?: string; + + /** + * Path to the file with the private key in PEM format. + */ + keyPath?: string; + + /** + * Path to the PFX or PKCS12 encoded private key and certificate chain. + */ + pfxPath?: string; + + /** + * Passphrase for the private key (PEM or PFX). + */ + passphrase?: string; + }>; + }>; + /** * Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. @@ -20173,6 +20302,49 @@ export interface BrowserContextOptions { */ bypassCSP?: boolean; + /** + * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a + * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the + * private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided + * with a glob pattern to match the URLs that the certificate is valid for. + * + * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. + * + * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it + * work by replacing `localhost` with `local.playwright`. + */ + clientCertificates?: Array<{ + /** + * Glob pattern to match the URLs that the certificate is valid for. + */ + url: string; + + /** + * List of client certificates to be used. + */ + certs: Array<{ + /** + * Path to the file with the certificate in PEM format. + */ + certPath?: string; + + /** + * Path to the file with the private key in PEM format. + */ + keyPath?: string; + + /** + * Path to the PFX or PKCS12 encoded private key and certificate chain. + */ + pfxPath?: string; + + /** + * Passphrase for the private key (PEM or PFX). + */ + passphrase?: string; + }>; + }>; + /** * Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See * [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) for more details. diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 01ae465097..65185925be 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -140,6 +140,7 @@ const playwrightFixtures: Fixtures = ({ permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }], proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }], storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }], + clientCertificates: [({ contextOptions }, use) => use(contextOptions.clientCertificates), { option: true }], timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }], userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }], viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }], @@ -155,6 +156,7 @@ const playwrightFixtures: Fixtures = ({ _combinedContextOptions: [async ({ acceptDownloads, bypassCSP, + clientCertificates, colorScheme, deviceScaleFactor, extraHTTPHeaders, @@ -209,6 +211,8 @@ const playwrightFixtures: Fixtures = ({ options.proxy = proxy; if (storageState !== undefined) options.storageState = storageState; + if (clientCertificates?.length) + options.clientCertificates = resolveClientCerticates(clientCertificates); if (timezoneId !== undefined) options.timezoneId = timezoneId; if (userAgent !== undefined) @@ -416,6 +420,28 @@ function attachConnectedHeaderIfNeeded(testInfo: TestInfo, browser: Browser | nu } } +function resolveFileToConfig(file: string | undefined) { + const config = test.info().config.configFile; + if (!config || !file) + return file; + if (path.isAbsolute(file)) + return file; + return path.resolve(path.dirname(config), file); +} + +type ClientCertificates = NonNullable; + +function resolveClientCerticates(clientCertificates: ClientCertificates): ClientCertificates { + for (const { certs } of clientCertificates) { + for (const cert of certs) { + cert.certPath = resolveFileToConfig(cert.certPath); + cert.keyPath = resolveFileToConfig(cert.keyPath); + cert.pfxPath = resolveFileToConfig(cert.pfxPath); + } + } + return clientCertificates; +} + const kTracingStarted = Symbol('kTracingStarted'); const kIsReusedContext = Symbol('kReusedContext'); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index d9d740f000..b5cc9aec8f 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -4823,6 +4823,7 @@ export type Fixtures; type ColorScheme = Exclude; +type ClientCertificate = Exclude[0]; type ExtraHTTPHeaders = Exclude; type Proxy = Exclude; type StorageState = Exclude; @@ -5200,6 +5201,45 @@ export interface PlaywrightTestOptions { * */ colorScheme: ColorScheme; + /** + * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a + * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the + * private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided + * with a glob pattern to match the URLs that the certificate is valid for. + * + * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. + * + * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it + * work by replacing `localhost` with `local.playwright`. + * + * **Usage** + * + * ```js + * // playwright.config.ts + * import { defineConfig } from '@playwright/test'; + * + * export default defineConfig({ + * projects: [ + * { + * name: 'Microsoft Edge', + * use: { + * ...devices['Desktop Edge'], + * clientCertificates: [{ + * url: 'https://example.com/**', + * certs: [{ + * certPath: './cert.pem', + * keyPath: './key.pem', + * passphase: 'mysecretpassword', + * }], + * }], + * }, + * }, + * ] + * }); + * ``` + * + */ + clientCertificates: ClientCertificate[] | undefined; /** * Specify device scale factor (can be thought of as dpr). Defaults to `1`. Learn more about * [emulating devices with device scale factor](https://playwright.dev/docs/emulation#devices). diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index e54e89ddcf..b8cccde6ef 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -576,6 +576,15 @@ export type PlaywrightNewRequestParams = { userAgent?: string, ignoreHTTPSErrors?: boolean, extraHTTPHeaders?: NameValue[], + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], httpCredentials?: { username: string, password: string, @@ -600,6 +609,15 @@ export type PlaywrightNewRequestOptions = { userAgent?: string, ignoreHTTPSErrors?: boolean, extraHTTPHeaders?: NameValue[], + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], httpCredentials?: { username: string, password: string, @@ -944,6 +962,15 @@ export type BrowserTypeLaunchPersistentContextParams = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -1017,6 +1044,15 @@ export type BrowserTypeLaunchPersistentContextOptions = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -1131,6 +1167,15 @@ export type BrowserNewContextParams = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -1190,6 +1235,15 @@ export type BrowserNewContextOptions = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -1252,6 +1306,15 @@ export type BrowserNewContextForReuseParams = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -1311,6 +1374,15 @@ export type BrowserNewContextForReuseOptions = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -4558,6 +4630,15 @@ export type AndroidDeviceLaunchBrowserParams = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, @@ -4615,6 +4696,15 @@ export type AndroidDeviceLaunchBrowserOptions = { height: number, }, ignoreHTTPSErrors?: boolean, + clientCertificates?: { + url: string, + certs: { + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + }[], javaScriptEnabled?: boolean, bypassCSP?: boolean, userAgent?: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 6b754c3619..28df319e18 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -433,6 +433,21 @@ ContextOptions: width: number height: number ignoreHTTPSErrors: boolean? + clientCertificates: + type: array? + items: + type: object + properties: + url: string + certs: + type: array + items: + type: object + properties: + cert: binary? + key: binary? + passphrase: string? + pfx: binary? javaScriptEnabled: boolean? bypassCSP: boolean? userAgent: string? @@ -673,6 +688,21 @@ Playwright: extraHTTPHeaders: type: array? items: NameValue + clientCertificates: + type: array? + items: + type: object + properties: + url: string + certs: + type: array + items: + type: object + properties: + cert: binary? + key: binary? + passphrase: string? + pfx: binary? httpCredentials: type: object? properties: diff --git a/tests/assets/client-certificates/README.md b/tests/assets/client-certificates/README.md new file mode 100644 index 0000000000..499a41e115 --- /dev/null +++ b/tests/assets/client-certificates/README.md @@ -0,0 +1,59 @@ +# Client Certificate test-certificates + +## Server + +```bash +openssl req \ + -x509 \ + -newkey rsa:4096 \ + -keyout server/server_key.pem \ + -out server/server_cert.pem \ + -nodes \ + -days 365 \ + -subj "/CN=localhost/O=Client\ Certificate\ Demo" +``` + +## Trusted client-certificate (server signed/valid) + +``` +mkdir -p client/trusted +# generate server-signed (valid) certifcate +openssl req \ + -newkey rsa:4096 \ + -keyout client/trusted/key.pem \ + -out client/trusted/csr.pem \ + -nodes \ + -days 365 \ + -subj "/CN=Alice" + +# sign with server_cert.pem +openssl x509 \ + -req \ + -in client/trusted/csr.pem \ + -CA server/server_cert.pem \ + -CAkey server/server_key.pem \ + -out client/trusted/cert.pem \ + -set_serial 01 \ + -days 365 +``` + +## Self-signed certificate (invalid) + +``` +mkdir -p client/self-signed +openssl req \ + -newkey rsa:4096 \ + -keyout client/self-signed/key.pem \ + -out client/self-signed/csr.pem \ + -nodes \ + -days 365 \ + -subj "/CN=Bob" + +# sign with self-signed/key.pem +openssl x509 \ + -req \ + -in client/self-signed/csr.pem \ + -signkey client/self-signed/key.pem \ + -out client/self-signed/cert.pem \ + -days 365 +``` diff --git a/tests/assets/client-certificates/client/self-signed/cert.pem b/tests/assets/client-certificates/client/self-signed/cert.pem new file mode 100644 index 0000000000..4cca9cc671 --- /dev/null +++ b/tests/assets/client-certificates/client/self-signed/cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEyzCCArOgAwIBAgIUBO3H8U57HcsnWmwP2Xm7wDgHnmkwDQYJKoZIhvcNAQEL +BQAwDjEMMAoGA1UEAwwDQm9iMB4XDTI0MDYyNDEyMzEyOVoXDTI1MDYyNDEyMzEy +OVowDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAugNWP3UMpf33fOykU7vrIMv1LS32KZjloQnW6o6t5kjt/Yr3JDKnEuMMshpC +4YGTEBJ0y5errBbCP3bQY62SnARAgXAaatRqGF+rqiKEnTDCXQo6ex27nna2LgjU +JjpU7uqW3fIbECUDIG2zXpE+9VJ1CWViGrRFN7M57zoJ4SEutyMSu9Z8qeZ+/sHm +UjqLgimjxx5KSLe6wx9BI7266cgVLbGLMEzZYvDz3+TznzcfTiUs0h74LBDlQYjh +76f2td3gZZ7jii2toJgV+E72tjIOANkLVtIVgx0le/4dgwgoIEkjTm5bvbTijIuh +O01TYjoV+r+aeTnl+uGGLGTG9KtMj+HOpDxAapFxLS8sM2kn9bBu+6XdP2sa89PX +7AmZ1VjsMFILM20kvbOmfnKrVKTszo68LBeMIcYfR60KaPc/ZglvHFE7lxB4rOBz +4QNCNMDciEh6npkntjGnO6q01DhYdBpccW65mAqX0+LrkjpUcBneYoTyd0Izd5M1 +QNm6WaL2Uye+wmNBIptq8I0A6PiWdVbef3xcF70I9JTJHFODHjs1PDov/llZpUyh +cNx8WC+y/nPdt+XS0BR/ap0QWJ66CNa5tsLs6txdx6Aoa373bq6HlY/RRlvkhPxr +OOKfon7qKQCQGPTSuSvw8qssehcko79chpaYTPJKaJZRK0sCAwEAAaMhMB8wHQYD +VR0OBBYEFBcJPRVNgMzwnvzLnNpsVlQySkWpMA0GCSqGSIb3DQEBCwUAA4ICAQAu +KjNiFaTPS4vRwva/kNY5MUBcTH0U3BGPOlWyvrudc3FM8+X7OhGpBLLAGmk/H+KT +Kzd2B3btr1AAxprLMehxXVlSF0+5A2DLyNDq2wwoI+V2APpeeGA4cCRsL91ZqCH8 +57T/XpRWKKorlY17yfxO9GUFJGdl1Oki3wvOOaXWcuxUb7nHOjI7oguUcR4jfdIR +WfeUe3P+Y8TLVe2WRdJYEdRfpKN2T+8dGNdVHJ3GokgoLQsj8wpKHM0cd81f+mMf +jKfP++mR9w+UCRKgWxbCTFMhZvz4BKwpmLI0mLphumiWkfFJPrxTUmx/0JFfOLdi +pDHqd5JfSeiBm+hKTWlY/kc7rPSe+VYiXM+Zs+4EIqjowjiRixW+lRU4lf+7ZDlm +v+mi4C4JLHW6I7H085GlM+A6BmEPPBRNx2OOPEIqqLCRkJMi3RmS6X6jkAcCdhmn +MsxEKjgG8dJn+kfDGxD1Vfz5PjqzFhHPyahdkXo2br8P3RH7jPY9lb6nvAKupHlV +GgKJodeibtbZl1eeCrwdHjrawmQ7VhyQ91Dk7PP+z9h6fQqFxxULYP6WC0P7KVfD +EBauab6AJdpwKPqsRL9w87yjl2481aeMdtezYjE1HuWTlObq9YcArbpE8Rm5gLb1 +9bZ5aZKz62m+O3LZMbsMDusgTeyZFMlB0yA32q+SGg== +-----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/self-signed/csr.pem b/tests/assets/client-certificates/client/self-signed/csr.pem new file mode 100644 index 0000000000..5fc8eb0cf8 --- /dev/null +++ b/tests/assets/client-certificates/client/self-signed/csr.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC +Ag8AMIICCgKCAgEAugNWP3UMpf33fOykU7vrIMv1LS32KZjloQnW6o6t5kjt/Yr3 +JDKnEuMMshpC4YGTEBJ0y5errBbCP3bQY62SnARAgXAaatRqGF+rqiKEnTDCXQo6 +ex27nna2LgjUJjpU7uqW3fIbECUDIG2zXpE+9VJ1CWViGrRFN7M57zoJ4SEutyMS +u9Z8qeZ+/sHmUjqLgimjxx5KSLe6wx9BI7266cgVLbGLMEzZYvDz3+TznzcfTiUs +0h74LBDlQYjh76f2td3gZZ7jii2toJgV+E72tjIOANkLVtIVgx0le/4dgwgoIEkj +Tm5bvbTijIuhO01TYjoV+r+aeTnl+uGGLGTG9KtMj+HOpDxAapFxLS8sM2kn9bBu ++6XdP2sa89PX7AmZ1VjsMFILM20kvbOmfnKrVKTszo68LBeMIcYfR60KaPc/Zglv +HFE7lxB4rOBz4QNCNMDciEh6npkntjGnO6q01DhYdBpccW65mAqX0+LrkjpUcBne +YoTyd0Izd5M1QNm6WaL2Uye+wmNBIptq8I0A6PiWdVbef3xcF70I9JTJHFODHjs1 +PDov/llZpUyhcNx8WC+y/nPdt+XS0BR/ap0QWJ66CNa5tsLs6txdx6Aoa373bq6H +lY/RRlvkhPxrOOKfon7qKQCQGPTSuSvw8qssehcko79chpaYTPJKaJZRK0sCAwEA +AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQAFGvffXDPy6TKwrdQShBTCd2sH6lTW7K3D +2AzL8ZWLC20QmQtz3YbyDODnauo/8EKGvCpvTHZZoPQbmKgFro9vbLMp++2npvY8 +9VD7FENkfnlyYspiaLicmmV+wN8MwDgKhnZYM41GnkxUrDCj8iOmFK3bmvvhOD1H +1SCWhnWG6VdaWhIbE0faXzK7+0WhHILaWxZTgVAHKavQ3APYEh2+s1UwseugNBsL +1p+ldROFK/SIisyzi009a24/Ccan9peJbmmWKKUF7oGqoIYfoeDMPpCG+rWxzpaG +1S4DNAVgupLpX/oapzBZs3+mXCfh9NSkjKW0Z+M2yO0qOrhdnO2tdpQ2693JoxsG +mFhNtfno+22pBHUY6w9jIFIxNbNMWlS54fZoCg7pgJ0YmME7zuoeu2IUeDb6MSoE +fd4S+iGdIhyb83yafe9ws+6u7Es7/ivrU2E5E9dtae11liuGQvYxqIpR5ArGndwr +Kv3czIKmkR6R8a7UPRXRona5do8L4uhBYz8SnEaH3ClW80NttDO5F3geHM+znTCS +uc3ruF3fl4h6rh7khDBY7c62jA1F8f+plAgjrCAI6ZSyNUdAMpIzwayGE+i/7M92 +ivWpiPd4neB3ZSP3T6fNlz2SjnmTMwvGHcqvnYAohdqMqrn9MzA4lLX+/Qg+CeRS +m3dDmOG0Kw== +-----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/self-signed/key.pem b/tests/assets/client-certificates/client/self-signed/key.pem new file mode 100644 index 0000000000..0d85c0096a --- /dev/null +++ b/tests/assets/client-certificates/client/self-signed/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +n8nUUVeSi2z5bdctVUaQjQqc6zKI +-----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/client/trusted/cert.pem b/tests/assets/client-certificates/client/trusted/cert.pem new file mode 100644 index 0000000000..e8216491b9 --- /dev/null +++ b/tests/assets/client-certificates/client/trusted/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFAzCCAuugAwIBAgIBATANBgkqhkiG9w0BAQsFADA2MRIwEAYDVQQDDAlsb2Nh +bGhvc3QxIDAeBgNVBAoMF0NsaWVudCBDZXJ0aWZpY2F0ZSBEZW1vMB4XDTI0MDYy +NDEyMzEyM1oXDTI1MDYyNDEyMzEyM1owEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC4FGkFO0MTc2xgk8N9lrE8JW5MJQyx +me+h4HUqc+FwOddY3EH4akCKuvgmHP8bmB76yg7hsWiMHzNW4dqb+ZpxGaA1FInY +2gS/GHPaKmVH+lqtNeIAczu3gNo5yBEVp3GQmv0GGxwp5/ugOu+INfPtPqHEKdRm ++Ti7uXvMAeohh4raAv3hx3N2x+AnXpEa4ocz8fXYh4y3puk86KCA9Zq5O7IwB6QC +qBxSHdtamniqIzuevouXGd//ZCqjK8P57HqVZiBep/wByQlAN6WTRtjaPwPRT97A +YM0ayH91aaQ8BtqSWu2LoXkILV+SRSqLRqbyB4TUaKg0RaG4Fk04vg4xjOnjUNcZ +TEAKmAozQXTtazYqXGAgd5VENKk93wK1/B3zGnQBBmyyFsGbkCRZLPkxhXMIhoDc +ZcS4qUqVfA+gtv6+KlgiPBD4D5lGEKFPVV03T0v8+elq61+RsWHVa8VO3afFqFvT +i8yYwDS8udqGaE5EN7lyl1cYJwafH2mdiNdnQc4/Wp/zqgyPALjCbatyu3PAbTfK +DleGBRBfxiLKUB39Jo2bafClqRYQnCdQA1vQX0OF2Q9ZTrsLhskVsVQZN5C/Yopc +RsMpDWq7HPAP3WndVeawCBg6QFwnR0z6nmfi+UDz1hiSyl6bf3JJs3Nigll1W7UU +J56zv89nS8M4xwIDAQABo0IwQDAdBgNVHQ4EFgQUo4T4xMvLao//h+6AcFhXMe5l +iGkwHwYDVR0jBBgwFoAUCtaPQfMWTFHrjCUREWMGqIWYC/MwDQYJKoZIhvcNAQEL +BQADggIBABoH28SHV5P1xsrsBtEx8ab0BhSPHuzEI8ytA0KgwojrXRKX5MsLbLyY +9lFGH7fu+8830oDnfIe7989JMDGtccLO6DXWoWZjBVTkHPNjPSjF6TrhfqjeqNJH +NsPW9XUn3UB2hEYJ1nLLAzYizXYVSfL5HExF6Ph7fAzP6AvSZPS6AsVHXh2/JPvz +E/JzbeEewDsv3mB+DqUDne7Ukj/IIegGVx190KXwyLmgXlFd69R9GEVEO4FcXqpA +NCCcCmnTzBtzrgYVyWB7vDigMoD2bKWlQXwCgthR6DKywtKpSm9UWilghbgkuzam +toAOzIyZFGBt+44MnV/486XhbbIXzlixhSZPgSeOKM1tI69OOZ7ilvR6VxvPOrws +ZsUJDWFYYXq++8uzWRe+nHglXEQc+4CWbqqhjnncqh94/jLpHNS+BcrgDK3BuVrq +Skj8bNq8xnXgUPr+i9Rzmd4lRefKyjVehbRNXj4gp1Ap/TDQbl/A5Vdb31pTLYvO +SvDQXImm5HqV5KyddvKnWCIYVD7ATzcvjLqJl52ykDz4eRorzWQ41K5uwd0SxEyB +JZ0MesoWyEaWRlpjzqCNxCwO7Pp6RLMxDvC4/lkRt+7HxLLDWaw7bHplFm24GAYm +ebC5La1SdbYKC8AahYwT5Ppn6ThiSqV1m3GsuzgBLjfkYMFGjRQQ +-----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/trusted/csr.pem b/tests/assets/client-certificates/client/trusted/csr.pem new file mode 100644 index 0000000000..f6ff4c6202 --- /dev/null +++ b/tests/assets/client-certificates/client/trusted/csr.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEVTCCAj0CAQAwEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC4FGkFO0MTc2xgk8N9lrE8JW5MJQyxme+h4HUqc+FwOddY +3EH4akCKuvgmHP8bmB76yg7hsWiMHzNW4dqb+ZpxGaA1FInY2gS/GHPaKmVH+lqt +NeIAczu3gNo5yBEVp3GQmv0GGxwp5/ugOu+INfPtPqHEKdRm+Ti7uXvMAeohh4ra +Av3hx3N2x+AnXpEa4ocz8fXYh4y3puk86KCA9Zq5O7IwB6QCqBxSHdtamniqIzue +vouXGd//ZCqjK8P57HqVZiBep/wByQlAN6WTRtjaPwPRT97AYM0ayH91aaQ8BtqS +Wu2LoXkILV+SRSqLRqbyB4TUaKg0RaG4Fk04vg4xjOnjUNcZTEAKmAozQXTtazYq +XGAgd5VENKk93wK1/B3zGnQBBmyyFsGbkCRZLPkxhXMIhoDcZcS4qUqVfA+gtv6+ +KlgiPBD4D5lGEKFPVV03T0v8+elq61+RsWHVa8VO3afFqFvTi8yYwDS8udqGaE5E +N7lyl1cYJwafH2mdiNdnQc4/Wp/zqgyPALjCbatyu3PAbTfKDleGBRBfxiLKUB39 +Jo2bafClqRYQnCdQA1vQX0OF2Q9ZTrsLhskVsVQZN5C/YopcRsMpDWq7HPAP3Wnd +VeawCBg6QFwnR0z6nmfi+UDz1hiSyl6bf3JJs3Nigll1W7UUJ56zv89nS8M4xwID +AQABoAAwDQYJKoZIhvcNAQELBQADggIBAG7XNJcmByEF0rvSV6bY28PrfirPt09K +dTsKNlv9a798k9eq/vqpnVrNslEFj/SVTBPl5r5FIYnueNiO54VlA6nJ+1yRSlvI +2SGvgCRoD4xNcMyJgzMwmxovhNRHdheRP+A82EUfgoT8/HCm0UasTw1PcZKokprb +T93pie4iV3CWBtVHd9/hZXsMnumT/LIbnUdCOkAsy7cIVsxOHNLZociJeV3LxIgL +B5HQdDHPd9i4C6zFgZmm7imrzvFrFk+ksUx1jUeMTXCCvwVyj5cPRbxKduFLCOIA +tP61RVeXt6ru5IZl+2n8GApKT9zkPhiGNmCdG54Z7Yc62fvijneeOlKPAwWRbxEq +smdW0fOaf73n0eguZ8ujk2ZLVDb2knqk5MoPTuEaSea/haVg/vgP8O5sFwjOcJ6P +D1flxGJafOGF0B0hnZXqFjUV2Ty+R5iSk7vaLiwc9asMybqyuEVv5q2/6gmoawt1 +6srZq/Z3jyaseOzYgeOugZcKZyZNpDgS5ZIq+iKu5fP5gcnmvhZca4fFqd2jYthO +v6Xrd5tMcgpcZSRWAezh4p0AICnmdTlpQNcw6rdfr7RAT2OwxQmqW/SbniJCr/Nn +W4v6KhZ7mYBrYt7ywVXuhZKmubwShZTprSEsQko5Tujk41/kLJl0C3kIdkMvMWzF +gKrkAP6tuQQ7 +-----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/trusted/key.pem b/tests/assets/client-certificates/client/trusted/key.pem new file mode 100644 index 0000000000..109e06e94b --- /dev/null +++ b/tests/assets/client-certificates/client/trusted/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +XJVwXoZ9PUMxrfgEI9YmZdHOSJdU9Qc= +-----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/server/server_cert.pem b/tests/assets/client-certificates/server/server_cert.pem new file mode 100644 index 0000000000..3ce410b7c2 --- /dev/null +++ b/tests/assets/client-certificates/server/server_cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFTTCCAzWgAwIBAgIUWdMb2scNR1R1TxDuI4RrFt7mGOwwDQYJKoZIhvcNAQEL +BQAwNjESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYDVQQKDBdDbGllbnQgQ2VydGlm +aWNhdGUgRGVtbzAeFw0yNDA2MjQxMjMxMTNaFw0yNTA2MjQxMjMxMTNaMDYxEjAQ +BgNVBAMMCWxvY2FsaG9zdDEgMB4GA1UECgwXQ2xpZW50IENlcnRpZmljYXRlIERl +bW8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9VMWaGggaYorXO5IF +qj3s+W4nvrzzi3LHYjnugFnUAruOYdXbBboieSR2wnJeITyVp0MbVEsrcfwsCmlB +RP3ehheJqW2GtDZ8eFTzZ/r2Npl/TQgGn5/R+HYe9NkXfWAT+VanURcNrP98eSRI +zDS/Rx5ZgG9/iYRbB/Lcd+Stkp2UTLDlMTvAv8CFwFLE1bwHmcAqYBMMqEpKh3Z5 +oshmOSQz28A5o3UK2XCJptywV0lbugs0bBSQiayBs2BzMLbUFiWyh45LEvuWLYpv +3FKQwMJi+ZBZ0lOXjFOfW0TXlZRLMSrSfnOP0dq8x2QAFEIIKGwgbZXyCLP+BVLD +IJ39+AadSjXaJ+12FkOE8ETe6EEIKoydwzFnxtndMvflKYFILRF8vJp44MTJ/cZ2 +9ZSY7QPVoSEK9KjtedlmYSvyWYYxtoWN5zR/K7Oyb/lFy45oKU75LuWt5qx9X2eA +uScy8SWgo17SN1IF1OdOYjExRXmjnHcQJIAPLfX6hkheee9S8uiWy8a42iBJW4ZV +QSsGtbMb3BojjykZYkhTdjmjvjy16IqlJ4JEcRGKxuuvmlCXOs33D1ot7xb1GHwX +oLvXo2IOSZAjDOSHCOFdvg9h0LcyEeGTeLHDeNrxRUo1z+/zRU3NE8A4UDyKP/rf +ZTVphGSNoyvo0UwyIxFQutZK6QIDAQABo1MwUTAdBgNVHQ4EFgQUCtaPQfMWTFHr +jCUREWMGqIWYC/MwHwYDVR0jBBgwFoAUCtaPQfMWTFHrjCUREWMGqIWYC/MwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEANgmi2fMKEFlIyKCz+4Oa +GnOKIAACR0+yk2G+nrGxkkmT2bIiibcbYtLmfB3gQyA6O3qoi94t66QS83fIvRA5 +FHRs/wWGzKHcDCaJApxVhIN7V+480NB2W7VS2KSQh9EQQ9dziXS6xftycz7LeLR0 +WYcvojLCd1tkCFFnaN0cRjqjMdfGfsONtvc5KFGoySNs3r65ckQct7e34BELMBjv +Zd+myNNnw6i8cnHFcnJpSYajX3dJrmaMtapjORjTjlpsjF7oYOx2dANHzOJ8sVqo +3cElK/Ou88S/EHX/xOXx/jncz2OPGGL/8UEDh9Z2Co4/p1TYaxMHOBTlWlqjERWZ +AmA9v9lU6gn+o5ITz0wm3M+StxF3DU9nCu3/FCezObNPM1sILRZoYmx8Ok85G/0l +DeYgxE4je4CCh1rxyRZClRljbimE/FBbw/Ui6zrHiTAABfwVirY+wyDvPMiYhGmt +sOfEveXVn5aX7sW1iOChZP3p2KuL5a19T5sl7+mev6Z1sea3IovYeXKsRFEVhTUU +7533qt9lknpip9I27BT8aJKWUGP3GWXDs/DVoSkygnvbDDJfLK5Wqp8EPZCLyS1K +L/lWamgel5BgCwb091P2rNT7Lu+XGhJOcfyFk1FAwiZ30yi3As4W9HXvdh3CNm5x +55g7Z6Xmxn6xdtviPZmBPZU= +-----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/server/server_key.pem b/tests/assets/client-certificates/server/server_key.pem new file mode 100644 index 0000000000..ff82b6ce40 --- /dev/null +++ b/tests/assets/client-certificates/server/server_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +m3Ea0Y3lf0rJCBJ10pFBP3z8orJyWkI= +-----END PRIVATE KEY----- diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts new file mode 100644 index 0000000000..fc964116d1 --- /dev/null +++ b/tests/library/client-certificates.spec.ts @@ -0,0 +1,264 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import { expect, playwrightTest as base } from '../config/browserTest'; +import type net from 'net'; +import type { BrowserContextOptions } from 'packages/playwright-test'; +const { createHttpsServer } = require('../../packages/playwright-core/lib/utils'); + +const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: string }>({ + serverURL: async ({ asset }, use) => { + const server = createHttpsServer({ + key: fs.readFileSync(asset('client-certificates/server/server_key.pem')), + cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')), + ca: [ + fs.readFileSync(asset('client-certificates/server/server_cert.pem')), + ], + requestCert: true, + rejectUnauthorized: false, + }, (req, res) => { + const cert = (req.socket as import('tls').TLSSocket).getPeerCertificate(); + if ((req as any).client.authorized) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`); + } else if (cert.subject) { + res.writeHead(403, { 'Content-Type': 'text/html' }); + res.end(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`); + } else { + res.writeHead(401, { 'Content-Type': 'text/html' }); + res.end(`Sorry, but you need to provide a client certificate to continue.`); + } + }); + process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem'); + await new Promise(f => server.listen(0, 'localhost', () => f())); + await use(`https://localhost:${(server.address() as net.AddressInfo).port}/`); + await new Promise(resolve => server.close(() => resolve())); + }, + serverURLRewrittenToLocalhost: async ({ serverURL, browserName }, use) => { + const parsed = new URL(serverURL); + parsed.hostname = 'local.playwright'; + const shouldRewriteToLocalhost = browserName === 'webkit' && process.platform === 'darwin'; + await use(shouldRewriteToLocalhost ? parsed.toString() : serverURL); + } +}); + +test.skip(({ mode }) => mode !== 'default'); + +const kDummyFileName = __filename; +const kValidationSubTests: [BrowserContextOptions, string][] = [ + [{ clientCertificates: [{ url: 'test', certs: [] }] }, 'No certs specified for url: test'], + [{ clientCertificates: [{ url: 'test', certs: [{}] }] }, 'None of cert, key, passphrase or pfx is specified'], + [{ + clientCertificates: [{ + url: 'test', + certs: [{ + certPath: kDummyFileName, + keyPath: kDummyFileName, + pfxPath: kDummyFileName, + passphrase: kDummyFileName, + }] + }] + }, 'pfx is specified together with cert, key or passphrase'], + [{ + proxy: { server: 'http://localhost:8080' }, + clientCertificates: [{ + url: 'test', + certs: [{ + certPath: kDummyFileName, + keyPath: kDummyFileName, + }] + }] + }, 'Cannot specify both proxy and clientCertificates'], +]; + +test.describe('fetch', () => { + test('validate input', async ({ playwright }) => { + for (const [contextOptions, expected] of kValidationSubTests) + await expect(playwright.request.newContext(contextOptions)).rejects.toThrow(expected); + }); + + test('should fail with no client certificates provided', async ({ playwright, serverURL }) => { + const request = await playwright.request.newContext(); + const response = await request.get(serverURL); + expect(response.status()).toBe(401); + expect(await response.text()).toBe('Sorry, but you need to provide a client certificate to continue.'); + await request.dispose(); + }); + + test('should keep supporting http', async ({ playwright, server, asset }) => { + const request = await playwright.request.newContext({ + clientCertificates: [{ + url: server.PREFIX, + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + const response = await request.get(server.PREFIX + '/one-style.html'); + expect(response.url()).toBe(server.PREFIX + '/one-style.html'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('
hello, world!
'); + await request.dispose(); + }); + + test('should throw with untrusted client certs', async ({ playwright, serverURL, asset }) => { + const request = await playwright.request.newContext({ + clientCertificates: [{ + url: serverURL, + certs: [{ + certPath: asset('client-certificates/client/self-signed/cert.pem'), + keyPath: asset('client-certificates/client/self-signed/key.pem'), + }], + }], + }); + const response = await request.get(serverURL); + expect(response.url()).toBe(serverURL); + expect(response.status()).toBe(403); + expect(await response.text()).toBe('Sorry Bob, certificates from Bob are not welcome here.'); + await request.dispose(); + }); + + test('pass with trusted client certificates', async ({ playwright, serverURL, asset }) => { + const request = await playwright.request.newContext({ + clientCertificates: [{ + url: serverURL, + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + const response = await request.get(serverURL); + expect(response.url()).toBe(serverURL); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('Hello Alice, your certificate was issued by localhost!'); + await request.dispose(); + }); + + test('should work in the browser with request interception', async ({ browser, playwright, serverURL, asset }) => { + const request = await playwright.request.newContext({ + clientCertificates: [{ + url: serverURL, + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + const page = await browser.newPage({ ignoreHTTPSErrors: true }); + await page.route('**/*', async route => { + const response = await request.fetch(route.request()); + await route.fulfill({ response }); + }); + await page.goto(serverURL); + await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible(); + await page.close(); + await request.dispose(); + }); +}); + + +test.describe('browser', () => { + test('validate input', async ({ browser }) => { + for (const [contextOptions, expected] of kValidationSubTests) + await expect(browser.newContext(contextOptions)).rejects.toThrow(expected); + }); + + test('should keep supporting http', async ({ browser, server, asset }) => { + const page = await browser.newPage({ + clientCertificates: [{ + url: server.PREFIX, + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + await page.goto(server.PREFIX + '/one-style.html'); + await expect(page.getByText('hello, world!')).toBeVisible(); + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); + await page.close(); + }); + + test('should fail with no client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => { + const page = await browser.newPage({ + clientCertificates: [{ + url: 'https://not-matching.com', + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + await page.goto(serverURLRewrittenToLocalhost); + await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible(); + await page.close(); + }); + + test('should fail with self-signed client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => { + const page = await browser.newPage({ + clientCertificates: [{ + url: serverURLRewrittenToLocalhost, + certs: [{ + certPath: asset('client-certificates/client/self-signed/cert.pem'), + keyPath: asset('client-certificates/client/self-signed/key.pem'), + }], + }], + }); + await page.goto(serverURLRewrittenToLocalhost); + await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible(); + await page.close(); + }); + + test('should pass with matching certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => { + const page = await browser.newPage({ + clientCertificates: [{ + url: serverURLRewrittenToLocalhost, + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + await page.goto(serverURLRewrittenToLocalhost); + await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible(); + await page.close(); + }); + + test.describe('persistentContext', () => { + test('validate input', async ({ launchPersistent }) => { + test.slow(); + for (const [contextOptions, expected] of kValidationSubTests) + await expect(launchPersistent(contextOptions)).rejects.toThrow(expected); + }); + + test('should pass with matching certificates', async ({ launchPersistent, serverURLRewrittenToLocalhost, asset }) => { + const { page } = await launchPersistent({ + clientCertificates: [{ + url: serverURLRewrittenToLocalhost, + certs: [{ + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + }], + }); + await page.goto(serverURLRewrittenToLocalhost); + await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible(); + }); + }); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 67b355b79f..3a42d27f83 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -152,6 +152,7 @@ export type Fixtures; type ColorScheme = Exclude; +type ClientCertificate = Exclude[0]; type ExtraHTTPHeaders = Exclude; type Proxy = Exclude; type StorageState = Exclude; @@ -209,6 +210,7 @@ export interface PlaywrightTestOptions { acceptDownloads: boolean; bypassCSP: boolean; colorScheme: ColorScheme; + clientCertificates: ClientCertificate[] | undefined; deviceScaleFactor: number | undefined; extraHTTPHeaders: ExtraHTTPHeaders | undefined; geolocation: Geolocation | undefined;