Skip to content

Commit bed4a8c

Browse files
cjihrigjasnell
authored andcommitted
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: #4464 Refs: #10349 Refs: nodejs/help#603 Refs: #15115 PR-URL: #23644 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1 parent 7db4281 commit bed4a8c

File tree

3 files changed

+214
-24
lines changed

3 files changed

+214
-24
lines changed

doc/api/tls.md

+12
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,18 @@ encryption/decryption of the [TLS Session Tickets][].
411411
Starts the server listening for encrypted connections.
412412
This method is identical to [`server.listen()`][] from [`net.Server`][].
413413

414+
### server.setSecureContext(options)
415+
<!-- YAML
416+
added: REPLACEME
417+
-->
418+
419+
* `options` {Object} An object containing any of the possible properties from
420+
the [`tls.createSecureContext()`][] `options` arguments (e.g. `key`, `cert`,
421+
`ca`, etc).
422+
423+
The `server.setSecureContext()` method replaces the secure context of an
424+
existing server. Existing connections to the server are not interrupted.
425+
414426
### server.setTicketKeys(keys)
415427
<!-- YAML
416428
added: v3.0.0

lib/_tls_wrap.js

+114-24
Original file line numberDiff line numberDiff line change
@@ -833,22 +833,11 @@ function Server(options, listener) {
833833
// Handle option defaults:
834834
this.setOptions(options);
835835

836-
this._sharedCreds = tls.createSecureContext({
837-
pfx: this.pfx,
838-
key: this.key,
839-
passphrase: this.passphrase,
840-
cert: this.cert,
841-
clientCertEngine: this.clientCertEngine,
842-
ca: this.ca,
843-
ciphers: this.ciphers,
844-
ecdhCurve: this.ecdhCurve,
845-
dhparam: this.dhparam,
846-
secureProtocol: this.secureProtocol,
847-
secureOptions: this.secureOptions,
848-
honorCipherOrder: this.honorCipherOrder,
849-
crl: this.crl,
850-
sessionIdContext: this.sessionIdContext
851-
});
836+
// setSecureContext() overlaps with setOptions() quite a bit. setOptions()
837+
// is an undocumented API that was probably never intended to be exposed
838+
// publicly. Unfortunately, it would be a breaking change to just remove it,
839+
// and there is at least one test that depends on it.
840+
this.setSecureContext(options);
852841

853842
this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000);
854843
this[kSNICallback] = options.SNICallback;
@@ -863,14 +852,6 @@ function Server(options, listener) {
863852
'options.SNICallback', 'function', options.SNICallback);
864853
}
865854

866-
if (this.sessionTimeout) {
867-
this._sharedCreds.context.setSessionTimeout(this.sessionTimeout);
868-
}
869-
870-
if (this.ticketKeys) {
871-
this._sharedCreds.context.setTicketKeys(this.ticketKeys);
872-
}
873-
874855
// constructor call
875856
net.Server.call(this, tlsConnectionListener);
876857

@@ -886,6 +867,115 @@ exports.createServer = function createServer(options, listener) {
886867
};
887868

888869

