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: nodejs#4464
Refs: nodejs#10349
Refs: nodejs/help#603
Refs: nodejs#15115
PR-URL: nodejs#23644
Reviewed-By: Ben Noordhuis <[email protected]>
  • Loading branch information
cjihrig authored and sam-github committed Apr 29, 2019
1 parent f54db0b commit f47e208
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 25 deletions.
12 changes: 12 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,18 @@ See [Session Resumption][] for more information.
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
140 changes: 115 additions & 25 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,115 @@ 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);
}

// 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,
Expand All @@ -886,34 +995,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.sessionTimeout) {
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);

if (listener) {
this.on('secureConnection', listener);
if (options.ticketKeys) {
this.ticketKeys = options.ticketKeys;
this.setTicketKeys(this.ticketKeys);
} else {
this.setTicketKeys(this.getTicketKeys());
}
}

util.inherits(Server, net.Server);
exports.Server = Server;
exports.createServer = function createServer(options, listener) {
return new Server(options, listener);
};


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 f47e208

Please sign in to comment.