From 3e7402a3e1bca63816bba0c5444993319b2371cb Mon Sep 17 00:00:00 2001 From: Kershaw Chang Date: Wed, 22 Nov 2023 09:15:39 +0000 Subject: [PATCH] Bug 1851570 - Allow necko to know when client auth is selected to drive TLS handshake, r=necko-reviewers,keeler,valentin Differential Revision: https://phabricator.services.mozilla.com/D194242 --- netwerk/protocol/http/TlsHandshaker.cpp | 18 ++ .../protocol/http/nsITlsHandshakeListener.idl | 2 + .../test/unit/test_client_auth_with_proxy.js | 176 ++++++++++++++++++ netwerk/test/unit/xpcshell.ini | 2 + security/manager/ssl/NSSSocketControl.cpp | 15 ++ 5 files changed, 213 insertions(+) create mode 100644 netwerk/test/unit/test_client_auth_with_proxy.js diff --git a/netwerk/protocol/http/TlsHandshaker.cpp b/netwerk/protocol/http/TlsHandshaker.cpp index a391cef900586..5288fb939d0f3 100644 --- a/netwerk/protocol/http/TlsHandshaker.cpp +++ b/netwerk/protocol/http/TlsHandshaker.cpp @@ -30,6 +30,24 @@ TlsHandshaker::TlsHandshaker(nsHttpConnectionInfo* aInfo, TlsHandshaker::~TlsHandshaker() { LOG(("TlsHandshaker dtor %p", this)); } +NS_IMETHODIMP +TlsHandshaker::CertVerificationDone() { + LOG(("TlsHandshaker::CertVerificationDone mOwner=%p", mOwner.get())); + if (mOwner) { + Unused << mOwner->ResumeSend(); + } + return NS_OK; +} + +NS_IMETHODIMP +TlsHandshaker::ClientAuthCertificateSelected() { + LOG(("TlsHandshaker::ClientAuthCertificateSelected mOwner=%p", mOwner.get())); + if (mOwner) { + Unused << mOwner->ResumeSend(); + } + return NS_OK; +} + NS_IMETHODIMP TlsHandshaker::HandshakeDone() { LOG(("TlsHandshaker::HandshakeDone mOwner=%p", mOwner.get())); diff --git a/netwerk/protocol/http/nsITlsHandshakeListener.idl b/netwerk/protocol/http/nsITlsHandshakeListener.idl index cb5444f6d75f9..0d5806be24c99 100644 --- a/netwerk/protocol/http/nsITlsHandshakeListener.idl +++ b/netwerk/protocol/http/nsITlsHandshakeListener.idl @@ -9,4 +9,6 @@ [uuid(b4bbe824-ec4c-48be-9a40-6a7339347f40)] interface nsITlsHandshakeCallbackListener : nsISupports { [noscript] void handshakeDone(); + [noscript] void certVerificationDone(); + [noscript] void clientAuthCertificateSelected(); }; diff --git a/netwerk/test/unit/test_client_auth_with_proxy.js b/netwerk/test/unit/test_client_auth_with_proxy.js new file mode 100644 index 0000000000000..5a205f4db12f4 --- /dev/null +++ b/netwerk/test/unit/test_client_auth_with_proxy.js @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ +/* import-globals-from head_servers.js */ + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +class SecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + + let output = this.output; + this.input.asyncWait( + { + onInputStreamReady(readyInput) { + let request = NetUtil.readInputStreamToString( + readyInput, + readyInput.available() + ); + ok( + request.startsWith("GET /") && request.includes("HTTP/1.1"), + "expecting an HTTP/1.1 GET request" + ); + let response = + "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\n" + + "Connection:Close\r\nContent-Length:2\r\n\r\nOK"; + output.write(response, response.length); + }, + }, + 0, + 0, + Services.tm.currentThread + ); + } +} + +function startServer(cert) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + + let securityObservers = []; + + let listener = { + onSocketAccepted(socket, transport) { + info("Accepted TLS client connection"); + let connectionInfo = transport.securityCallbacks.getInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + connectionInfo.setSecurityObserver(new SecurityObserver(input, output)); + }, + + onStopListening() { + info("onStopListening"); + for (let securityObserver of securityObservers) { + securityObserver.input.close(); + securityObserver.output.close(); + } + }, + }; + + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS); + + tlsServer.asyncListen(listener); + + return tlsServer; +} + +// Replace the UI dialog that prompts the user to pick a client certificate. +const clientAuthDialogService = { + chooseCertificate(hostname, certArray, loadContext, callback) { + callback.certificateChosen(certArray[0], false); + }, + QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]), +}; + +let server; +add_setup(async function setup() { + do_get_profile(); + + let clientAuthDialogServiceCID = MockRegistrar.register( + "@mozilla.org/security/ClientAuthDialogService;1", + clientAuthDialogService + ); + + let cert = getTestServerCertificate(); + ok(!!cert, "Got self-signed cert"); + server = startServer(cert); + + certOverrideService.rememberValidityOverride( + "localhost", + server.port, + {}, + cert, + true + ); + + registerCleanupFunction(async function () { + MockRegistrar.unregister(clientAuthDialogServiceCID); + certOverrideService.clearValidityOverride("localhost", server.port, {}); + server.close(); + }); +}); + +add_task(async function test_client_auth_with_proxy() { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); + + let proxies = [ + NodeHTTPProxyServer, + NodeHTTPSProxyServer, + NodeHTTP2ProxyServer, + ]; + + for (let p of proxies) { + info(`Test with proxy:${p.name}`); + let proxy = new p(); + await proxy.start(); + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + let chan = makeChan(`https://localhost:${server.port}`); + let [req, buff] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + equal(req.status, Cr.NS_OK); + equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200); + equal(buff, "OK"); + req.QueryInterface(Ci.nsIProxiedChannel); + ok(!!req.proxyInfo); + notEqual(req.proxyInfo.type, "direct"); + await proxy.stop(); + } +}); diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index e9ea371fb185c..e4db641863c11 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -778,3 +778,5 @@ skip-if = run-sequentially = node server exceptions dont replay well [test_non_ipv4_hostname_ending_in_number_cookie_db.js] [test_verify_traffic.js] +[test_client_auth_with_proxy.js] +skip-if = os == "android" diff --git a/security/manager/ssl/NSSSocketControl.cpp b/security/manager/ssl/NSSSocketControl.cpp index f3422fdef1da8..9d618a82284e1 100644 --- a/security/manager/ssl/NSSSocketControl.cpp +++ b/security/manager/ssl/NSSSocketControl.cpp @@ -383,7 +383,15 @@ void NSSSocketControl::SetCertVerificationResult(PRErrorCode errorCode) { AssertedCast(mPlaintextBytesRead)); } + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p] SetCertVerificationResult to AfterCertVerification, " + "mTlsHandshakeCallback=%p", + (void*)mFd, mTlsHandshakeCallback.get())); + mCertVerificationState = AfterCertVerification; + if (mTlsHandshakeCallback) { + Unused << mTlsHandshakeCallback->CertVerificationDone(); + } } void NSSSocketControl::ClientAuthCertificateSelected( @@ -435,6 +443,13 @@ void NSSSocketControl::ClientAuthCertificateSelected( mFd, sendingClientAuthCert ? SECSuccess : SECFailure, sendingClientAuthCert ? key.release() : nullptr, sendingClientAuthCert ? cert.release() : nullptr); + + MOZ_LOG(gPIPNSSLog, LogLevel::Debug, + ("[%p] ClientAuthCertificateSelected mTlsHandshakeCallback=%p", + (void*)mFd, mTlsHandshakeCallback.get())); + if (mTlsHandshakeCallback) { + Unused << mTlsHandshakeCallback->ClientAuthCertificateSelected(); + } } SharedSSLState& NSSSocketControl::SharedState() {