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 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 context = await pending;
1100
- if (context) return context;
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.context;
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 context = tls.createSecureContext({ key, cert: cert + chain });
1123
- this._secureContexts.set(subject, { version, context });
1124
- return context;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -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
- execFileSync('openssl', ['req', '-x509', ...keyArgs, '-out', cert,
36
- '-subj', '/CN=example.com', '-days', '1', '-set_serial', String(serial)], { stdio: 'ignore' });
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 });