Bug 1040130 - Allow specifying a client cert for sockets. r=keeler, r=mcmanus

This commit is contained in:
J. Ryan Stinnett 2014-08-07 16:32:00 -04:00
parent 2a94f956c6
commit 951ce56ee2
15 changed files with 328 additions and 4 deletions

View File

@ -7,6 +7,7 @@
#include "nsISupports.idl"
interface nsIInterfaceRequestor;
interface nsIX509Cert;
%{C++
template<class T> class nsTArray;
@ -14,7 +15,7 @@ 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;
@ -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;
};

View File

@ -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<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) {

View File

@ -151,6 +151,8 @@ private:
uint32_t mProviderFlags;
mozilla::TimeStamp mSocketCreationTimestamp;
uint64_t mPlaintextBytesRead;
nsCOMPtr<nsIX509Cert> mClientCert;
};
class nsSSLIOLayerHelpers

View File

@ -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("<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);
@ -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();

View File

@ -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();
}

View File

@ -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
]);

View File

@ -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}

View File

@ -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")

View File

@ -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);
}

View File

@ -8,6 +8,7 @@ FAIL_ON_WARNINGS = True
SIMPLE_PROGRAMS = [
'BadCertServer',
'ClientAuthServer',
'GenerateOCSPResponse',
'OCSPStaplingServer',
]

View File

@ -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"

View File

@ -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 \

View File

@ -371,6 +371,7 @@ class XPCShellRemote(xpcshell.XPCShellTests, object):
"certutil",
"pk12util",
"BadCertServer",
"ClientAuthServer",
"OCSPStaplingServer",
"GenerateOCSPResponse"]
for fname in binaries:

View File

@ -615,6 +615,7 @@ NO_PKG_FILES += \
certutil* \
pk12util* \
BadCertServer* \
ClientAuthServer* \
OCSPStaplingServer* \
GenerateOCSPResponse* \
winEmbed.exe \