roster-server 3.2.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/index.js +17 -7
- package/package.json +1 -1
- package/test/https-integration.test.js +3 -2
- package/test/static-and-tls.test.js +38 -0
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);
|
|
@@ -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
|
}
|
|
@@ -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 });
|