diff options
author | cjihrig <cjihrig@gmail.com> | 2018-10-13 14:18:31 -0400 |
---|---|---|
committer | James M Snell <jasnell@gmail.com> | 2018-10-21 12:27:11 -0700 |
commit | bed4a8c6e056c9ea74215875269e457f8f4ac268 (patch) | |
tree | 0c01020d59fe0728d3bd79049ea96c428834675f | |
parent | 7db4281e526aec23b42eac19c7f50ed8e20bd962 (diff) | |
download | node-new-bed4a8c6e056c9ea74215875269e457f8f4ac268.tar.gz |
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>
-rw-r--r-- | doc/api/tls.md | 12 | ||||
-rw-r--r-- | lib/_tls_wrap.js | 138 | ||||
-rw-r--r-- | test/parallel/test-tls-set-secure-context.js | 88 |
3 files changed, 214 insertions, 24 deletions
diff --git a/doc/api/tls.md b/doc/api/tls.md index 4f3430854e..f9a313f914 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -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 diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index b28e3b62b7..aa8b66b715 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -833,22 +833,11 @@ function Server(options, listener) { // Handle option defaults: this.setOptions(options); - this._sharedCreds = tls.createSecureContext({ - pfx: this.pfx, - key: this.key, - passphrase: this.passphrase, - cert: this.cert, - clientCertEngine: this.clientCertEngine, - ca: this.ca, - ciphers: this.ciphers, - ecdhCurve: this.ecdhCurve, - dhparam: this.dhparam, - secureProtocol: this.secureProtocol, - secureOptions: this.secureOptions, - honorCipherOrder: this.honorCipherOrder, - crl: this.crl, - sessionIdContext: this.sessionIdContext - }); + // 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; @@ -863,14 +852,6 @@ function Server(options, listener) { 'options.SNICallback', 'function', options.SNICallback); } - if (this.sessionTimeout) { - this._sharedCreds.context.setSessionTimeout(this.sessionTimeout); - } - - if (this.ticketKeys) { - this._sharedCreds.context.setTicketKeys(this.ticketKeys); - } - // constructor call net.Server.call(this, tlsConnectionListener); @@ -886,6 +867,115 @@ exports.createServer = function createServer(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, + passphrase: this.passphrase, + cert: this.cert, + clientCertEngine: this.clientCertEngine, + ca: this.ca, + ciphers: this.ciphers, + ecdhCurve: this.ecdhCurve, + dhparam: this.dhparam, + secureProtocol: this.secureProtocol, + secureOptions: this.secureOptions, + honorCipherOrder: this.honorCipherOrder, + crl: this.crl, + sessionIdContext: this.sessionIdContext + }); + + 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()); + } +}; + + Server.prototype._getServerData = function() { return { ticketKeys: this.getTicketKeys().toString('hex') diff --git a/test/parallel/test-tls-set-secure-context.js b/test/parallel/test-tls-set-secure-context.js new file mode 100644 index 0000000000..51ab3af10b --- /dev/null +++ b/test/parallel/test-tls-set-secure-context.js @@ -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); + }); + }); +} |