@small-tech/auto-encrypt 4.0.0 → 4.1.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 +22 -22
- package/index.js +1 -77
- package/lib/AcmeRequest.js +7 -1
- package/lib/Certificate.js +1 -1
- package/lib/Order.js +8 -1
- package/lib/acmeCsr.js +0 -6
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# Auto Encrypt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Automatically provisions and renews [Let’s Encrypt](https://letsencrypt.org) TLS certificates on [Node.js](https://nodejs.org) [https](https://nodejs.org/dist/latest-v12.x/docs/api/https.html) servers (including [Kitten](https://kitten.small-web.org), [Polka](https://github.com/lukeed/polka/tree/next), [Express.js](https://expressjs.com/), etc.)
|
|
4
4
|
|
|
5
5
|
Implements the subset of RFC 8555 – Automatic Certificate Management Environment (ACME) – necessary for a client to support TLS certificate provisioning from Let’s Encrypt using HTTP-01 challenges.
|
|
6
6
|
|
|
7
7
|
## How it works
|
|
8
8
|
|
|
9
|
-
The first time your web site is hit, it
|
|
9
|
+
The first time your web site is hit, it takes a couple of seconds to load as your Let’s Encrypt TLS certificates are automatically provisioned for you. From there on, your certificates are seamlessly renewed 30 days before their expiry date.
|
|
10
10
|
|
|
11
|
-
When not provisioning certificates, Auto Encrypt
|
|
11
|
+
When not provisioning certificates, Auto Encrypt also forwards HTTP calls to HTTPS on your server.
|
|
12
12
|
|
|
13
13
|
## Compatibility
|
|
14
14
|
|
|
15
|
-
All tests pass on Node.js LTS (
|
|
16
|
-
|
|
17
|
-
[Tests fail on Node.js 19 (socket hang up error).](https://codeberg.org/small-tech/auto-encrypt/issues/3)
|
|
15
|
+
All tests pass on Node.js LTS (version 22).
|
|
18
16
|
|
|
19
17
|
## Installation
|
|
20
18
|
|
|
@@ -49,7 +47,7 @@ npm i @small-tech/auto-encrypt
|
|
|
49
47
|
|
|
50
48
|
### Example
|
|
51
49
|
|
|
52
|
-
The following code creates an HTTPS server running on port 443
|
|
50
|
+
The following code creates an HTTPS server running on port 443 that automatically provisions and renews TLS certificates from [Let’s Encrypt](https://letsencrypt.org) for the domains _<hostname>_ and _www.<hostname>_.
|
|
53
51
|
|
|
54
52
|
```js
|
|
55
53
|
import AutoEncrypt from '@small-tech/auto-encrypt'
|
|
@@ -69,7 +67,7 @@ server.close(() => {
|
|
|
69
67
|
})
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
Note that on Linux, ports 80 and 443 require special privileges. Please see [A note on Linux and the security farce that is “privileged ports”](#a-note-on-linux-and-the-security-farce-that-is-priviliged-ports). If you just need a
|
|
70
|
+
Note that on Linux, ports 80 and 443 require special privileges. Please see [A note on Linux and the security farce that is “privileged ports”](#a-note-on-linux-and-the-security-farce-that-is-priviliged-ports). If you just need a [Small Web](https://ar.al/2024/06/24/small-web-computer-science-colloquium-at-university-of-groningen/) server that handles all that and more for you (or to see how to implement privilege escalation seamlessly in your own servers, see [Kitten](https://kitten.small-web.org)).
|
|
73
71
|
|
|
74
72
|
## Configuration
|
|
75
73
|
|
|
@@ -77,6 +75,8 @@ You can customise the default configuration by adding Auto Encrypt-specific opti
|
|
|
77
75
|
|
|
78
76
|
You can specify the domains you want the certificate to support, whether the Let’s Encrypt staging server or a local [Pebble](https://github.com/letsencrypt/pebble) server should be used instead of the default production server (useful during development and testing), and to specify a custom settings path for your Let’s Encrypt account and certificate information to be stored in.
|
|
79
77
|
|
|
78
|
+
(Auto Encrypt uses [Node Pebble](https://codeberg.org/small-tech/node-pebble) to enable testing with a local Pebble server from Node.js.)
|
|
79
|
+
|
|
80
80
|
### Example
|
|
81
81
|
|
|
82
82
|
```js
|
|
@@ -111,7 +111,7 @@ When you’re ready to exit your app, just call the `server.close()` method as y
|
|
|
111
111
|
|
|
112
112
|
## Developer documentation
|
|
113
113
|
|
|
114
|
-
If you want to help improve Auto Encrypt or better understand how it is structured and operates, please see the [developer documentation](https://
|
|
114
|
+
If you want to help improve Auto Encrypt or better understand how it is structured and operates, please see the [developer documentation](https://codeberg.org/small-tech/auto-encrypt/src/branch/main/developer-documentation.md).
|
|
115
115
|
|
|
116
116
|
## Examples
|
|
117
117
|
|
|
@@ -177,17 +177,17 @@ If you’re evaluating this for a “startup” or an enterprise, let us save yo
|
|
|
177
177
|
|
|
178
178
|
## Client details
|
|
179
179
|
|
|
180
|
-
Auto Encrypt does one thing and one thing well: it automatically provisions a Let’s Encrypt TLS certificate for your Node.js https servers using the HTTP-01 challenge method when your server is first hit from its hostname,
|
|
180
|
+
Auto Encrypt does one thing and one thing well: it automatically provisions a Let’s Encrypt TLS certificate for your Node.js https servers using the HTTP-01 challenge method when your server is first hit from its hostname, and automatically renews your certificate thereafter. When not provisioning certificates, it forwards any HTTP requests that your machine gets to HTTPS.
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
__Auto Encrypt _does not_ and _will not:___
|
|
183
183
|
|
|
184
|
-
-
|
|
184
|
+
- __Implement wildcard certificates.__ For most [small tech](https://small-tech.org/about/#small-technology) needs (personal web sites and web apps), you will likely need no more than two domains (the root domain and, due to historic and conventional reasons, the www subdomain). You will definitely not need more than the 100 domains that are supported per certificate. If you do, chances are you are looking to use Auto Encrypt in a startup or corporate setting, which is not what its for.
|
|
185
185
|
|
|
186
|
-
-
|
|
186
|
+
- __Implement DNS-01__ or any other methods that cannot be fully automated.
|
|
187
187
|
|
|
188
188
|
## Staging and production server behaviour and rate limits
|
|
189
189
|
|
|
190
|
-
By default, Auto Encrypt
|
|
190
|
+
By default, Auto Encrypt uses Let’s Encrypt’s production environment. This is most likely what you want as it means your HTTPS server will Just Work™, i.e., provision its TLS certificate automatically the first time the server is hit via its hostname and from thereon automatically renew the certificate a month ahead of its expiry date.
|
|
191
191
|
|
|
192
192
|
However, be aware that the production server has [rate limits](https://letsencrypt.org/docs/rate-limits/).
|
|
193
193
|
|
|
@@ -203,24 +203,24 @@ From lower-level to higher-level:
|
|
|
203
203
|
|
|
204
204
|
### Auto Encrypt Localhost
|
|
205
205
|
|
|
206
|
-
- Source: https://
|
|
206
|
+
- Source: https://codeberg.org/small-tech/auto-encrypt-localhost
|
|
207
207
|
- Package: [@small-tech/auto-encrypt-localhost](https://www.npmjs.com/package/@small-tech/auto-encrypt-localhost)
|
|
208
208
|
|
|
209
|
-
Automatically provisions and installs locally-trusted TLS certificates for Node.js https servers (
|
|
209
|
+
Automatically provisions and installs locally-trusted TLS certificates for Node.js https servers in 100% JavaScript (without any native dependencies like mkcert and certutil).
|
|
210
210
|
|
|
211
211
|
### HTTPS
|
|
212
212
|
|
|
213
213
|
- Source: https://source.small-tech.org/site.js/lib/https
|
|
214
214
|
- Package: [@small-tech/https](https://www.npmjs.com/package/@small-tech/https)
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
Drop-in replace for the [standard Node.js HTTPS module](https://nodejs.org/dist/latest-v12.x/docs/api/https.html) replacement that automatically handles TLS certificate provisioning and renewal both at localhost (via Auto Encrypt Localhost) and at hostname (via Auto Encrypt).
|
|
217
217
|
|
|
218
|
-
###
|
|
218
|
+
### Kitten
|
|
219
219
|
|
|
220
|
-
-
|
|
221
|
-
- Source: https://
|
|
220
|
+
- Site: https://kitten.small-web.org
|
|
221
|
+
- Source: https://codeberg.org/kitten/app
|
|
222
222
|
|
|
223
|
-
A
|
|
223
|
+
A [💕 Small Web](https://ar.al/2020/08/07/what-is-the-small-web/) development kit.
|
|
224
224
|
|
|
225
225
|
## Tests and coverage
|
|
226
226
|
|
|
@@ -228,7 +228,7 @@ This project aims for > 80% coverage. At a recent check, coverage was at 97.42%
|
|
|
228
228
|
|
|
229
229
|
To see the current state of code coverage, run `npm run coverage`.
|
|
230
230
|
|
|
231
|
-
For more details, please see the [developer documentation](https://
|
|
231
|
+
For more details, please see the [developer documentation](https://codeberg.org/small-tech/auto-encrypt/src/branch/main/developer-documentation.md#tests).
|
|
232
232
|
|
|
233
233
|
## A note on Linux and the security farce that is “privileged ports”
|
|
234
234
|
|
package/index.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import os from 'os'
|
|
15
15
|
import util from 'util'
|
|
16
16
|
import https from 'https'
|
|
17
|
-
import ocsp from 'ocsp'
|
|
18
17
|
import monkeyPatchTls from './lib/staging/monkeyPatchTls.js'
|
|
19
18
|
import LetsEncryptServer from './lib/LetsEncryptServer.js'
|
|
20
19
|
import Configuration from './lib/Configuration.js'
|
|
@@ -77,9 +76,6 @@ export default class AutoEncrypt {
|
|
|
77
76
|
*/
|
|
78
77
|
static get https () { return AutoEncrypt }
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
static ocspCache = null
|
|
82
|
-
|
|
83
79
|
/**
|
|
84
80
|
* Automatically manages Let’s Encrypt certificate provisioning and renewal for Node.js
|
|
85
81
|
* https servers using the HTTP-01 challenge on first hit of an HTTPS route via use of
|
|
@@ -174,7 +170,7 @@ export default class AutoEncrypt {
|
|
|
174
170
|
}
|
|
175
171
|
}
|
|
176
172
|
|
|
177
|
-
const server =
|
|
173
|
+
const server = https.createServer(options, listener)
|
|
178
174
|
|
|
179
175
|
//
|
|
180
176
|
// Monkey-patch the server.
|
|
@@ -212,25 +208,11 @@ export default class AutoEncrypt {
|
|
|
212
208
|
}
|
|
213
209
|
|
|
214
210
|
|
|
215
|
-
/**
|
|
216
|
-
* The OCSP module does not have a means of clearing its cache check timers
|
|
217
|
-
* so we do it here. (Otherwise, the test suite would hang.)
|
|
218
|
-
*/
|
|
219
|
-
static clearOcspCacheTimers () {
|
|
220
|
-
if (this.ocspCache !== null) {
|
|
221
|
-
const cacheIds = Object.keys(this.ocspCache.cache)
|
|
222
|
-
cacheIds.forEach(cacheId => {
|
|
223
|
-
clearInterval(this.ocspCache.cache[cacheId].timer)
|
|
224
|
-
})
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
211
|
/**
|
|
229
212
|
* Shut Auto Encrypt down. Do this before app exit. Performs necessary clean-up and removes
|
|
230
213
|
* any references that might cause the app to not exit.
|
|
231
214
|
*/
|
|
232
215
|
static shutdown () {
|
|
233
|
-
this.clearOcspCacheTimers()
|
|
234
216
|
this.certificate.stopCheckingForRenewal()
|
|
235
217
|
}
|
|
236
218
|
|
|
@@ -238,64 +220,6 @@ export default class AutoEncrypt {
|
|
|
238
220
|
// Private.
|
|
239
221
|
//
|
|
240
222
|
|
|
241
|
-
/**
|
|
242
|
-
* Adds Online Certificate Status Protocol (OCSP) stapling (also known as TLS Certificate Status Request extension)
|
|
243
|
-
* support to the passed server instance.
|
|
244
|
-
*
|
|
245
|
-
* @private
|
|
246
|
-
* @param {https.Server} server HTTPS server instance without OCSP Stapling support.
|
|
247
|
-
* @returns {https.Server} HTTPS server instance with OCSP Stapling support.
|
|
248
|
-
*/
|
|
249
|
-
static addOcspStapling(server) {
|
|
250
|
-
// OCSP stapling
|
|
251
|
-
//
|
|
252
|
-
// Many browsers will fetch OCSP from Let’s Encrypt when they load your site. This is a performance and privacy
|
|
253
|
-
// problem. Ideally, connections to your site should not wait for a secondary connection to Let’s Encrypt. Also,
|
|
254
|
-
// OCSP requests tell Let’s Encrypt which sites people are visiting. We have a good privacy policy and do not record
|
|
255
|
-
// individually identifying details from OCSP requests, we’d rather not even receive the data in the first place.
|
|
256
|
-
// Additionally, we anticipate our bandwidth costs for serving OCSP every time a browser visits a Let’s Encrypt site
|
|
257
|
-
// for the first time will be a big part of our infrastructure expense.
|
|
258
|
-
//
|
|
259
|
-
// By turning on OCSP Stapling, you can improve the performance of your website, provide better privacy protections
|
|
260
|
-
// … and help Let’s Encrypt efficiently serve as many people as possible.
|
|
261
|
-
//
|
|
262
|
-
// (Source: https://letsencrypt.org/docs/integration-guide/implement-ocsp-stapling)
|
|
263
|
-
|
|
264
|
-
this.ocspCache = new ocsp.Cache()
|
|
265
|
-
const cache = this.ocspCache
|
|
266
|
-
|
|
267
|
-
server.on('OCSPRequest', (certificate, issuer, callback) => {
|
|
268
|
-
|
|
269
|
-
if (certificate == null) {
|
|
270
|
-
return callback(new Error('Cannot OCSP staple: certificate not yet provisioned.'))
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
ocsp.getOCSPURI(certificate, function(error, uri) {
|
|
274
|
-
if (error) return callback(error)
|
|
275
|
-
if (uri === null) return callback()
|
|
276
|
-
|
|
277
|
-
const request = ocsp.request.generate(certificate, issuer)
|
|
278
|
-
|
|
279
|
-
cache.probe(request.id, (error, cached) => {
|
|
280
|
-
if (error) return callback(error)
|
|
281
|
-
|
|
282
|
-
if (cached !== false) {
|
|
283
|
-
return callback(null, cached.response)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const options = {
|
|
287
|
-
url: uri,
|
|
288
|
-
ocsp: request.data
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
cache.request(request.id, options, callback);
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
return server
|
|
297
|
-
}
|
|
298
|
-
|
|
299
223
|
// Custom object description for console output (for debugging).
|
|
300
224
|
static [util.inspect.custom] () {
|
|
301
225
|
return `
|
package/lib/AcmeRequest.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* @license AGPLv3 or later.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import fs from 'fs'
|
|
10
|
+
import path from 'path'
|
|
11
|
+
import { fileURLToPath } from 'url'
|
|
9
12
|
import jose from 'jose'
|
|
10
13
|
import prepareRequest from 'bent'
|
|
11
14
|
import types from '../typedefs/lib/AcmeRequest.js'
|
|
@@ -23,6 +26,7 @@ const throws = new Throws({
|
|
|
23
26
|
[Symbol.for('AcmeRequest.requestError')]: error => `(${error.status} ${error.type} ${error.detail})`
|
|
24
27
|
})
|
|
25
28
|
|
|
29
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
26
30
|
/**
|
|
27
31
|
* Abstract base request class for carrying out signed ACME requests over HTTPS.
|
|
28
32
|
*
|
|
@@ -34,6 +38,8 @@ export default class AcmeRequest {
|
|
|
34
38
|
static accountIdentity = null
|
|
35
39
|
static nonce = null
|
|
36
40
|
static __account = null
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
static autoEncryptVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'))).version
|
|
37
43
|
|
|
38
44
|
static initialise (directory = throws.ifMissing(), accountIdentity = throws.ifMissing()) {
|
|
39
45
|
this.directory = directory
|
|
@@ -228,7 +234,7 @@ export default class AcmeRequest {
|
|
|
228
234
|
|
|
229
235
|
const httpsHeaders = {
|
|
230
236
|
'Content-Type': 'application/jose+json',
|
|
231
|
-
'User-Agent':
|
|
237
|
+
'User-Agent': `small-tech.org-auto-encrypt/${AcmeRequest.autoEncryptVersion}`,
|
|
232
238
|
'Accept-Language': 'en-US'
|
|
233
239
|
}
|
|
234
240
|
|
package/lib/Certificate.js
CHANGED
|
@@ -369,7 +369,7 @@ export default class Certificate {
|
|
|
369
369
|
const issuer = certificate.issuer.value[0][0].value.toString('utf-8').slice(2).trim()
|
|
370
370
|
const issuedAt = new Date(certificate.validity.notBefore.value)
|
|
371
371
|
const expiresAt = new Date(certificate.validity.notAfter.value)
|
|
372
|
-
const subject = certificate.subject.value[0][0].value.toString('utf-8').slice(2).trim()
|
|
372
|
+
const subject = certificate.subject.value.length > 0 ? certificate.subject.value[0][0].value.toString('utf-8').slice(2).trim() : '(No subject)'
|
|
373
373
|
|
|
374
374
|
const alternativeNames = ((certificate.extensions.filter(extension => {
|
|
375
375
|
return extension.extnID === 'subjectAlternativeName'
|
package/lib/Order.js
CHANGED
|
@@ -31,6 +31,7 @@ const throws = new Throws()
|
|
|
31
31
|
export default class Order {
|
|
32
32
|
#data = null
|
|
33
33
|
#headers = null
|
|
34
|
+
#location = null
|
|
34
35
|
#order = null
|
|
35
36
|
#certificate = null
|
|
36
37
|
#certificateIdentity = null
|
|
@@ -65,6 +66,10 @@ export default class Order {
|
|
|
65
66
|
get data () { return this.#data }
|
|
66
67
|
set data (value) {
|
|
67
68
|
this.#data = value
|
|
69
|
+
// It seems that Let’s Encrypt are no longer sending the location
|
|
70
|
+
// back on status checks, only on order finalisation, so let’s\
|
|
71
|
+
// make sure we don’t accidentally.
|
|
72
|
+
if (this.#data.headers.location !== undefined) this.#location = this.#data.headers.location
|
|
68
73
|
this.#headers = this.#data.headers
|
|
69
74
|
this.#order = this.#data.body
|
|
70
75
|
}
|
|
@@ -168,10 +173,12 @@ export default class Order {
|
|
|
168
173
|
try {
|
|
169
174
|
if (numAttempts === 1) {
|
|
170
175
|
// Finalise using CSR.
|
|
176
|
+
log(' 📝 ❨auto-encrypt❩ Finalising using CSR.')
|
|
171
177
|
this.data = await (new FinaliseOrderRequest()).execute(this.finaliseUrl, csr)
|
|
172
178
|
} else {
|
|
173
179
|
// Check for order status.
|
|
174
|
-
|
|
180
|
+
log(' 👀 ❨auto-encrypt❩ Checking for order status.')
|
|
181
|
+
this.data = await (new CheckOrderStatusRequest()).execute(this.#location)
|
|
175
182
|
}
|
|
176
183
|
} catch (error) {
|
|
177
184
|
// TODO: Handle error.
|
package/lib/acmeCsr.js
CHANGED
|
@@ -53,12 +53,6 @@ function csrAsPem (domains, key) {
|
|
|
53
53
|
extensions: [{
|
|
54
54
|
name: 'subjectAltName',
|
|
55
55
|
altNames
|
|
56
|
-
}, {
|
|
57
|
-
// OCSP Must Staple
|
|
58
|
-
// RFC 7633. Also see https://scotthelme.co.uk/ocsp-must-staple/
|
|
59
|
-
// 1.3.6.1.5.5.7.1.24 = DER(3003020105) (Sequence > Int > 5) *smh* ASN.1 is devil spawn.
|
|
60
|
-
id: '1.3.6.1.5.5.7.1.24',
|
|
61
|
-
value: forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [ forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.INTEGER, false, forge.asn1.integerToDer(5).getBytes())])
|
|
62
56
|
}]
|
|
63
57
|
}])
|
|
64
58
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@small-tech/auto-encrypt",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.1.1",
|
|
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.2.0"
|
|
7
7
|
},
|
|
@@ -68,12 +68,11 @@
|
|
|
68
68
|
"encodeurl": "^1.0.2",
|
|
69
69
|
"jose": "^1.28.2",
|
|
70
70
|
"moment": "^2.29.4",
|
|
71
|
-
"node-forge": "^1.3.1"
|
|
72
|
-
"ocsp": "^1.2.0"
|
|
71
|
+
"node-forge": "^1.3.1"
|
|
73
72
|
},
|
|
74
73
|
"devDependencies": {
|
|
75
74
|
"@small-tech/esm-tape-runner": "^1.0.3",
|
|
76
|
-
"@small-tech/node-pebble": "^
|
|
75
|
+
"@small-tech/node-pebble": "^5.1.3",
|
|
77
76
|
"@small-tech/tap-monkey": "^1.3.0",
|
|
78
77
|
"c8": "^7.12.0",
|
|
79
78
|
"dependency-cruiser": "^12.3.0",
|