Commit 3a72273d authored by Kenton Varda's avatar Kenton Varda

Implement TLS option to verify client certificates.

Use case for me is Cloudflare's "Authenticated Origin Pulls", in which Cloudflare authenticates itself to the origin server using client certificates. Thus the origin server can ensure that it only accepts traffic through Cloudflare, without resorting to IP whitelists.
parent 2e066e16
...@@ -411,6 +411,70 @@ KJ_TEST("TLS certificate validation") { ...@@ -411,6 +411,70 @@ KJ_TEST("TLS certificate validation") {
"self signed certificate"); "self signed certificate");
} }
KJ_TEST("TLS client certificate verification") {
TlsContext::Options serverOptions = TlsTest::defaultServer();
TlsContext::Options clientOptions = TlsTest::defaultClient();
serverOptions.verifyClients = true;
serverOptions.trustedCertificates = clientOptions.trustedCertificates;
// No certificate loaded in the client: fail
{
TlsTest test(clientOptions, serverOptions);
auto pipe = test.io.provider->newTwoWayPipe();
auto clientPromise = test.tlsClient.wrapClient(kj::mv(pipe.ends[0]), "example.com");
auto serverPromise = test.tlsServer.wrapServer(kj::mv(pipe.ends[1]));
KJ_EXPECT_THROW_MESSAGE("peer did not return a certificate",
serverPromise.wait(test.io.waitScope));
KJ_EXPECT_THROW_MESSAGE("alert handshake failure",
clientPromise.wait(test.io.waitScope));
}
// Self-signed certificate loaded in the client: fail
{
TlsKeypair selfSignedKeypair = { TlsPrivateKey(HOST_KEY), TlsCertificate(SELF_SIGNED_CERT) };
clientOptions.defaultKeypair = selfSignedKeypair;
TlsTest test(clientOptions, serverOptions);
auto pipe = test.io.provider->newTwoWayPipe();
auto clientPromise = test.tlsClient.wrapClient(kj::mv(pipe.ends[0]), "example.com");
auto serverPromise = test.tlsServer.wrapServer(kj::mv(pipe.ends[1]));
KJ_EXPECT_THROW_MESSAGE("certificate verify failed",
serverPromise.wait(test.io.waitScope));
KJ_EXPECT_THROW_MESSAGE("alert unknown ca",
clientPromise.wait(test.io.waitScope));
}
// Trusted certificate loaded in the client: success.
{
clientOptions.defaultKeypair = serverOptions.defaultKeypair;
TlsTest test(clientOptions, serverOptions);
ErrorNexus e;
auto pipe = test.io.provider->newTwoWayPipe();
auto clientPromise = e.wrap(test.tlsClient.wrapClient(kj::mv(pipe.ends[0]), "example.com"));
auto serverPromise = e.wrap(test.tlsServer.wrapServer(kj::mv(pipe.ends[1])));
auto client = clientPromise.wait(test.io.waitScope);
auto server = serverPromise.wait(test.io.waitScope);
auto writePromise = client->write("foo", 3);
char buf[4];
server->read(&buf, 3).wait(test.io.waitScope);
buf[3] = '\0';
KJ_ASSERT(kj::StringPtr(buf) == "foo");
}
}
#ifdef KJ_EXTERNAL_TESTS #ifdef KJ_EXTERNAL_TESTS
KJ_TEST("TLS to capnproto.org") { KJ_TEST("TLS to capnproto.org") {
kj::AsyncIoContext io = setupAsyncIo(); kj::AsyncIoContext io = setupAsyncIo();
......
...@@ -473,6 +473,7 @@ private: ...@@ -473,6 +473,7 @@ private:
TlsContext::Options::Options() TlsContext::Options::Options()
: useSystemTrustStore(true), : useSystemTrustStore(true),
verifyClients(false),
minVersion(TlsVersion::TLS_1_0), minVersion(TlsVersion::TLS_1_0),
cipherList("ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS") {} cipherList("ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS") {}
// Cipher list is Mozilla's "intermediate" list, except with classic DH removed since we don't // Cipher list is Mozilla's "intermediate" list, except with classic DH removed since we don't
...@@ -516,6 +517,10 @@ TlsContext::TlsContext(Options options) { ...@@ -516,6 +517,10 @@ TlsContext::TlsContext(Options options) {
} }
} }
if (options.verifyClients) {
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
}
// honor options.minVersion // honor options.minVersion
long optionFlags = 0; long optionFlags = 0;
if (options.minVersion > TlsVersion::SSL_3) { if (options.minVersion > TlsVersion::SSL_3) {
......
...@@ -58,6 +58,13 @@ public: ...@@ -58,6 +58,13 @@ public:
bool useSystemTrustStore; bool useSystemTrustStore;
// Whether or not to trust the system's default trust store. Default: true. // Whether or not to trust the system's default trust store. Default: true.
bool verifyClients;
// If true, when acting as a server, require the client to present a certificate. The
// certificate must be signed by one of the trusted CAs, otherwise the client will be rejected.
// (Typically you should set `useSystemTrustStore` false when using this flag, and specify
// your specific trusted CAs in `trustedCertificates`.)
// Default: false
kj::ArrayPtr<const TlsCertificate> trustedCertificates; kj::ArrayPtr<const TlsCertificate> trustedCertificates;
// Additional certificates which should be trusted. Default: none. // Additional certificates which should be trusted. Default: none.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment