Bug 1040130 - Allow specifying a client cert for sockets. r=keeler, r=mcmanus
authorJ. Ryan Stinnett <jryans@gmail.com>
Thu, 07 Aug 2014 16:32:00 -0400
changeset 198735 04500acd1bb79d5af4ae2e119fa1c51acf6a02e1
parent 198734 f3f62111caff3007a858cc1c69d00109882402c7
child 198736 c2e53a571e98a835e0f218dc39c8f5e75bfa066e
push id27284
push userryanvm@gmail.com
push dateSat, 09 Aug 2014 15:25:31 +0000
treeherdermozilla-central@ad8cb646fad6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler, mcmanus
bugs1040130
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1040130 - Allow specifying a client cert for sockets. r=keeler, r=mcmanus
netwerk/socket/nsISSLSocketControl.idl
security/manager/ssl/src/nsNSSIOLayer.cpp
security/manager/ssl/src/nsNSSIOLayer.h
security/manager/ssl/tests/unit/head_psm.js
security/manager/ssl/tests/unit/test_client_cert.js
security/manager/ssl/tests/unit/test_client_cert/cert_dialog.js
security/manager/ssl/tests/unit/test_client_cert/cert_dialog.manifest
security/manager/ssl/tests/unit/test_client_cert/client-cert.p12
security/manager/ssl/tests/unit/test_client_cert/generate.py
security/manager/ssl/tests/unit/tlsserver/cmd/ClientAuthServer.cpp
security/manager/ssl/tests/unit/tlsserver/cmd/moz.build
security/manager/ssl/tests/unit/xpcshell.ini
testing/mochitest/Makefile.in
testing/xpcshell/remotexpcshelltests.py
toolkit/mozapps/installer/packager.mk
--- a/netwerk/socket/nsISSLSocketControl.idl
+++ b/netwerk/socket/nsISSLSocketControl.idl
@@ -2,24 +2,25 @@
  *
  * 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/. */
 
 #include "nsISupports.idl"
 
 interface nsIInterfaceRequestor;
+interface nsIX509Cert;
 
 %{C++
 template<class T> class nsTArray;
 class nsCString;
 %}
 [ref] native nsCStringTArrayRef(nsTArray<nsCString>);
 