870+
Server.prototype.setSecureContext = function(options) {
871+
if (options === null || typeof options !== 'object')
872+
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
873+
874+
if (options.pfx)
875+
this.pfx = options.pfx;
876+
else
877+
this.pfx = undefined;
878+
879+
if (options.key)
880+
this.key = options.key;
881+
else
882+
this.key = undefined;
883+
884+
if (options.passphrase)
885+
this.passphrase = options.passphrase;
886+
else
887+
this.passphrase = undefined;
888+
889+
if (options.cert)
890+
this.cert = options.cert;
891+
else
892+
this.cert = undefined;
893+
894+
if (options.clientCertEngine)
895+
this.clientCertEngine = options.clientCertEngine;
896+
else
897+
this.clientCertEngine = undefined;
898+
899+
if (options.ca)
900+
this.ca = options.ca;
901+
else
902+
this.ca = undefined;
903+
904+
if (options.secureProtocol)
905+
this.secureProtocol = options.secureProtocol;
906+
else
907+
this.secureProtocol = undefined;
908+
909+
if (options.crl)
910+
this.crl = options.crl;
911+
else
912+
this.crl = undefined;
913+
914+
if (options.ciphers)
915+
this.ciphers = options.ciphers;
916+
else
917+
this.ciphers = undefined;
918+
919+
if (options.ecdhCurve !== undefined)
920+
this.ecdhCurve = options.ecdhCurve;
921+
else
922+
this.ecdhCurve = undefined;
923+
924+
if (options.dhparam)
925+
this.dhparam = options.dhparam;
926+
else
927+
this.dhparam = undefined;
928+
929+
if (options.honorCipherOrder !== undefined)
930+
this.honorCipherOrder = !!options.honorCipherOrder;
931+
else
932+
this.honorCipherOrder = true;
933+
934+
const secureOptions = options.secureOptions || 0;
935+
936+
if (secureOptions)
937+
this.secureOptions = secureOptions;
938+
else
939+
this.secureOptions = undefined;
940+
941+
if (options.sessionIdContext) {
942+
this.sessionIdContext = options.sessionIdContext;
943+
} else {
944+
this.sessionIdContext = crypto.createHash('sha1')
945+
.update(process.argv.join(' '))
946+
.digest('hex')
947+
.slice(0, 32);
948+
}
949+
950+
this._sharedCreds = tls.createSecureContext({
951+
pfx: this.pfx,
952+
key: this.key,
953+
passphrase: this.passphrase,
954+
cert: this.cert,
955+
clientCertEngine: this.clientCertEngine,
956+
ca: this.ca,
957+
ciphers: this.ciphers,
958+
ecdhCurve: this.ecdhCurve,
959+
dhparam: this.dhparam,
960+
secureProtocol: this.secureProtocol,
961+
secureOptions: this.secureOptions,
962+
honorCipherOrder: this.honorCipherOrder,
963+
crl: this.crl,
964+
sessionIdContext: this.sessionIdContext
965+
});
966+
967+
if (this.sessionTimeout)
968+
this._sharedCreds.context.setSessionTimeout(this.sessionTimeout);
969+
970+
if (options.ticketKeys) {
971+
this.ticketKeys = options.ticketKeys;
972+
this.setTicketKeys(this.ticketKeys);
973+
} else {
974+
this.setTicketKeys(this.getTicketKeys());
975+
}
976+
};
977+
978+
889979
Server.prototype._getServerData = function() {
890980
return {
891981
ticketKeys: this.getTicketKeys().toString('hex')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const https = require('https');
9+
const fixtures = require('../common/fixtures');
10+
const credentialOptions = [
11+
{
12+
key: fixtures.readKey('agent1-key.pem'),
13+
cert: fixtures.readKey('agent1-cert.pem'),
14+
ca: fixtures.readKey('ca1-cert.pem')
15+
},
16+
{
17+
key: fixtures.readKey('agent2-key.pem'),
18+
cert: fixtures.readKey('agent2-cert.pem'),
19+
ca: fixtures.readKey('ca2-cert.pem')
20+
}
21+
];
22+
let requestsCount = 0;
23+
let firstResponse;
24+
25+
const server = https.createServer(credentialOptions[0], (req, res) => {
26+
requestsCount++;
27+
28+
if (requestsCount === 1) {
29+
firstResponse = res;
30+
firstResponse.write('multi-');
31+
return;
32+
} else if (requestsCount === 3) {
33+
firstResponse.write('success-');
34+
}
35+
36+
res.end('success');
37+
});
38+
39+
server.listen(0, common.mustCall(async () => {
40+
const { port } = server.address();
41+
const firstRequest = makeRequest(port);
42+
43+
assert.strictEqual(await makeRequest(port), 'success');
44+
45+
server.setSecureContext(credentialOptions[1]);
46+
firstResponse.write('request-');
47+
await assert.rejects(async () => {
48+
await makeRequest(port);
49+
}, /^Error: self signed certificate$/);
50+
51+
server.setSecureContext(credentialOptions[0]);
52+
assert.strictEqual(await makeRequest(port), 'success');
53+
54+
server.setSecureContext(credentialOptions[1]);
55+
firstResponse.end('fun!');
56+
await assert.rejects(async () => {
57+
await makeRequest(port);
58+
}, /^Error: self signed certificate$/);
59+
60+
assert.strictEqual(await firstRequest, 'multi-request-success-fun!');
61+
server.close();
62+
}));
63+
64+
function makeRequest(port) {
65+
return new Promise((resolve, reject) => {
66+
const options = {
67+
rejectUnauthorized: true,
68+
ca: credentialOptions[0].ca,
69+
servername: 'agent1'
70+
};
71+
72+
https.get(`https://localhost:${port}`, options, (res) => {
73+
let response = '';
74+
75+
res.setEncoding('utf8');
76+
77+
res.on('data', (chunk) => {
78+
response += chunk;
79+
});
80+
81+
res.on('end', common.mustCall(() => {
82+
resolve(response);
83+
}));
84+
}).on('error', (err) => {
85+
reject(err);
86+
});
87+
});
88+
}

0 commit comments

Comments
 (0)