From 20980a3a285700462d2e0743b21b3f4c2e37e44e Mon Sep 17 00:00:00 2001 From: Sam Roberts Date: Wed, 11 Oct 2017 20:54:26 -0700 Subject: [PATCH] test: test TLS client authentication TLS client authentication should be tested, including failure scenarios. PR-URL: https://github.com/nodejs/node/pull/24733 Reviewed-By: Anna Henningsen Reviewed-By: Ben Noordhuis --- doc/api/tls.md | 2 + test/fixtures/tls-connect.js | 9 +- test/parallel/test-tls-client-auth.js | 331 ++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-tls-client-auth.js diff --git a/doc/api/tls.md b/doc/api/tls.md index af5dad83eeefab..526a01cdd676e8 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -1033,6 +1033,8 @@ changes: certificate can match or chain to. For self-signed certificates, the certificate is its own CA, and must be provided. + For PEM encoded certificates, supported types are "X509 CERTIFICATE", and + "CERTIFICATE". * `cert` {string|string[]|Buffer|Buffer[]} Cert chains in PEM format. One cert chain should be provided per private key. Each cert chain should consist of the PEM formatted certificate for a provided private `key`, followed by the diff --git a/test/fixtures/tls-connect.js b/test/fixtures/tls-connect.js index 303061adc3223e..764bea77033d28 100644 --- a/test/fixtures/tls-connect.js +++ b/test/fixtures/tls-connect.js @@ -26,15 +26,18 @@ const keys = exports.keys = { agent5: load('agent5', 'ca2'), agent6: load('agent6', 'ca1'), agent7: load('agent7', 'fake-cnnic-root'), + agent10: load('agent10', 'ca2'), + ec10: load('ec10', 'ca5'), ec: load('ec', 'ec'), }; -function load(cert, issuer) { - issuer = issuer || cert; // Assume self-signed if no issuer +// root is the self-signed root of the trust chain, not an intermediate ca. +function load(cert, root) { + root = root || cert; // Assume self-signed if no issuer const id = { key: fixtures.readKey(cert + '-key.pem', 'binary'), cert: fixtures.readKey(cert + '-cert.pem', 'binary'), - ca: fixtures.readKey(issuer + '-cert.pem', 'binary'), + ca: fixtures.readKey(root + '-cert.pem', 'binary'), }; return id; } diff --git a/test/parallel/test-tls-client-auth.js b/test/parallel/test-tls-client-auth.js new file mode 100644 index 00000000000000..1701981692280e --- /dev/null +++ b/test/parallel/test-tls-client-auth.js @@ -0,0 +1,331 @@ +'use strict'; + +require('../common'); +const fixtures = require('../common/fixtures'); + +const { + assert, connect, keys +} = require(fixtures.path('tls-connect')); + +// Use ec10 and agent10, they are the only identities with intermediate CAs. +const client = keys.ec10; +const server = keys.agent10; + +// The certificates aren't for "localhost", so override the identity check. +function checkServerIdentity(hostname, cert) { + assert.strictEqual(hostname, 'localhost'); + assert.strictEqual(cert.subject.CN, 'agent10.example.com'); +} + +// Split out the single end-entity cert and the subordinate CA for later use. +split(client.cert, client); +split(server.cert, server); + +function split(file, into) { + const certs = /([^]*END CERTIFICATE-----\r?\n)(-----BEGIN[^]*)/.exec(file); + assert.strictEqual(certs.length, 3); + into.single = certs[1]; + into.subca = certs[2]; +} + +// Typical setup, nothing special, complete cert chains sent to peer. +connect({ + client: { + key: client.key, + cert: client.cert, + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// As above, but without requesting client's cert. +connect({ + client: { + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// Request cert from client that doesn't have one. +connect({ + client: { + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'ECONNRESET'); + return cleanup(); +}); + +// Typical configuration error, incomplete cert chains sent, we have to know the +// peer's subordinate CAs in order to verify the peer. +connect({ + client: { + key: client.key, + cert: client.single, + ca: [server.ca, server.subca], + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.single, + ca: [client.ca, client.subca], + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// Like above, but provide root CA and subordinate CA as multi-PEM. +connect({ + client: { + key: client.key, + cert: client.single, + ca: server.ca + '\n' + server.subca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.single, + ca: client.ca + '\n' + client.subca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// Like above, but provide multi-PEM in an array. +connect({ + client: { + key: client.key, + cert: client.single, + ca: [server.ca + '\n' + server.subca], + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.single, + ca: [client.ca + '\n' + client.subca], + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// Fail to complete server's chain +connect({ + client: { + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.single, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'); + return cleanup(); +}); + +// Fail to complete client's chain. +connect({ + client: { + key: client.key, + cert: client.single, + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(pair.client.error); + assert.ifError(pair.server.error); + assert.strictEqual(err.code, 'ECONNRESET'); + return cleanup(); +}); + +// Fail to find CA for server. +connect({ + client: { + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'); + return cleanup(); +}); + +// Server sent their CA, but CA cannot be trusted if it is not locally known. +connect({ + client: { + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert + '\n' + server.ca, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'SELF_SIGNED_CERT_IN_CHAIN'); + return cleanup(); +}); + +// Server sent their CA, wrongly, but its OK since we know the CA locally. +connect({ + client: { + checkServerIdentity, + ca: server.ca, + }, + server: { + key: server.key, + cert: server.cert + '\n' + server.ca, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// Fail to complete client's chain. +connect({ + client: { + key: client.key, + cert: client.single, + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'ECONNRESET'); + return cleanup(); +}); + +// Fail to find CA for client. +connect({ + client: { + key: client.key, + cert: client.cert, + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'ECONNRESET'); + return cleanup(); +}); + +// Confirm lack of support for "BEGIN TRUSTED CERTIFICATE". +connect({ + client: { + key: client.key, + cert: client.cert, + ca: server.ca.replace(/CERTIFICATE/g, 'TRUSTED CERTIFICATE'), + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'); + return cleanup(); +}); + +// Confirm lack of support for "BEGIN TRUSTED CERTIFICATE". +connect({ + client: { + key: client.key, + cert: client.cert, + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca.replace(/CERTIFICATE/g, 'TRUSTED CERTIFICATE'), + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.strictEqual(err.code, 'ECONNRESET'); + return cleanup(); +}); + +// Confirm support for "BEGIN X509 CERTIFICATE". +connect({ + client: { + key: client.key, + cert: client.cert, + ca: server.ca.replace(/CERTIFICATE/g, 'X509 CERTIFICATE'), + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca, + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +}); + +// Confirm support for "BEGIN X509 CERTIFICATE". +connect({ + client: { + key: client.key, + cert: client.cert, + ca: server.ca, + checkServerIdentity, + }, + server: { + key: server.key, + cert: server.cert, + ca: client.ca.replace(/CERTIFICATE/g, 'X509 CERTIFICATE'), + requestCert: true, + }, +}, function(err, pair, cleanup) { + assert.ifError(err); + return cleanup(); +});