@small-tech/auto-encrypt 4.2.0 → 4.3.0

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
@@ -14,6 +14,7 @@
14
14
  import os from 'os'
15
15
  import util from 'util'
16
16
  import https from 'https'
17
+ import ocsp from 'ocsp'
17
18
  import monkeyPatchTls from './lib/staging/monkeyPatchTls.js'
18
19
  import LetsEncryptServer from './lib/LetsEncryptServer.js'
19
20
  import Configuration from './lib/Configuration.js'
@@ -76,6 +77,9 @@ export default class AutoEncrypt {
76
77
  */
77
78
  static get https () { return AutoEncrypt }
78
79
 
80
+
81
+ static ocspCache = null
82
+
79
83
  /**
80
84
  * Automatically manages Let’s Encrypt certificate provisioning and renewal for Node.js
81
85
  * https servers using the HTTP-01 challenge on first hit of an HTTPS route via use of
@@ -170,7 +174,19 @@ export default class AutoEncrypt {
170
174
  }
171
175
  }
172
176
 
173
- const server = https.createServer(options, listener)
177
+ const shouldAddOcspMustStaple = certificate.hasOcspMustStaple || false
178
+
179
+ // During the transitionary period where Let’s Encrypt has shut down
180
+ // OCSP support but there are still servers out there with certificates
181
+ // that have OCSP stapling because their 6-month validity period isn’t
182
+ // over, we create the server accordingly based on whether the certificate
183
+ // has OCSP stapling or not so it works for both cases.
184
+ // TODO: OCSP support can be fully removed in August 2025. All existing
185
+ // servers with valid certificates will be non-OCSP at that point.
186
+ const serverWithoutOcspMustStaple = https.createServer(options, listener)
187
+ const server = shouldAddOcspMustStaple
188
+ ? this.addOcspStapling(serverWithoutOcspMustStaple)
189
+ : serverWithoutOcspMustStaple
174
190
 
175
191
  //
176
192
  // Monkey-patch the server.
@@ -208,11 +224,25 @@ export default class AutoEncrypt {
208
224
  }
209
225
 
210
226
 
227
+ /**
228
+ * The OCSP module does not have a means of clearing its cache check timers
229
+ * so we do it here. (Otherwise, the test suite would hang.)
230
+ */
231
+ static clearOcspCacheTimers () {
232
+ if (this.ocspCache !== null) {
233
+ const cacheIds = Object.keys(this.ocspCache.cache)
234
+ cacheIds.forEach(cacheId => {
235
+ clearInterval(this.ocspCache.cache[cacheId].timer)
236
+ })
237
+ }
238
+ }
239
+
211
240
  /**
212
241
  * Shut Auto Encrypt down. Do this before app exit. Performs necessary clean-up and removes
213
242
  * any references that might cause the app to not exit.
214
243
  */
215
244
  static shutdown () {
245
+ this.clearOcspCacheTimers()
216
246
  this.certificate.stopCheckingForRenewal()
217
247
  }
218
248
 
@@ -220,6 +250,64 @@ export default class AutoEncrypt {
220
250
  // Private.
221
251
  //
222
252
 
253
+ /**
254
+ * Adds Online Certificate Status Protocol (OCSP) stapling (also known as TLS Certificate Status Request extension)
255
+ * support to the passed server instance.
256
+ *
257
+ * @private
258
+ * @param {https.Server} server HTTPS server instance without OCSP Stapling support.
259
+ * @returns {https.Server} HTTPS server instance with OCSP Stapling support.
260
+ */
261
+ static addOcspStapling(server) {
262
+ // OCSP stapling
263
+ //
264
+ // Many browsers will fetch OCSP from Let’s Encrypt when they load your site. This is a performance and privacy
265
+ // problem. Ideally, connections to your site should not wait for a secondary connection to Let’s Encrypt. Also,
266
+ // OCSP requests tell Let’s Encrypt which sites people are visiting. We have a good privacy policy and do not record
267
+ // individually identifying details from OCSP requests, we’d rather not even receive the data in the first place.
268
+ // Additionally, we anticipate our bandwidth costs for serving OCSP every time a browser visits a Let’s Encrypt site
269
+ // for the first time will be a big part of our infrastructure expense.
270
+ //
271
+ // By turning on OCSP Stapling, you can improve the performance of your website, provide better privacy protections
272
+ // … and help Let’s Encrypt efficiently serve as many people as possible.
273
+ //
274
+ // (Source: https://letsencrypt.org/docs/integration-guide/implement-ocsp-stapling)
275
+
276
+ this.ocspCache = new ocsp.Cache()
277
+ const cache = this.ocspCache
278
+
279
+ server.on('OCSPRequest', (certificate, issuer, callback) => {
280
+
281
+ if (certificate == null) {
282
+ return callback(new Error('Cannot OCSP staple: certificate not yet provisioned.'))
283
+ }
284
+
285
+ ocsp.getOCSPURI(certificate, function(error, uri) {
286
+ if (error) return callback(error)
287
+ if (uri === null) return callback()
288
+
289
+ const request = ocsp.request.generate(certificate, issuer)
290
+
291
+ cache.probe(request.id, (error, cached) => {
292
+ if (error) return callback(error)
293
+
294
+ if (cached !== false) {
295
+ return callback(null, cached.response)
296
+ }
297
+
298
+ const options = {
299
+ url: uri,
300
+ ocsp: request.data
301
+ }
302
+
303
+ cache.request(request.id, options, callback);
304
+ })
305
+ })
306
+ })
307
+
308
+ return server
309
+ }
310
+
223
311
  // Custom object description for console output (for debugging).
224
312
  static [util.inspect.custom] () {
225
313
  return `
@@ -94,29 +94,32 @@ export default class Certificate {
94
94
  #_serialNumber = null
95
95
  #_issueDate = null
96
96
  #_expiryDate = null
97
-
98
- get isProvisioned () { return this.#_pem !== null }
99
- get pem () { return this.#_pem }
100
- get identity () { return this.#_identity }
101
- get key () { return this.#_key }
102
- get serialNumber () { return this.#_serialNumber }
103
- get issuer () { return this.#_issuer }
104
- get subject () { return this.#_subject }
105
- get alternativeNames () { return this.#_alternativeNames }
106
- get issueDate () { return this.#_issueDate }
107
- get expiryDate () { return this.#_expiryDate }
108
- get renewalDate () { return this.#renewalDate }
97
+ #_hasOcspMustStaple = null
98
+
99
+ get isProvisioned () { return this.#_pem !== null }
100
+ get pem () { return this.#_pem }
101
+ get identity () { return this.#_identity }
102
+ get key () { return this.#_key }
103
+ get serialNumber () { return this.#_serialNumber }
104
+ get issuer () { return this.#_issuer }
105
+ get subject () { return this.#_subject }
106
+ get alternativeNames () { return this.#_alternativeNames }
107
+ get issueDate () { return this.#_issueDate }
108
+ get expiryDate () { return this.#_expiryDate }
109
+ get renewalDate () { return this.#renewalDate }
110
+ get hasOcspMustStaple () { return this.#_hasOcspMustStaple }
109
111
 
110
112
  set pem (certificatePem) {
111
113
  this.#_pem = certificatePem
112
114
 
113
115
  const details = this.parseDetails(certificatePem)
114
- this.#_serialNumber = details.serialNumber
115
- this.#_issuer = details.issuer
116
- this.#_subject = details.subject
117
- this.#_alternativeNames = details.alternativeNames
118
- this.#_issueDate = moment(details.issuedAt)
119
- this.#_expiryDate = moment(details.expiresAt)
116
+ this.#_serialNumber = details.serialNumber
117
+ this.#_issuer = details.issuer
118
+ this.#_subject = details.subject
119
+ this.#_alternativeNames = details.alternativeNames
120
+ this.#_issueDate = moment(details.issuedAt)
121
+ this.#_expiryDate = moment(details.expiresAt)
122
+ this.#_hasOcspMustStaple = details.hasOcspMustStaple
120
123
 
121
124
  // Display the certificate with a nice border :)
122
125
  const logMessagePrefix = ' ❨auto-encrypt❩ '
@@ -370,18 +373,26 @@ export default class Certificate {
370
373
  const issuedAt = new Date(certificate.validity.notBefore.value)
371
374
  const expiresAt = new Date(certificate.validity.notAfter.value)
372
375
  const subject = certificate.subject.value.length > 0 ? certificate.subject.value[0][0].value.toString('utf-8').slice(2).trim() : '(No subject)'
376
+ let hasOcspMustStaple = false
373
377
 
374
378
  const alternativeNames = ((certificate.extensions.filter(extension => {
375
379
  return extension.extnID === 'subjectAlternativeName'
376
380
  }))[0].extnValue).map(name => name.value)
377
381
 
382
+ certificate.extensions.forEach(extension => {
383
+ if (Array.isArray(extension.extnID) && extension.extnID.join('.') === '1.3.6.1.5.5.7.1.24') {
384
+ hasOcspMustStaple = true
385
+ }
386
+ })
387
+
378
388
  return {
379
389
  serialNumber,
380
390
  issuer,
381
391
  subject,
382
392
  alternativeNames,
383
393
  issuedAt,
384
- expiresAt
394
+ expiresAt,
395
+ hasOcspMustStaple
385
396
  }
386
397
  }
387
398
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@small-tech/auto-encrypt",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Automatically provisions and renews Let’s Encrypt TLS certificates on Node.js https servers (including Kitten, Polka, Express.js, etc.)",
5
5
  "engines": {
6
6
  "node": ">=18.20.0"
@@ -68,7 +68,8 @@
68
68
  "encodeurl": "^1.0.2",
69
69
  "jose": "^1.28.2",
70
70
  "moment": "^2.29.4",
71
- "node-forge": "^1.3.1"
71
+ "node-forge": "^1.3.1",
72
+ "ocsp": "^1.2.0"
72
73
  },
73
74
  "devDependencies": {
74
75
  "@small-tech/esm-tape-runner": "^1.0.3",