-[scriptable, builtinclass, uuid(2032ad83-229f-4ddb-818a-59b9ae4ecd4b)]
+[scriptable, builtinclass, uuid(7836a872-e50c-4e43-8224-fd08a8d09699)]
 interface nsISSLSocketControl : nsISupports {
     attribute nsIInterfaceRequestor     notificationCallbacks;
 
     void proxyStartSSL();
     void StartTLS();
 
     /* NPN (Next Protocol Negotiation) is a mechanism for
        negotiating the protocol to be spoken inside the SSL
@@ -89,10 +90,17 @@ interface nsISSLSocketControl : nsISuppo
     const short SSL_MAC_MD5     = 1;
     const short SSL_MAC_SHA     = 2;
     const short SSL_HMAC_MD5    = 3;
     const short SSL_HMAC_SHA    = 4;
     const short SSL_HMAC_SHA256 = 5;
     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;
 };
 
--- a/security/manager/ssl/src/nsNSSIOLayer.cpp
+++ b/security/manager/ssl/src/nsNSSIOLayer.cpp
@@ -135,17 +135,18 @@ nsNSSSocketInfo::nsNSSSocketInfo(SharedS
     mNotedTimeUntilReady(false),
     mKEAUsed(nsISSLSocketControl::KEY_EXCHANGE_UNKNOWN),
     mKEAExpected(nsISSLSocketControl::KEY_EXCHANGE_UNKNOWN),
     mKEAKeyBits(0),
     mSSLVersionUsed(nsISSLSocketControl::SSL_VERSION_UNKNOWN),
     mMACAlgorithmUsed(nsISSLSocketControl::SSL_MAC_UNKNOWN),
     mProviderFlags(providerFlags),
     mSocketCreationTimestamp(TimeStamp::Now()),
-    mPlaintextBytesRead(0)
+    mPlaintextBytesRead(0),
+    mClientCert(nullptr)
 {
   mTLSVersionRange.min = 0;
   mTLSVersionRange.max = 0;
 }
 
 nsNSSSocketInfo::~nsNSSSocketInfo()
 {
 }
@@ -199,16 +200,32 @@ nsNSSSocketInfo::GetSSLVersionUsed(int16
 NS_IMETHODIMP
 nsNSSSocketInfo::GetMACAlgorithmUsed(int16_t* aMac)
 {
   *aMac = mMACAlgorithmUsed;
   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)
 {
   NS_ENSURE_ARG_POINTER(aRemember);
   *aRemember = mRememberClientAuthCertificate;
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -1903,16 +1920,39 @@ ClientAuthDataRunnable::RunOnTargetThrea
   CERTCertListNode* node;
   ScopedCERTCertNicknames nicknames;
   int keyError = 0; // used for private key retrieval error
   SSM_UserCertChoice certChoice;
   int32_t NumberOfCerts = 0;
   void* wincx = mSocketInfo;
   nsresult rv;
 
+  nsCOMPtr<nsIX509Cert> 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) {
     goto loser;
   }
 
   caNameStrings = (char**) PORT_ArenaAlloc(arena,
                                            sizeof(char*) * (mCANames->nnames));
--- a/security/manager/ssl/src/nsNSSIOLayer.h
+++ b/security/manager/ssl/src/nsNSSIOLayer.h
@@ -146,16 +146,18 @@ private:
   int16_t mKEAExpected;
   uint32_t mKEAKeyBits;
   int16_t mSSLVersionUsed;
   int16_t mMACAlgorithmUsed;
 
   uint32_t mProviderFlags;
   mozilla::TimeStamp mSocketCreationTimestamp;
   uint64_t mPlaintextBytesRead;
+
+  nsCOMPtr<nsIX509Cert> mClientCert;
 };
 
 class nsSSLIOLayerHelpers
 {
 public:
   nsSSLIOLayerHelpers();
   ~nsSSLIOLayerHelpers();
 
--- a/security/manager/ssl/tests/unit/head_psm.js
+++ b/security/manager/ssl/tests/unit/head_psm.js
@@ -52,16 +52,17 @@ const SEC_ERROR_OCSP_UNAUTHORIZED_RESPON
 const SEC_ERROR_OCSP_OLD_RESPONSE                       = SEC_ERROR_BASE + 132;
 const SEC_ERROR_OCSP_INVALID_SIGNING_CERT               = SEC_ERROR_BASE + 144;
 const SEC_ERROR_POLICY_VALIDATION_FAILED                = SEC_ERROR_BASE + 160; // -8032
 const SEC_ERROR_OCSP_BAD_SIGNATURE                      = SEC_ERROR_BASE + 157;
 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;
 
 // Supported Certificate Usages
 const certificateUsageSSLClient              = 0x0001;
 const certificateUsageSSLServer              = 0x0002;
 const certificateUsageSSLCA                  = 0x0008;
 const certificateUsageEmailSigner            = 0x0010;
@@ -197,17 +198,18 @@ function clearSessionCache() {
 
 function run_test() {
   do_get_profile();
   add_tls_server_setup("<test-server-name>");
 
   add_connection_test("<test-name-1>.example.com",
                       getXPCOMStatusFromNSS(SEC_ERROR_xxx),
                       function() { ... },
-                      function(aTransportSecurityInfo) { ... });
+                      function(aTransportSecurityInfo) { ... },
+                      function(aTransport) { ... });
   [...]
   add_connection_test("<test-name-n>.example.com", Cr.NS_OK);
 
   run_next_test();
 }
 
 */
 function add_tls_server_setup(serverBinName) {
@@ -218,18 +220,21 @@ function add_tls_server_setup(serverBinN
 
 // Add a TLS connection test case. aHost is the hostname to pass in the SNI TLS
 // extension; this should unambiguously identifiy which test is being run.
 // aExpectedResult is the expected nsresult of the connection.
 // aBeforeConnect is a callback function that takes no arguments that will be
 // 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) {
     this.host = aHost;
     let threadManager = Cc["@mozilla.org/thread-manager;1"]
                           .getService(Ci.nsIThreadManager);
     this.thread = threadManager.currentThread;
     this.defer = Promise.defer();
@@ -263,16 +268,19 @@ function add_connection_test(aHost, aExp
       } catch (e) {
         this.result = e.result;
       }
       this.defer.resolve(this);
     },
 
     // nsIOutputStreamCallback
     onOutputStreamReady: function(aStream) {
+      if (aAfterStreamOpen) {
+        aAfterStreamOpen(this.transport);
+      }
       let sslSocketControl = this.transport.securityInfo
                                .QueryInterface(Ci.nsISSLSocketControl);
       sslSocketControl.proxyStartSSL();
       this.outputStream.write("0", 1);
       let inStream = this.transport.openInputStream(0, 0, 0)
                        .QueryInterface(Ci.nsIAsyncInputStream);
       this.inputStream = inStream;
       this.inputStream.asyncWait(this, 0, 0, this.thread);
new file mode 100644
--- /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();
+}
new file mode 100644
--- /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
+]);
new file mode 100644
--- /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}
new file mode 100644
index 0000000000000000000000000000000000000000..cb939cb0f41cf41d7c3ed2b034102fdc6e52ec22
GIT binary patch
literal 2285
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
new file mode 100755
--- /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")
new file mode 100644
--- /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 <stdio.h>
+
+#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 <NSS DB directory>\n", argv[0]);
+    return 1;
+  }
+
+  return StartServer(argv[1], DoSNISocketConfig, nullptr);
+}
--- a/security/manager/ssl/tests/unit/tlsserver/cmd/moz.build
+++ b/security/manager/ssl/tests/unit/tlsserver/cmd/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 FAIL_ON_WARNINGS = True
 
 SIMPLE_PROGRAMS = [
     'BadCertServer',
+    'ClientAuthServer',
     'GenerateOCSPResponse',
     'OCSPStaplingServer',
 ]
 
 SOURCES += [
     '%s.cpp' % s for s in SIMPLE_PROGRAMS
 ]
 
