From 71e614dc5ad667efdb4fe7c8f60e161aee3a94d8 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 5 Aug 2024 10:54:33 +0200 Subject: [PATCH] fix(client-certificates): report error to the browser if incorrect passphrase (#32007) --- .../socksClientCertificatesInterceptor.ts | 57 +++++++++++------- tests/assets/client-certificates/README.md | 2 + .../client/trusted/cert.pfx | Bin 0 -> 4195 bytes tests/library/client-certificates.spec.ts | 30 +++++++++ 4 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 tests/assets/client-certificates/client/trusted/cert.pfx diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 131aa30de9..13590579fe 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -142,34 +142,14 @@ class SocksProxyConnection { dummyServer.emit('connection', this.internal); dummyServer.on('secureConnection', internalTLS => { debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`); - const tlsOptions: tls.ConnectionOptions = { - socket: this.target, - host: this.host, - port: this.port, - rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors, - ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'], - ...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, new URL(`https://${this.host}:${this.port}`).origin), - }; - if (!net.isIP(this.host)) - tlsOptions.servername = this.host; - const targetTLS = tls.connect(tlsOptions); - targetTLS.on('secureConnect', () => { - internalTLS.pipe(targetTLS); - targetTLS.pipe(internalTLS); - }); - - // Handle close and errors + let targetTLS: tls.TLSSocket | undefined = undefined; const closeBothSockets = () => { internalTLS.end(); - targetTLS.end(); + targetTLS?.end(); }; - internalTLS.on('end', () => closeBothSockets()); - targetTLS.on('end', () => closeBothSockets()); - - internalTLS.on('error', () => closeBothSockets()); - targetTLS.on('error', error => { + const handleError = (error: Error) => { debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`); const responseBody = 'Playwright client-certificate error: ' + error.message; if (internalTLS?.alpnProtocol === 'h2') { @@ -204,7 +184,38 @@ class SocksProxyConnection { ].join('\r\n')); closeBothSockets(); } + }; + + let secureContext: tls.SecureContext; + try { + secureContext = tls.createSecureContext(clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, new URL(`https://${this.host}:${this.port}`).origin)); + } catch (error) { + handleError(error); + return; + } + + const tlsOptions: tls.ConnectionOptions = { + socket: this.target, + host: this.host, + port: this.port, + rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors, + ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'], + servername: !net.isIP(this.host) ? this.host : undefined, + secureContext, + }; + + targetTLS = tls.connect(tlsOptions); + + targetTLS.on('secureConnect', () => { + internalTLS.pipe(targetTLS); + targetTLS.pipe(internalTLS); }); + + internalTLS.on('end', () => closeBothSockets()); + targetTLS.on('end', () => closeBothSockets()); + + internalTLS.on('error', () => closeBothSockets()); + targetTLS.on('error', handleError); }); }); } diff --git a/tests/assets/client-certificates/README.md b/tests/assets/client-certificates/README.md index b0ee78e707..7ee690de52 100644 --- a/tests/assets/client-certificates/README.md +++ b/tests/assets/client-certificates/README.md @@ -36,6 +36,8 @@ openssl x509 \ -out client/trusted/cert.pem \ -set_serial 01 \ -days 365 +# create pfx +openssl pkcs12 -export -out client/trusted/cert.pfx -inkey client/trusted/key.pem -in client/trusted/cert.pem -passout pass:secure ``` ## Self-signed certificate (invalid) diff --git a/tests/assets/client-certificates/client/trusted/cert.pfx b/tests/assets/client-certificates/client/trusted/cert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..48726d703a4f943d2873dcf1ccba6bf510c7216d GIT binary patch literal 4195 zcmai1S5Om-(xoJkK!nhH@6}KR1XMsEK#(F$2t|4qq(yq@C{0j`QlzN_DWMlBkq#=o zSH;jl2?C*B@BIIK-{XDQ+1Wi0yED6U_ACqywIw4ZhoPa2Ah1}RPTcWz(rcuJXebDP zhLXY1G?y?maO+}+@;+8399Sr=!Rn90H#R9{ zBGEuQo!m{PDJFCIvWbT)pMMGKxErsvr2hF9=}1{0yJCzi;Vq)jA`bYh`cm&3R((sf zG@>DBsK-%HNxhCSNG43FVF*@9(Xp6DqbfnV=p_3bDr^LHtg$;(V(s3IBW+yxY^@5D z<=Qnf#=*9T6YhfokosnNWdp^{&>n)y44xqch9=(Ta;qmTdupxY0Be)w9FjUuL`fl?_DO}+}+PCe+utI z`r>Pji*I(b93-?^S~5?aR(skVWy_7~q75(OTf*O2lHU{)q7R;QHf(I#b zT)mn1%W|35VA97ajedBaeY-&7O%QibZSI}WUKKM42KLS(KYP&Y@lSXX$Fb^OZgRHxMmG?%FHLYRfDyRf15}|MQ z1t}@m&oJP5bD?aA)^}nyc!>RhRpJbl{NocWO!%c2!gRYJ#h|H_XtYkNZbtb;1II9~AW1wfE<-k0x?d~`84XioU zt*=vX;)#&-l7sL8lO041+)d)zjXKwq)8yWbQu46WchtCv@k4Wq-x4sBJU4V`2`^(4 z^M~QPU?+nzhbB?wny=3_F`@|F6&n+&R zjQ7sX^&BM1dol$-D${^y}R2AdQ zsO#ddUdKFN*!e(_-%-FIt@gF#Z3>Q*%RbxPjl^l^FepYXl^BeN_4}lYs>cM6{+$y7 zC%E$)%e)I*<@LD9_Bo~quNQIrEphQ;S!}yn*o2SZfNC*K5DdSC^(-th@)Ppd8q=Ua zSF*FZEk3Igc-;+Z{3XW&CErD0QG7pBE+6I}5X5$(2u}BndCwLI;E0G#+ttY%oi0S} zJJE_Zu%aHEG8IMb;#K1Vo6^5~fXm`)YBMEru&?_cR-ZpsF0fZd*}$K<9o?Ic5ihup z@3l-9YOD6GHmdTBPc27K@w}Z{>sPWp*y>OJAV{hA(>euzVr(l#oTd+kXB+3Am_t0i znrT`p(Dvy1ki6+Ueqy*S&U`U;%C{<|Qiy|XLU8GA8)uEl2g*W`h8o;8AM91WtQaID z4R+cjQI7k>N`Iu%N9~VH8;pC8t?UL}%z8+@yDYe`KZqk^^0RJABTmYVnnb@klxsAK zlliKV=gg0LT{R~?{Vn1ZVS6!GKag=uPtCQzaZO=(3ig>(nvG4ABI?$dO7t>ewPZWj|i(&lU7+F;-LD$x0 zI@Ip}hozg-I-CKyV^LmGt{|4cKL?Lw_KcRLIodZcqjYE3PQjyc2X?Dm_r|Z0zMIhCL8+Taxc|0E2yUrtbGPKPq9n7NM3cR2%ZOKXKQF^I*80i0kM`Ks5FtBluc*F!gz+}ZBdD)-b$%@WCf>_8 zfS*n{tTpdyVl*)s2tkk+L9XTT(rw$)X9`!>F&T@XZ}%oime&L-owIUOv0})nO<~e{ zhELiQcUC84-8G*!PmhSvS=1KRcAhNIt4i5XDc%aIJe!bDo?$DUJe+(n50G?@zHJ_K znILA)t}2eDYF4fhjo8dTBD*5w$Vbom)g5;hjsHMy3c2|YH<{4XIqy7d^7Ae{?dGEZ z-B`b{SSoPQn!T$K6F^O@h-$LkQLk1@Y3}m*kaZRbpZX+7x~MP<-%M;Nf848cp3Pi1 zzN_7tj?+=%aM%86WZ_-wYcoqNpUykP9U`Un!}omAyh%xlNy$>0zO7O=Na>y8hYT)j zyjo}lUAw4^(Hn!0MrZM6)>N1B&pNEu{oMyp)6Skv{n*3x!eL{U3IMxLMYl9dL5elE zXL+A-tHV<3#jQvBkx?nkY{Re0Ig@%rXO_Z%?m4v+o zg?YB$tO`#{ATQWo=$cMw&&WFzYTXE3w>6~}p(!34b@^pnD-vKQy&m=@m^e)%sxkD$ zpC!gJ*ZiVp*iu4(IJ;^#TN~-UW5KoknIWK(SkCjt8YrLIX0NEbWVBwTq3QjQrtq_^ z)feVp!@Q}C(Uceutqd?XH_PX!xrL+F)gRag)dh_mSus%}d_}41n3AfV+)s0kviOwg zs(T*DTR2UdlHjDwW291{Y63ww1tz1#{DpL}X+~wy_|*}%9>GB71lX9inX9Z`c-sSz z6p>tLMP-R+q=dHbhMY4`_R^Ne2L=m3gznNWBbt!wRch&3D?`!0*6c95nhr+s#oS9b zN>1BeY9qQ-eM-rnmF~SCthe0C4}KUn#wV@!*0oBmE*rxSOL;OjAeUEzTK2*e!OLQ_ z3p(Q2lQiksjP}JlLt~0bwAl6sjR%ntd@`|d%Bx`yu`1-G}36+%~TcdpLGZgzg8 z!rQzs`Av;m*S6O;>_|CbQGdBGmkRf0o#J7ohx=a%ysw=ho5oI-KN&vhUVitGPa;x*+B$HZ>en3FnONn5^ramMNA zHB^$B_ZN%hXDOA@Gb|f9fRywu+v*L`!9sDjp@h1gd^+#+wV{K(j>fXnNaG`BZcY3- zB2Yi8ar|SUgU9o>pgkmE%hIQxmk zmkn>#9fq!f2ue9NUldoS49C-})W-h2>GO5Ij29IVV9uKxw0248c;`7G+nGy>IdH*1 z9g6l;%g*@7OQ2i7d!`d!IaW0PDxcr?s^C|*3ztf{3cPxksy*nk_rhKMk7r#O%3eZy zXB(xEZe1SLwbQ&?QLVUri1YMj@z@#&Rv78B-%YUvSj4`kxXRn1yeRP@JxFPK$$a)< z+%MZZm&ODuf@ymU;v&ivZ?~LT^>cWS0)B|jB|ihg-FDNEpgRros&aXT(JuzsQ&E;W|potpJARk=?-?UNKlA;DIV^{c(`n*IK$S4Ons(d_5^y zu`N8$MzIf{%N9A4seKz|x|!i1u3z(W>2VxOx?8hpy$SH#n7IpOe6+S-=fqqY;CwM zW0;#@dKap`QD|qC?AMb`DvC$v?+nGF%HSAm;K%jX-$!R3O_0QMGgGa#Q&+K(AARJ* zZico|=(Rk-Ur1d&caw(KCo?jO%D>IQNfgY)YEjnc|JAW4I^y=6wqO!)SVp*mS(KB^ z$i~wx{lP^8fTdWP@zG2&H#jU@W#+yKS_u50Os3&AfH>H#NsL8)otvR4(ai-Nr*;4Q z_K{a;lV{G|^jw!$@x=PfIs=rU{hQDC2!C7Y{7e&SU|0i%LUWg z`!y_Sy^rrGQF9cv1-?AJUX_)TZ!xJCQ=Xr!riU04d%9ya;a&0x#S~+>^OO{t*nBt^ zwks{77KhZ2cYj$KMDVOw$Vw_LRwCBqfF)N(Sv`Ga$<8g_v$o*=94kz;>q};0H3KfN zOrXxh4Z5l3MmdD>@$s(?oqc%Www6;(amdnWOt#U^YBU;r9>UWRLY!7xzW*-lH za`byhu)kFt{+_fS^P}z;Tycc5OI4Ja!4XD~8}IF?W1GG3BjBS}FMNQ?v@I^_1f%U@ zHwHi*qOC1l`^@eg=QN1zQ}tZ73-ICaVHipSZ!zu)0l);=n&y$a2RCNUyWL^|FDm$Sd)55H!Rr{(&^-63|U0?$nUst-PO+U-^FjCvgYl0%u=A+ zjU|+&B2mIm$w+|foKIbYXH=Y$xwEt4T^p1edLhXqZHHy{?_>EIIfJ@0Cr^3@2e_zi zgv4p29SaTIN-v`_J)+o{-xYFhXJ&m{w00CS^j@j|ac4f6O=j*=0&lK2-Pj~!a$g|1 zm$;B2W!GB%)A+IH*d+%BD@0GRp!;fO*P^+eA#G5Ct$&|^vf5Tg7I~AjAdLSXp$BG^ zW&$vXDJtb~-xc3Rkz_#9+>}x^K1l|^?!u&D*Z+CT$w+}D03He8+Yd8=(m-vt@jJ$_ k{GUszttP*|q`&M4w$)6hb4~$J2#;n`v6xrM|5xh$2O|vIx&QzG literal 0 HcmV?d00001 diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 867e3d3965..d54281b143 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -257,6 +257,36 @@ test.describe('browser', () => { await page.close(); }); + test('should pass with matching certificates in 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.pfx'), + passphrase: 'secure' + }], + }); + await page.goto(serverURL); + await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); + 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({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + pfxPath: asset('client-certificates/client/trusted/cert.pfx'), + passphrase: 'this-password-is-incorrect' + }], + }); + await page.goto(serverURL); + await expect(page.getByText('Playwright client-certificate error: mac verify failure')).toBeVisible(); + await page.close(); + }); + test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => { const serverURL = await startCCServer({ host: '127.0.0.1' }); const baseOptions = {