Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test/document TLS authentication, then add support for TRUSTED CERTIFICATE pem headers #24733

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,9 @@ argument.
<!-- YAML
added: v0.11.13
changes:
- version: REPLACEME
pr-url: REPLACEME
description: The `ca:` option now supports `BEGIN TRUSTED CERTIFICATE`.
- version: v11.4.0
pr-url: https://github.com/nodejs/node/pull/24405
description: The `minVersion` and `maxVersion` can be used to restrict
Expand Down Expand Up @@ -1092,6 +1095,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 "TRUSTED CERTIFICATE",
"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
Expand Down
2 changes: 1 addition & 1 deletion src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ void SecureContext::AddCACert(const FunctionCallbackInfo<Value>& args) {
return;

X509_STORE* cert_store = SSL_CTX_get_cert_store(sc->ctx_.get());
while (X509* x509 = PEM_read_bio_X509(
while (X509* x509 = PEM_read_bio_X509_AUX(
bio.get(), nullptr, NoPasswordCallback, nullptr)) {
if (cert_store == root_cert_store) {
cert_store = NewRootCertStore();
Expand Down
9 changes: 6 additions & 3 deletions test/fixtures/tls-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
331 changes: 331 additions & 0 deletions test/parallel/test-tls-client-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
'use strict';

require('../common');
const fixtures = require('../common/fixtures');

const {
assert, connect, keys
} = require(fixtures.path('tls-connect'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this module maybe rather be in test/common/?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. It predates test/common, AFAICT, but perhaps it should have been moved there when test/common was created? Moving it would be a good code-n-learn activity.


// 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap in common.mustCall(...) here and below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The common connect() function does that already.

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 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.ifError(err);
return cleanup();
});

// Confirm 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.ifError(err);
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();
});