roster-server 3.1.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/index.js +18 -8
- package/package.json +1 -1
- package/test/https-integration.test.js +3 -2
- package/test/roster-server.test.js +2 -2
- package/test/static-and-tls.test.js +38 -0
package/README.md
CHANGED
|
@@ -272,7 +272,7 @@ const dnsChallenge = {
|
|
|
272
272
|
|
|
273
273
|
Pass this as the constructor's `dnsChallenge` option. To use another provider, install its Greenlock DNS plugin in the consuming application and supply its module name and provider options. Configure DNS timing on that object with `propagationDelay`, `dryRunDelay`, `dnsPollIntervalMs`, and `dnsPollTimeoutMs`; the wrapper's polling interval defaults to `15000` ms.
|
|
274
274
|
|
|
275
|
-
`
|
|
275
|
+
New certificates use an `EC-P256` key by default, which makes each full TLS handshake much cheaper for the server than an RSA key. Clients that cannot negotiate ECDSA, such as Windows XP or Android 2.x, need `serverKeyType: 'RSA-2048'`. The key type applies to certificates issued after it is set: renewal reuses a certificate's existing key, so certificates issued before 3.2.0 keep their RSA key. To move an existing name to the configured key type, remove its directory under `greenlockStorePath/live/` and restart; Roster issues a new certificate on the first connection for that name.
|
|
276
276
|
|
|
277
277
|
Roster's file-based SNI resolvers cache TLS contexts and detect changed certificate files through metadata, including updates from another process. Concurrent requests for the same certificate name share loads and checks. Standalone startup reuses the initialized certificate runtime and Roster's cancellable renewal loop. Loading an existing certificate does not itself force renewal.
|
|
278
278
|
|
|
@@ -299,7 +299,7 @@ Pass operational settings through the constructor, using the consuming applicati
|
|
|
299
299
|
| `isolation` | `true` (`false` with `cluster`) | One process per site directory under `start()`; `false` or `{ restartDelayMs, backlog, helloTimeoutMs }` (defaults `1000`, `511`, `10000`). See [site isolation](#site-isolation). |
|
|
300
300
|
| `tlsMinVersion`, `tlsMaxVersion` | `TLSv1.2`, `TLSv1.3` | Protocol limits for created HTTPS servers. |
|
|
301
301
|
| `keepAliveTimeoutMs` | Node.js default (`5000`) | Idle time before a site server closes a keep-alive connection, in isolated and main-process sites. Set it above the interval at which clients repeat requests so they reuse the connection instead of paying a new TLS handshake. |
|
|
302
|
-
| `serverKeyType` | `
|
|
302
|
+
| `serverKeyType` | `EC-P256` | Key for newly issued certificates: `EC-P256`, `EC-P384`, `RSA-2048`, `RSA-3072`, or `RSA-4096`. See [certificates](#certificates-and-dns). |
|
|
303
303
|
| `skipLocalCheck` | `true` | Skips Greenlock dry-run/local challenge checks. |
|
|
304
304
|
| `dnsChallenge` | CLI wrapper | DNS-01 options or `false`; see [certificates](#certificates-and-dns). |
|
|
305
305
|
| `disableWildcard`, `combineWildcardCerts` | `false`, `false` | Wildcard registration and certificate behavior. |
|
package/index.js
CHANGED
|
@@ -139,6 +139,12 @@ function buildCertLookupCandidates(servername) {
|
|
|
139
139
|
return candidates;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// The certificate stored for a host's own subject serves it; a wildcard or zone
|
|
143
|
+
// certificate found as a fallback serves only the names it covers.
|
|
144
|
+
function certDirServesHost(certDir, host, certificate) {
|
|
145
|
+
return certDirCandidatesForSubject(host).includes(certDir) || certificate.checkHost(host) !== undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
function certCoversName(certPem, name) {
|
|
143
149
|
try {
|
|
144
150
|
const x509 = new crypto.X509Certificate(certPem);
|
|
@@ -360,7 +366,7 @@ class Roster {
|
|
|
360
366
|
if (this.keepAliveTimeoutMs !== undefined && (!Number.isFinite(this.keepAliveTimeoutMs) || this.keepAliveTimeoutMs <= 0)) {
|
|
361
367
|
throw new TypeError('keepAliveTimeoutMs must be a positive finite number');
|
|
362
368
|
}
|
|
363
|
-
this.serverKeyType = options.serverKeyType ?? '
|
|
369
|
+
this.serverKeyType = options.serverKeyType ?? 'EC-P256';
|
|
364
370
|
if (!SERVER_KEY_TYPES.includes(this.serverKeyType)) {
|
|
365
371
|
throw new TypeError(`serverKeyType must be one of ${SERVER_KEY_TYPES.join(', ')}`);
|
|
366
372
|
}
|
|
@@ -887,7 +893,7 @@ class Roster {
|
|
|
887
893
|
const candidates = buildCertLookupCandidates(host);
|
|
888
894
|
for (const candidate of candidates) {
|
|
889
895
|
const pems = this._loadCert(candidate);
|
|
890
|
-
if (pems) return pems;
|
|
896
|
+
if (pems && certDirServesHost(candidate, host, new crypto.X509Certificate(pems.cert))) return pems;
|
|
891
897
|
}
|
|
892
898
|
return null;
|
|
893
899
|
}
|
|
@@ -1096,8 +1102,8 @@ class Roster {
|
|
|
1096
1102
|
pending = this._loadSecureContext(subject).finally(() => this._contextLoads.delete(subject));
|
|
1097
1103
|
this._contextLoads.set(subject, pending);
|
|
1098
1104
|
}
|
|
1099
|
-
const
|
|
1100
|
-
if (
|
|
1105
|
+
const loaded = await pending;
|
|
1106
|
+
if (loaded && certDirServesHost(subject, host, loaded.certificate)) return loaded.context;
|
|
1101
1107
|
}
|
|
1102
1108
|
return null;
|
|
1103
1109
|
}
|
|
@@ -1116,12 +1122,16 @@ class Roster {
|
|
|
1116
1122
|
this._assertOpen();
|
|
1117
1123
|
const version = stats.map(stat => `${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`).join('|');
|
|
1118
1124
|
const cached = this._secureContexts.get(subject);
|
|
1119
|
-
if (cached && cached.version === version) return cached
|
|
1125
|
+
if (cached && cached.version === version) return cached;
|
|
1120
1126
|
const [key, cert, chain] = await Promise.all(files.map(file => fs.promises.readFile(file, 'utf8')));
|
|
1121
1127
|
this._assertOpen();
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1128
|
+
const loaded = {
|
|
1129
|
+
version,
|
|
1130
|
+
context: tls.createSecureContext({ key, cert: cert + chain }),
|
|
1131
|
+
certificate: new crypto.X509Certificate(cert)
|
|
1132
|
+
};
|
|
1133
|
+
this._secureContexts.set(subject, loaded);
|
|
1134
|
+
return loaded;
|
|
1125
1135
|
}
|
|
1126
1136
|
|
|
1127
1137
|
_checkCertificate(servername) {
|
package/package.json
CHANGED
|
@@ -32,8 +32,9 @@ function certificate(roster, subject, serial) {
|
|
|
32
32
|
const key = path.join(directory, 'privkey.pem');
|
|
33
33
|
const cert = path.join(directory, 'cert.pem');
|
|
34
34
|
const keyArgs = fs.existsSync(key) ? ['-key', key] : ['-newkey', 'rsa:2048', '-nodes', '-keyout', key];
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const name = subject.replace(/^_wildcard_\./, '*.');
|
|
36
|
+
execFileSync('openssl', ['req', '-x509', ...keyArgs, '-out', cert, '-subj', '/CN=example.com',
|
|
37
|
+
'-addext', `subjectAltName=DNS:${name}`, '-days', '1', '-set_serial', String(serial)], { stdio: 'ignore' });
|
|
37
38
|
fs.writeFileSync(path.join(directory, 'chain.pem'), '');
|
|
38
39
|
return directory;
|
|
39
40
|
}
|
|
@@ -822,8 +822,8 @@ describe('Roster generateConfigJson', () => {
|
|
|
822
822
|
roster.generateConfigJson();
|
|
823
823
|
return JSON.parse(fs.readFileSync(path.join(tmpDir, 'config.json'), 'utf8')).defaults.serverKeyType;
|
|
824
824
|
};
|
|
825
|
-
assert.strictEqual(keyTypeFor({}), '
|
|
826
|
-
assert.strictEqual(keyTypeFor({ serverKeyType: '
|
|
825
|
+
assert.strictEqual(keyTypeFor({}), 'EC-P256');
|
|
826
|
+
assert.strictEqual(keyTypeFor({ serverKeyType: 'RSA-2048' }), 'RSA-2048');
|
|
827
827
|
} finally {
|
|
828
828
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
829
829
|
}
|
|
@@ -218,6 +218,44 @@ it('reuses TLS contexts, coalesces concurrent loads, and reloads changed certifi
|
|
|
218
218
|
assert.equal(roster._secureContexts.size, 0);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
function installCertificate(root, directoryName, names) {
|
|
222
|
+
const directory = path.join(root, 'live', directoryName);
|
|
223
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
224
|
+
execFileSync('openssl', ['req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes',
|
|
225
|
+
'-keyout', path.join(directory, 'privkey.pem'), '-out', path.join(directory, 'cert.pem'),
|
|
226
|
+
'-subj', `/CN=${names[0]}`, '-addext', `subjectAltName=${names.map(name => `DNS:${name}`).join(',')}`,
|
|
227
|
+
'-days', '1'], { stdio: 'ignore' });
|
|
228
|
+
fs.writeFileSync(path.join(directory, 'chain.pem'), '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
it('falls back to zone and wildcard certificates only when they cover the host', async t => {
|
|
232
|
+
const root = temporaryDirectory(t);
|
|
233
|
+
installCertificate(root, 'example.com', ['example.com', 'www.example.com']);
|
|
234
|
+
installCertificate(root, '_wildcard_.wild.example.com', ['*.wild.example.com']);
|
|
235
|
+
const roster = new Roster({ local: true, wwwPath: root, greenlockStorePath: root });
|
|
236
|
+
t.after(() => roster.close());
|
|
237
|
+
const issued = [];
|
|
238
|
+
roster._greenlockRuntime = {
|
|
239
|
+
get: async ({ servername }) => {
|
|
240
|
+
issued.push(servername);
|
|
241
|
+
installCertificate(root, servername, [servername]);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const www = await roster._getSecureContext('www.example.com');
|
|
246
|
+
assert.equal(www, roster._secureContexts.get('example.com').context);
|
|
247
|
+
const wild = await roster._getSecureContext('shop.wild.example.com');
|
|
248
|
+
assert.equal(wild, roster._secureContexts.get('_wildcard_.wild.example.com').context);
|
|
249
|
+
assert.deepEqual(issued, []);
|
|
250
|
+
|
|
251
|
+
const uncovered = await roster._getSecureContext('api.example.com');
|
|
252
|
+
assert.deepEqual(issued, ['api.example.com']);
|
|
253
|
+
assert.equal(uncovered, roster._secureContexts.get('api.example.com').context);
|
|
254
|
+
|
|
255
|
+
assert.match(roster._resolvePemsForServername('www.example.com').cert, /BEGIN CERTIFICATE/);
|
|
256
|
+
assert.equal(roster._resolvePemsForServername('mail.example.com'), null);
|
|
257
|
+
});
|
|
258
|
+
|
|
221
259
|
it('coalesces certificate checks and prevents new issuance after close', async t => {
|
|
222
260
|
const root = temporaryDirectory(t);
|
|
223
261
|
const roster = new Roster({ local: true, wwwPath: root });
|