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:
parent
517955a474
commit
96a986d675
@ -411,6 +411,18 @@ encryption/decryption of the [TLS Session Tickets][].
|
||||
Starts the server listening for encrypted connections.
|
||||
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)
|
||||
<!-- YAML
|
||||
added: v3.0.0
|
||||
|
152
lib/_tls_wrap.js
152
lib/_tls_wrap.js
@ -833,6 +833,120 @@ function Server(options, listener) {
|
||||
// Handle option defaults:
|
||||
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({
|
||||
pfx: this.pfx,
|
||||
key: this.key,
|
||||
@ -850,39 +964,15 @@ function Server(options, listener) {
|
||||
sessionIdContext: this.sessionIdContext
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.sessionTimeout) {
|
||||
if (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);
|
||||
};
|
||||
|
||||
|
||||
|
88
test/parallel/test-tls-set-secure-context.js
Normal file
88
test/parallel/test-tls-set-secure-context.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user