diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 444a86efb5..aef805798d 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -40,7 +40,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'; +import { clientCertificatesToTLSOptions, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; type FetchRequestOptions = { userAgent: string; @@ -452,7 +452,7 @@ export abstract class APIRequestContext extends SdkObject { body.on('data', chunk => chunks.push(chunk)); body.on('end', notifyBodyFinished); }); - request.on('error', reject); + request.on('error', error => reject(rewriteOpenSSLErrorIfNeeded(error))); const disposeListener = () => { reject(new Error('Request context disposed.')); diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 13590579fe..17a4a708a6 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -22,7 +22,7 @@ import fs from 'fs'; import tls from 'tls'; import stream from 'stream'; import { createSocket, createTLSSocket } from '../utils/happy-eyeballs'; -import { ManualPromise } from '../utils'; +import { ManualPromise, rewriteErrorMessage } from '../utils'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy'; import type * as channels from '@protocol/channels'; @@ -150,8 +150,9 @@ class SocksProxyConnection { }; const handleError = (error: Error) => { - debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`); - const responseBody = 'Playwright client-certificate error: ' + error.message; + error = rewriteOpenSSLErrorIfNeeded(error); + debugLogger.log('client-certificates', `error when connecting to target: ${error.message.replaceAll('\n', ' ')}`); + const responseBody = 'Playwright client-certificate error: ' + error.message.replaceAll('\n', '
'); if (internalTLS?.alpnProtocol === 'h2') { // This method is available only in Node.js 20+ if ('performServerHandshake' in http2) { @@ -297,3 +298,17 @@ export function clientCertificatesToTLSOptions( function rewriteToLocalhostIfNeeded(host: string): string { return host === 'local.playwright' ? 'localhost' : host; } + +export function rewriteOpenSSLErrorIfNeeded(error: Error): Error { + if (error.message !== 'unsupported') + return error; + return rewriteErrorMessage(error, [ + 'Unsupported TLS certificate.', + 'Node.js (OpenSSL) has deprecated certifiates with the security algorithm of the given client-certifiate.', + 'To fix this issue, you need to modernise the certificates by running the following command:', + 'openssl pkcs12 -in oldPfxFile.pfx -nodes -legacy -out decryptedPfxFile.tmp', + 'openssl pkcs12 -in decryptedPfxFile.tmp -export -out newPfxFile.pfx', + 'Then, you can use the newPfxFile.pfx in your client-certificates configuration.', + 'For more information, please refer to OpenSSL: https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md#the-legacy-provider', + ].join('\n')); +} \ No newline at end of file diff --git a/tests/assets/client-certificates/client/trusted/cert-legacy.pfx b/tests/assets/client-certificates/client/trusted/cert-legacy.pfx new file mode 100644 index 0000000000..9f06aa35c8 Binary files /dev/null and b/tests/assets/client-certificates/client/trusted/cert-legacy.pfx differ diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index d54281b143..fd54f020a4 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -169,6 +169,51 @@ test.describe('fetch', () => { await request.dispose(); }); + test('pass with trusted client certificates in pfx format', async ({ playwright, startCCServer, asset }) => { + const serverURL = await startCCServer(); + const request = await playwright.request.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + pfxPath: asset('client-certificates/client/trusted/cert.pfx'), + passphrase: 'secure' + }], + }); + const response = await request.get(serverURL); + expect(response.url()).toBe(serverURL); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!'); + await request.dispose(); + }); + + test('should throw a http error if the pfx passphrase is incorect', async ({ playwright, startCCServer, asset, browserName }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); + const request = await playwright.request.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + pfxPath: asset('client-certificates/client/trusted/cert.pfx'), + passphrase: 'this-password-is-incorrect' + }], + }); + await expect(request.get(serverURL)).rejects.toThrow('mac verify failure'); + await request.dispose(); + }); + + test('should pass with matching certificates in legacy pfx format', async ({ playwright, startCCServer, asset, browserName }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); + const request = await playwright.request.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'), + passphrase: 'secure' + }], + }); + await expect(request.get(serverURL)).rejects.toThrow('Unsupported TLS certificate'); + await request.dispose(); + }); + test('should work in the browser with request interception', async ({ browser, playwright, startCCServer, asset }) => { const serverURL = await startCCServer(); const request = await playwright.request.newContext({ @@ -272,6 +317,21 @@ test.describe('browser', () => { await page.close(); }); + test('should pass with matching certificates in legacy pfx format', async ({ browser, startCCServer, asset, browserName }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); + const page = await browser.newPage({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'), + passphrase: 'secure' + }], + }); + await page.goto(serverURL); + await expect(page.getByText('Unsupported TLS certificate.')).toBeVisible(); + await page.close(); + }); + test('should throw a http error if the pfx passphrase is incorect', async ({ browser, startCCServer, asset, browserName }) => { const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); const page = await browser.newPage({