Skip to content

Commit f47e208

Browse files
cjihrigsam-github
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: nodejs#4464 Refs: nodejs#10349 Refs: nodejs/help#603 Refs: nodejs#15115 PR-URL: nodejs#23644 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1 parent f54db0b commit f47e208

File tree

3 files changed

+215
-25
lines changed

3 files changed

+215
-25
lines changed

doc/api/tls.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,18 @@ See [Session Resumption][] for more information.
507507
Starts the server listening for encrypted connections.
508508
This method is identical to [`server.listen()`][] from [`net.Server`][].
509509

510+
### server.setSecureContext(options)
511+
<!-- YAML
512+
added: REPLACEME
513+
-->
514+
515+
* `options` {Object} An object containing any of the possible properties from
516+
the [`tls.createSecureContext()`][] `options` arguments (e.g. `key`, `cert`,
517+
`ca`, etc).
518+
519+
The `server.setSecureContext()` method replaces the secure context of an
520+
existing server. Existing connections to the server are not interrupted.
521+
510522
### server.setTicketKeys(keys)
511523
<!-- YAML
512524
added: v3.0.0

lib/_tls_wrap.js

Lines changed: 115 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,115 @@ function Server(options, listener) {
867867
// Handle option defaults:
868868
this.setOptions(options);
869869

870+
// setSecureContext() overlaps with setOptions() quite a bit. setOptions()
871+
// is an undocumented API that was probably never intended to be exposed
872+
// publicly. Unfortunately, it would be a breaking change to just remove it,
873+
// and there is at least one test that depends on it.
874+
this.setSecureContext(options);
875+
876+
this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000);
877+
this[kSNICallback] = options.SNICallback;
878+
879+
if (typeof this[kHandshakeTimeout] !== 'number') {
880+
throw new ERR_INVALID_ARG_TYPE(
881+
'options.handshakeTimeout', 'number', options.handshakeTimeout);
882+
}
883+
884+
// constructor call
885+
net.Server.call(this, tlsConnectionListener);
886+
887+
if (listener) {
888+
this.on('secureConnection', listener);
889+
}
890+
}
891+
892+
util.inherits(Server, net.Server);
893+
exports.Server = Server;
894+
exports.createServer = function createServer(options, listener) {
895+
return new Server(options, listener);
896+
};
897+
898+
899+
Server.prototype.setSecureContext = function(options) {
900+
if (options === null || typeof options !== 'object')
901+
throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
902+
903+
if (options.pfx)
904+
this.pfx = options.pfx;
905+
else
906+
this.pfx = undefined;
907+
908+
if (options.key)
909+
this.key = options.key;
910+
else
911+
this.key = undefined;
912+
913+
if (options.passphrase)
914+
this.passphrase = options.passphrase;
915+
else
916+
this.passphrase = undefined;
917+
918+
if (options.cert)
919+
this.cert = options.cert;
920+
else
921+
this.cert = undefined;
922+
923+
if (options.clientCertEngine)
924+
this.clientCertEngine = options.clientCertEngine;
925+
else
926+
this.clientCertEngine = undefined;
927+
928+
if (options.ca)
929+
this.ca = options.ca;
930+
else
931+
this.ca = undefined;
932+
933+
if (options.secureProtocol)
934+
this.secureProtocol = options.secureProtocol;
935+
else
936+
this.secureProtocol = undefined;
937+
938+
if (options.crl)
939+
this.crl = options.crl;
940+
else
941+
this.crl = undefined;
942+
943+
if (options.ciphers)
944+
this.ciphers = options.ciphers;
945+
else
946+
this.ciphers = undefined;
947+
948+
if (options.ecdhCurve !== undefined)
949+
this.ecdhCurve = options.ecdhCurve;
950+
else
951+
this.ecdhCurve = undefined;
952+
953+
if (options.dhparam)
954+
this.dhparam = options.dhparam;
955+
else
956+
this.dhparam = undefined;
957+
958+
if (options.honorCipherOrder !== undefined)
959+
this.honorCipherOrder = !!options.honorCipherOrder;
960+
else
961+
this.honorCipherOrder = true;
962+
963+
const secureOptions = options.secureOptions || 0;
964+
965+
if (secureOptions)
966+
this.secureOptions = secureOptions;
967+
else
968+
this.secureOptions = undefined;
969+
970+
if (options.sessionIdContext) {
971+
this.sessionIdContext = options.sessionIdContext;
972+
} else {
973+
this.sessionIdContext = crypto.createHash('sha1')
974+
.update(process.argv.join(' '))
975+
.digest('hex')
976+
.slice(0, 32);
977+
}
978+
870979
this._sharedCreds = tls.createSecureContext({
871980
pfx: this.pfx,
872981
key: this.key,
@@ -886,34 +995,15 @@ function Server(options, listener) {
886995
sessionIdContext: this.sessionIdContext
887996
});
888997

889-
this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000);
890-
this[kSNICallback] = options.SNICallback;
891-
892-
if (typeof this[kHandshakeTimeout] !== 'number') {
893-
throw new ERR_INVALID_ARG_TYPE(
894-
'options.handshakeTimeout', 'number', options.handshakeTimeout);
895-
}
896-
897-
if (this.sessionTimeout) {
998+
if (this.sessionTimeout)
898999
this._sharedCreds.context.setSessionTimeout(this.sessionTimeout);
899-
}
9001000

901-
if (this.ticketKeys) {
902-
this._sharedCreds.context.setTicketKeys(this.ticketKeys);
903-
}
904-
905-
// constructor call
906-
net.Server.call(this, tlsConnectionListener);
907-
908-
if (listener) {
909-
this.on('secureConnection', listener);
1001+
if (options.ticketKeys) {
1002+
this.ticketKeys = options.ticketKeys;
1003+
this.setTicketKeys(this.ticketKeys);
1004+
} else {
1005+
this.setTicketKeys(this.getTicketKeys());
9101006
}
911-
}
912-
913-
util.inherits(Server, net.Server);
914-
exports.Server = Server;
915-
exports.createServer = function createServer(options, listener) {
916-
return new Server(options, listener);
9171007
};
9181008

9191009

Lines changed: 88 additions & 0 deletions
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)