diff --git a/netwerk/socket/nsISSLSocketControl.idl b/netwerk/socket/nsISSLSocketControl.idl index 0703bbe4765..e9c3d746c79 100644 --- a/netwerk/socket/nsISSLSocketControl.idl +++ b/netwerk/socket/nsISSLSocketControl.idl @@ -7,6 +7,7 @@ #include "nsISupports.idl" interface nsIInterfaceRequestor; +interface nsIX509Cert; %{C++ template class nsTArray; @@ -14,7 +15,7 @@ class nsCString; %} [ref] native nsCStringTArrayRef(nsTArray); -[scriptable, builtinclass, uuid(2032ad83-229f-4ddb-818a-59b9ae4ecd4b)] +[scriptable, builtinclass, uuid(7836a872-e50c-4e43-8224-fd08a8d09699)] interface nsISSLSocketControl : nsISupports { attribute nsIInterfaceRequestor notificationCallbacks; @@ -94,5 +95,12 @@ interface nsISSLSocketControl : nsISupports { const short SSL_MAC_AEAD = 6; [infallible] readonly attribute short MACAlgorithmUsed; + + /** + * If set before the server requests a client cert (assuming it does so at + * all), then this cert will be presented to the server, instead of asking + * the user or searching the set of rememebered user cert decisions. + */ + attribute nsIX509Cert clientCert; }; diff --git a/security/manager/ssl/src/nsNSSIOLayer.cpp b/security/manager/ssl/src/nsNSSIOLayer.cpp index d9c25dee494..b688d3814f0 100644 --- a/security/manager/ssl/src/nsNSSIOLayer.cpp +++ b/security/manager/ssl/src/nsNSSIOLayer.cpp @@ -140,7 +140,8 @@ nsNSSSocketInfo::nsNSSSocketInfo(SharedSSLState& aState, uint32_t providerFlags) mMACAlgorithmUsed(nsISSLSocketControl::SSL_MAC_UNKNOWN), mProviderFlags(providerFlags), mSocketCreationTimestamp(TimeStamp::Now()), - mPlaintextBytesRead(0) + mPlaintextBytesRead(0), + mClientCert(nullptr) { mTLSVersionRange.min = 0; mTLSVersionRange.max = 0; @@ -203,6 +204,22 @@ nsNSSSocketInfo::GetMACAlgorithmUsed(int16_t* aMac) return NS_OK; } +NS_IMETHODIMP +nsNSSSocketInfo::GetClientCert(nsIX509Cert** aClientCert) +{ + NS_ENSURE_ARG_POINTER(aClientCert); + *aClientCert = mClientCert; + NS_IF_ADDREF(*aClientCert); + return NS_OK; +} + +NS_IMETHODIMP +nsNSSSocketInfo::SetClientCert(nsIX509Cert* aClientCert) +{ + mClientCert = aClientCert; + return NS_OK; +} + NS_IMETHODIMP nsNSSSocketInfo::GetRememberClientAuthCertificate(bool* aRemember) { @@ -1908,6 +1925,29 @@ ClientAuthDataRunnable::RunOnTargetThread() void* wincx = mSocketInfo; nsresult rv; + nsCOMPtr socketClientCert; + mSocketInfo->GetClientCert(getter_AddRefs(socketClientCert)); + + // If a client cert preference was set on the socket info, use that and skip + // the client cert UI and/or search of the user's past cert decisions. + if (socketClientCert) { + cert = socketClientCert->GetCert(); + if (!cert) { + goto loser; + } + + // Get the private key + privKey = PK11_FindKeyByAnyCert(cert.get(), wincx); + if (!privKey) { + goto loser; + } + + *mPRetCert = cert.forget(); + *mPRetKey = privKey.forget(); + mRV = SECSuccess; + return; + } + // create caNameStrings arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE); if (!arena) { diff --git a/security/manager/ssl/src/nsNSSIOLayer.h b/security/manager/ssl/src/nsNSSIOLayer.h index 6c5661e1649..f822e46208b 100644 --- a/security/manager/ssl/src/nsNSSIOLayer.h +++ b/security/manager/ssl/src/nsNSSIOLayer.h @@ -151,6 +151,8 @@ private: uint32_t mProviderFlags; mozilla::TimeStamp mSocketCreationTimestamp; uint64_t mPlaintextBytesRead; + + nsCOMPtr mClientCert; }; class nsSSLIOLayerHelpers diff --git a/security/manager/ssl/tests/unit/head_psm.js b/security/manager/ssl/tests/unit/head_psm.js index a6f8d88f423..c62073572a4 100644 --- a/security/manager/ssl/tests/unit/head_psm.js +++ b/security/manager/ssl/tests/unit/head_psm.js @@ -57,6 +57,7 @@ const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = SEC_ERROR_BASE + 176; const SEC_ERROR_APPLICATION_CALLBACK_ERROR = SEC_ERROR_BASE + 178; const SSL_ERROR_BAD_CERT_DOMAIN = SSL_ERROR_BASE + 12; +const SSL_ERROR_BAD_CERT_ALERT = SSL_ERROR_BASE + 17; const MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE = MOZILLA_PKIX_ERROR_BASE + 0; @@ -202,7 +203,8 @@ function run_test() { add_connection_test(".example.com", getXPCOMStatusFromNSS(SEC_ERROR_xxx), function() { ... }, - function(aTransportSecurityInfo) { ... }); + function(aTransportSecurityInfo) { ... }, + function(aTransport) { ... }); [...] add_connection_test(".example.com", Cr.NS_OK); @@ -223,8 +225,11 @@ function add_tls_server_setup(serverBinName) { // called before the connection is attempted. // aWithSecurityInfo is a callback function that takes an // nsITransportSecurityInfo, which is called after the TLS handshake succeeds. +// aAfterStreamOpen is a callback function that is called with the +// nsISocketTransport once the output stream is ready. function add_connection_test(aHost, aExpectedResult, - aBeforeConnect, aWithSecurityInfo) { + aBeforeConnect, aWithSecurityInfo, + aAfterStreamOpen) { const REMOTE_PORT = 8443; function Connection(aHost) { @@ -268,6 +273,9 @@ function add_connection_test(aHost, aExpectedResult, // nsIOutputStreamCallback onOutputStreamReady: function(aStream) { + if (aAfterStreamOpen) { + aAfterStreamOpen(this.transport); + } let sslSocketControl = this.transport.securityInfo .QueryInterface(Ci.nsISSLSocketControl); sslSocketControl.proxyStartSSL(); diff --git a/security/manager/ssl/tests/unit/test_client_cert.js b/security/manager/ssl/tests/unit/test_client_cert.js new file mode 100644 index 00000000000..e7d51fe7c87 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_client_cert.js @@ -0,0 +1,68 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// 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"; + +// Tests specifying a particular client cert to use via the nsISSLSocketControl +// |clientCert| attribute prior to connecting to the server. + +function run_test() { + do_get_profile(); + + // Init key token (to prevent password prompt) + const tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"] + .getService(Ci.nsIPK11TokenDB); + let keyToken = tokenDB.getInternalKeyToken(); + if (keyToken.needsUserInit) { + keyToken.initPassword(""); + } + + // Replace the UI dialog that would prompt for the following PKCS #12 file's + // password, as well as an alert that appears after it succeeds. + do_load_manifest("test_client_cert/cert_dialog.manifest"); + + // Load the user cert and look it up in XPCOM format + const certDB = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + let clientCertFile = do_get_file("test_client_cert/client-cert.p12", false); + certDB.importPKCS12File(null, clientCertFile); + + // Find the cert by its common name + let clientCert; + let certs = certDB.getCerts().getEnumerator(); + while (certs.hasMoreElements()) { + let cert = certs.getNext().QueryInterface(Ci.nsIX509Cert); + if (cert.certType === Ci.nsIX509Cert.USER_CERT && + cert.commonName === "client-cert") { + clientCert = cert; + break; + } + } + ok(clientCert, "Client cert found"); + + add_tls_server_setup("ClientAuthServer"); + + add_connection_test("noclientauth.example.com", Cr.NS_OK); + + add_connection_test("requestclientauth.example.com", Cr.NS_OK); + add_connection_test("requestclientauth.example.com", Cr.NS_OK, + null, null, transport => { + do_print("Setting client cert on transport"); + let sslSocketControl = transport.securityInfo + .QueryInterface(Ci.nsISSLSocketControl); + sslSocketControl.clientCert = clientCert; + }); + + add_connection_test("requireclientauth.example.com", + getXPCOMStatusFromNSS(SSL_ERROR_BAD_CERT_ALERT)); + add_connection_test("requireclientauth.example.com", Cr.NS_OK, + null, null, transport => { + do_print("Setting client cert on transport"); + let sslSocketControl = + transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl); + sslSocketControl.clientCert = clientCert; + }); + + run_next_test(); +} diff --git a/security/manager/ssl/tests/unit/test_client_cert/cert_dialog.js b/security/manager/ssl/tests/unit/test_client_cert/cert_dialog.js new file mode 100644 index 00000000000..96087cccefd --- /dev/null +++ b/security/manager/ssl/tests/unit/test_client_cert/cert_dialog.js @@ -0,0 +1,37 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// 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/. + +const { utils: Cu, interfaces: Ci } = Components; +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); + +function CertDialogService() {} +CertDialogService.prototype = { + classID: Components.ID("{a70153f2-3590-4317-93e9-73b3e7ffca5d}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsICertificateDialogs]), + + getPKCS12FilePassword: function() { + return true; // Simulates entering an empty password + } +}; + +let Prompter = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]), + alert: function() {} // Do nothing when asked to show an alert +}; + +function WindowWatcherService() {} +WindowWatcherService.prototype = { + classID: Components.ID("{01ae923c-81bb-45db-b860-d423b0fc4fe1}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher]), + + getNewPrompter: function() { + return Prompter; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ + CertDialogService, + WindowWatcherService +]); diff --git a/security/manager/ssl/tests/unit/test_client_cert/cert_dialog.manifest b/security/manager/ssl/tests/unit/test_client_cert/cert_dialog.manifest new file mode 100644 index 00000000000..91c2c579f46 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_client_cert/cert_dialog.manifest @@ -0,0 +1,4 @@ +component {a70153f2-3590-4317-93e9-73b3e7ffca5d} cert_dialog.js +contract @mozilla.org/nsCertificateDialogs;1 {a70153f2-3590-4317-93e9-73b3e7ffca5d} +component {01ae923c-81bb-45db-b860-d423b0fc4fe1} cert_dialog.js +contract @mozilla.org/embedcomp/window-watcher;1 {01ae923c-81bb-45db-b860-d423b0fc4fe1} diff --git a/security/manager/ssl/tests/unit/test_client_cert/client-cert.p12 b/security/manager/ssl/tests/unit/test_client_cert/client-cert.p12 new file mode 100644 index 00000000000..cb939cb0f41 Binary files /dev/null and b/security/manager/ssl/tests/unit/test_client_cert/client-cert.p12 differ diff --git a/security/manager/ssl/tests/unit/test_client_cert/generate.py b/security/manager/ssl/tests/unit/test_client_cert/generate.py new file mode 100755 index 00000000000..5464acecb5a --- /dev/null +++ b/security/manager/ssl/tests/unit/test_client_cert/generate.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python + +# 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/. + +# After running this file you MUST modify ClientAuthServer.cpp to change the +# fingerprint of the client cert + +import tempfile, os, sys, random + +libpath = os.path.abspath("../psm_common_py") +sys.path.append(libpath) + +import CertUtils + +dest_dir = os.getcwd() +db = tempfile.mkdtemp() + +serial = random.randint(100, 40000000) +name = "client-cert" +[key, cert] = CertUtils.generate_cert_generic(db, dest_dir, serial, "rsa", + name, "") +CertUtils.generate_pkcs12(db, dest_dir, cert, key, name) + +# Remove unnecessary .der file +os.remove(dest_dir + "/" + name + ".der") + +print ("You now MUST modify ClientAuthServer.cpp to ensure the xpchell debug " + + "certificate there matches this newly generated one\n") diff --git a/security/manager/ssl/tests/unit/tlsserver/cmd/ClientAuthServer.cpp b/security/manager/ssl/tests/unit/tlsserver/cmd/ClientAuthServer.cpp new file mode 100644 index 00000000000..3f0c2ad415b --- /dev/null +++ b/security/manager/ssl/tests/unit/tlsserver/cmd/ClientAuthServer.cpp @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 tw=80 et: */ +/* 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/. */ + +// This is a standalone server for testing client cert authentication. +// The client is expected to connect, initiate an SSL handshake (with SNI +// to indicate which "server" to connect to), and verify the certificate. +// If all is good, the client then sends one encrypted byte and receives that +// same byte back. +// This server also has the ability to "call back" another process waiting on +// it. That is, when the server is all set up and ready to receive connections, +// it will connect to a specified port and issue a simple HTTP request. + +#include + +#include "hasht.h" +#include "ScopedNSSTypes.h" +#include "ssl.h" +#include "TLSServer.h" + +using namespace mozilla; +using namespace mozilla::test; + +struct ClientAuthHost +{ + const char *mHostName; + bool mRequestClientAuth; + bool mRequireClientAuth; +}; + +// Hostname, cert nickname pairs. +static const ClientAuthHost sClientAuthHosts[] = +{ + { "noclientauth.example.com", false, false }, + { "requestclientauth.example.com", true, false }, + { "requireclientauth.example.com", true, true }, + { nullptr, nullptr } +}; + +static const unsigned char sClientCertFingerprint[] = +{ + 0xD2, 0x2F, 0x00, 0x9A, 0x9E, 0xED, 0x79, 0xDC, + 0x8D, 0x17, 0x98, 0x8E, 0xEC, 0x76, 0x05, 0x91, + 0xA5, 0xF6, 0xC9, 0xFA, 0x16, 0x8B, 0xD2, 0x5F, + 0xE1, 0x52, 0x04, 0x7C, 0xF4, 0x76, 0x42, 0x9D +}; + +SECStatus +AuthCertificateHook(void* arg, PRFileDesc* fd, PRBool checkSig, + PRBool isServer) +{ + ScopedCERTCertificate clientCert(SSL_PeerCertificate(fd)); + + unsigned char certFingerprint[SHA256_LENGTH]; + SECStatus rv = PK11_HashBuf(SEC_OID_SHA256, certFingerprint, + clientCert->derCert.data, + clientCert->derCert.len); + if (rv != SECSuccess) { + return rv; + } + + static_assert(sizeof(sClientCertFingerprint) == SHA256_LENGTH, + "Ensure fingerprint has corrent length"); + bool match = !memcmp(certFingerprint, sClientCertFingerprint, + sizeof(certFingerprint)); + return match ? SECSuccess : SECFailure; +} + +int32_t +DoSNISocketConfig(PRFileDesc* aFd, const SECItem* aSrvNameArr, + uint32_t aSrvNameArrSize, void* aArg) +{ + const ClientAuthHost *host = GetHostForSNI(aSrvNameArr, aSrvNameArrSize, + sClientAuthHosts); + if (!host) { + return SSL_SNI_SEND_ALERT; + } + + if (gDebugLevel >= DEBUG_VERBOSE) { + fprintf(stderr, "found pre-defined host '%s'\n", host->mHostName); + } + + SECStatus srv = ConfigSecureServerWithNamedCert(aFd, DEFAULT_CERT_NICKNAME, + nullptr, nullptr); + if (srv != SECSuccess) { + return SSL_SNI_SEND_ALERT; + } + + SSL_OptionSet(aFd, SSL_REQUEST_CERTIFICATE, host->mRequestClientAuth); + if (host->mRequireClientAuth) { + SSL_OptionSet(aFd, SSL_REQUIRE_CERTIFICATE, SSL_REQUIRE_ALWAYS); + } else { + SSL_OptionSet(aFd, SSL_REQUIRE_CERTIFICATE, SSL_REQUIRE_NEVER); + } + + // Override default client auth hook to just check fingerprint + srv = SSL_AuthCertificateHook(aFd, AuthCertificateHook, nullptr); + if (srv != SECSuccess) { + return SSL_SNI_SEND_ALERT; + } + + return 0; +} + +int +main(int argc, char* argv[]) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + + return StartServer(argv[1], DoSNISocketConfig, nullptr); +} diff --git a/security/manager/ssl/tests/unit/tlsserver/cmd/moz.build b/security/manager/ssl/tests/unit/tlsserver/cmd/moz.build index 9d358bcdaec..a679f8a0b90 100644 --- a/security/manager/ssl/tests/unit/tlsserver/cmd/moz.build +++ b/security/manager/ssl/tests/unit/tlsserver/cmd/moz.build @@ -8,6 +8,7 @@ FAIL_ON_WARNINGS = True SIMPLE_PROGRAMS = [ 'BadCertServer', + 'ClientAuthServer', 'GenerateOCSPResponse', 'OCSPStaplingServer', ] diff --git a/security/manager/ssl/tests/unit/xpcshell.ini b/security/manager/ssl/tests/unit/xpcshell.ini index 37be9647441..512a6e2a65e 100644 --- a/security/manager/ssl/tests/unit/xpcshell.ini +++ b/security/manager/ssl/tests/unit/xpcshell.ini @@ -6,6 +6,7 @@ support-files = test_signed_apps/** tlsserver/** test_cert_signatures/** + test_client_cert/** test_ev_certs/** test_getchain/** test_intermediate_basic_usage_constraints/** @@ -94,3 +95,7 @@ run-sequentially = hardcoded ports skip-if = os == "android" [test_add_preexisting_cert.js] [test_keysize.js] +[test_client_cert.js] +run-sequentially = hardcoded ports +# Bug 1009158: this test times out on Android +skip-if = os == "android" diff --git a/testing/mochitest/Makefile.in b/testing/mochitest/Makefile.in index 370aa977495..c8c5f6cd2da 100644 --- a/testing/mochitest/Makefile.in +++ b/testing/mochitest/Makefile.in @@ -112,6 +112,7 @@ TEST_HARNESS_BINS := \ certutil$(BIN_SUFFIX) \ pk12util$(BIN_SUFFIX) \ BadCertServer$(BIN_SUFFIX) \ + ClientAuthServer$(BIN_SUFFIX) \ OCSPStaplingServer$(BIN_SUFFIX) \ GenerateOCSPResponse$(BIN_SUFFIX) \ fix_stack_using_bpsyms.py \ diff --git a/testing/xpcshell/remotexpcshelltests.py b/testing/xpcshell/remotexpcshelltests.py index 1ad120c6b7d..5502d388efb 100644 --- a/testing/xpcshell/remotexpcshelltests.py +++ b/testing/xpcshell/remotexpcshelltests.py @@ -371,6 +371,7 @@ class XPCShellRemote(xpcshell.XPCShellTests, object): "certutil", "pk12util", "BadCertServer", + "ClientAuthServer", "OCSPStaplingServer", "GenerateOCSPResponse"] for fname in binaries: diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk index 3bf80e8284b..854f5c5a569 100644 --- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -615,6 +615,7 @@ NO_PKG_FILES += \ certutil* \ pk12util* \ BadCertServer* \ + ClientAuthServer* \ OCSPStaplingServer* \ GenerateOCSPResponse* \ winEmbed.exe \