tls: support changing credentials dynamically

This commit adds a setSecureContext() method to TLS servers. In
order to maintain backwards compatibility, the method takes the
options needed to create a new SecureContext, rather than an
instance of SecureContext.

Fixes: https://github.com/nodejs/node/issues/4464
Refs: https://github.com/nodejs/node/issues/10349
Refs: https://github.com/nodejs/help/issues/603
Refs: https://github.com/nodejs/node/issues/15115
PR-URL: https://github.com/nodejs/node/pull/23644
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
This commit is contained in:
cjihrig 2018-10-13 14:18:31 -04:00
parent 517955a474
commit 96a986d675
No known key found for this signature in database
GPG Key ID: 7434390BDBE9B9C5
3 changed files with 221 additions and 31 deletions

View File

@ -411,6 +411,18 @@ encryption/decryption of the [TLS Session Tickets][].
Starts the server listening for encrypted connections. Starts the server listening for encrypted connections.
This method is identical to [`server.listen()`][] from [`net.Server`][]. This method is identical to [`server.listen()`][] from [`net.Server`][].
### server.setSecureContext(options)
<!-- YAML
added: REPLACEME
-->
* `options` {Object} An object containing any of the possible properties from
the [`tls.createSecureContext()`][] `options` arguments (e.g. `key`, `cert`,
`ca`, etc).
The `server.setSecureContext()` method replaces the secure context of an
existing server. Existing connections to the server are not interrupted.
### server.setTicketKeys(keys) ### server.setTicketKeys(keys)
<!-- YAML <!-- YAML
added: v3.0.0 added: v3.0.0

View File

@ -833,6 +833,120 @@ function Server(options, listener) {
// Handle option defaults: // Handle option defaults:
this.setOptions(options); this.setOptions(options);
// setSecureContext() overlaps with setOptions() quite a bit. setOptions()
// is an undocumented API that was probably never intended to be exposed
// publicly. Unfortunately, it would be a breaking change to just remove it,
// and there is at least one test that depends on it.
this.setSecureContext(options);
this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000);
this[kSNICallback] = options.SNICallback;
if (typeof this[kHandshakeTimeout] !== 'number') {
throw new ERR_INVALID_ARG_TYPE(
'options.handshakeTimeout', 'number', options.handshakeTimeout);
}
if (this[kSNICallback] && typeof this[kSNICallback] !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.SNICallback', 'function', options.SNICallback);
}
// constructor call
net.Server.call(this, tlsConnectionListener);
if (listener) {
this.on('secureConnection', listener);
}
}
util.inherits(Server, net.Server);
exports.Server = Server;
exports.createServer = function createServer(options, listener) {
return new Server(options, listener);
};
Server.prototype.setSecureContext = function(options) {
if (options === null || typeof options !== 'object')
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
if (options.pfx)
this.pfx = options.pfx;
else
this.pfx = undefined;
if (options.key)
this.key = options.key;
else
this.key = undefined;
if (options.passphrase)
this.passphrase = options.passphrase;
else
this.passphrase = undefined;
if (options.cert)
this.cert = options.cert;
else
this.cert = undefined;
if (options.clientCertEngine)
this.clientCertEngine = options.clientCertEngine;
else
this.clientCertEngine = undefined;
if (options.ca)
this.ca = options.ca;
else
this.ca = undefined;
if (options.secureProtocol)
this.secureProtocol = options.secureProtocol;
else
this.secureProtocol = undefined;
if (options.crl)
this.crl = options.crl;
else
this.crl = undefined;
if (options.ciphers)
this.ciphers = options.ciphers;
else
this.ciphers = undefined;
if (options.ecdhCurve !== undefined)
this.ecdhCurve = options.ecdhCurve;
else
this.ecdhCurve = undefined;
if (options.dhparam)
this.dhparam = options.dhparam;
else
this.dhparam = undefined;
if (options.honorCipherOrder !== undefined)
this.honorCipherOrder = !!options.honorCipherOrder;
else
this.honorCipherOrder = true;
const secureOptions = options.secureOptions || 0;
if (secureOptions)
this.secureOptions = secureOptions;
else
this.secureOptions = undefined;
if (options.sessionIdContext) {
this.sessionIdContext = options.sessionIdContext;
} else {
this.sessionIdContext = crypto.createHash('sha1')
.update(process.argv.join(' '))
.digest('hex')
.slice(0, 32);
}
this._sharedCreds = tls.createSecureContext({ this._sharedCreds = tls.createSecureContext({
pfx: this.pfx, pfx: this.pfx,
key: this.key, key: this.key,
@ -850,39 +964,15 @@ function Server(options, listener) {
sessionIdContext: this.sessionIdContext sessionIdContext: this.sessionIdContext
}); });
this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000); if (this.sessionTimeout)
this[kSNICallback] = options.SNICallback;
if (typeof this[kHandshakeTimeout] !== 'number') {
throw new ERR_INVALID_ARG_TYPE(
'options.handshakeTimeout', 'number', options.handshakeTimeout);
}
if (this[kSNICallback] && typeof this[kSNICallback] !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.SNICallback', 'function', options.SNICallback);
}
if (this.sessionTimeout) {
this._sharedCreds.context.setSessionTimeout(this.sessionTimeout); this._sharedCreds.context.setSessionTimeout(this.sessionTimeout);
if (options.ticketKeys) {
this.ticketKeys = options.ticketKeys;
this.setTicketKeys(this.ticketKeys);
} else {
this.setTicketKeys(this.getTicketKeys());
} }
if (this.ticketKeys) {
this._sharedCreds.context.setTicketKeys(this.ticketKeys);
}
// constructor call
net.Server.call(this, tlsConnectionListener);
if (listener) {
this.on('secureConnection', listener);
}
}
util.inherits(Server, net.Server);
exports.Server = Server;
exports.createServer = function createServer(options, listener) {
return new Server(options, listener);
}; };

View File

@ -0,0 +1,88 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const fixtures = require('../common/fixtures');
const credentialOptions = [
{
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
ca: fixtures.readKey('ca1-cert.pem')
},
{
key: fixtures.readKey('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem'),
ca: fixtures.readKey('ca2-cert.pem')
}
];
let requestsCount = 0;
let firstResponse;
const server = https.createServer(credentialOptions[0], (req, res) => {
requestsCount++;
if (requestsCount === 1) {
firstResponse = res;
firstResponse.write('multi-');
return;
} else if (requestsCount === 3) {
firstResponse.write('success-');
}
res.end('success');
});
server.listen(0, common.mustCall(async () => {
const { port } = server.address();
const firstRequest = makeRequest(port);
assert.strictEqual(await makeRequest(port), 'success');
server.setSecureContext(credentialOptions[1]);
firstResponse.write('request-');
await assert.rejects(async () => {
await makeRequest(port);
}, /^Error: self signed certificate$/);
server.setSecureContext(credentialOptions[0]);
assert.strictEqual(await makeRequest(port), 'success');
server.setSecureContext(credentialOptions[1]);
firstResponse.end('fun!');
await assert.rejects(async () => {
await makeRequest(port);
}, /^Error: self signed certificate$/);
assert.strictEqual(await firstRequest, 'multi-request-success-fun!');
server.close();
}));
function makeRequest(port) {
return new Promise((resolve, reject) => {
const options = {
rejectUnauthorized: true,
ca: credentialOptions[0].ca,
servername: 'agent1'
};
https.get(`https://localhost:${port}`, options, (res) => {
let response = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
response += chunk;
});
res.on('end', common.mustCall(() => {
resolve(response);
}));
}).on('error', (err) => {
reject(err);
});
});
}