--- a/security/manager/ssl/tests/unit/xpcshell.ini
+++ b/security/manager/ssl/tests/unit/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 head = head_psm.js
 tail =
 support-files =
   test_certificate_usages/**
   test_signed_apps/**
   tlsserver/**
   test_cert_signatures/**
+  test_client_cert/**
   test_ev_certs/**
   test_getchain/**
   test_intermediate_basic_usage_constraints/**
   test_name_constraints/**
   test_cert_trust/**
   test_cert_version/**
   test_cert_eku/**
   test_ocsp_url/**
@@ -89,8 +90,12 @@ run-sequentially = hardcoded ports
 # Bug 1009158: this test times out on Android
 skip-if = os == "android"
 [test_ocsp_no_hsts_upgrade.js]
 run-sequentially = hardcoded ports
 # Bug 1009158: this test times out on Android
 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"
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -107,16 +107,17 @@ libs::
 # Binaries and scripts that don't get packaged with the build,
 # but that we need for the test harness
 TEST_HARNESS_BINS := \
   xpcshell$(BIN_SUFFIX) \
   ssltunnel$(BIN_SUFFIX) \
   certutil$(BIN_SUFFIX) \
   pk12util$(BIN_SUFFIX) \
   BadCertServer$(BIN_SUFFIX) \
+  ClientAuthServer$(BIN_SUFFIX) \
   OCSPStaplingServer$(BIN_SUFFIX) \
   GenerateOCSPResponse$(BIN_SUFFIX) \
   fix_stack_using_bpsyms.py \
   $(NULL)
 
 ifeq ($(OS_ARCH),WINNT)
 TEST_HARNESS_BINS += \
   crashinject$(BIN_SUFFIX) \
--- a/testing/xpcshell/remotexpcshelltests.py
+++ b/testing/xpcshell/remotexpcshelltests.py
@@ -366,16 +366,17 @@ class XPCShellRemote(xpcshell.XPCShellTe
         # The xpcshell binary is required for all tests. Additional binaries
         # are required for some tests. This list should be similar to
         # TEST_HARNESS_BINS in testing/mochitest/Makefile.in.
         binaries = ["xpcshell",
                     "ssltunnel",
                     "certutil",
                     "pk12util",
                     "BadCertServer",
+                    "ClientAuthServer",
                     "OCSPStaplingServer",
                     "GenerateOCSPResponse"]
         for fname in binaries:
             local = os.path.join(self.localBin, fname)
             if os.path.isfile(local):
                 print >> sys.stderr, "Pushing %s.." % fname
                 remoteFile = remoteJoin(self.remoteBinDir, fname)
                 self.device.pushFile(local, remoteFile)
--- a/toolkit/mozapps/installer/packager.mk
+++ b/toolkit/mozapps/installer/packager.mk
@@ -610,16 +610,17 @@ NO_PKG_FILES += \
 	nm2tsv* \
 	nsinstall* \
 	res/samples \
 	res/throbber \
 	shlibsign* \
 	certutil* \
 	pk12util* \
 	BadCertServer* \
+	ClientAuthServer* \
 	OCSPStaplingServer* \
 	GenerateOCSPResponse* \
 	winEmbed.exe \
 	chrome/chrome.rdf \
 	chrome/app-chrome.manifest \
 	chrome/overlayinfo \
 	components/compreg.dat \
 	components/xpti.dat \