Skip to content

Commit

Permalink
tls: support changing credentials dynamically
Browse files Browse the repository at this point in the history
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: #4464
Refs: #10349
Refs: nodejs/help#603
Refs: #15115
PR-URL: #23644
Reviewed-By: Ben Noordhuis <[email protected]>
  • Loading branch information
cjihrig authored and jasnell committed Oct 21, 2018
1 parent 7db4281 commit bed4a8c
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 24 deletions.
12 changes: 12 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
138 changes: 114 additions & 24 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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')
Expand Down
88 changes: 88 additions & 0 deletions test/parallel/test-tls-set-secure-context.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
}

0 comments on commit bed4a8c

Please sign in to comment.