@small-tech/node-pebble
Version:
A Node.js wrapper for Let’s Encrypt’s Pebble (“a small RFC 8555 ACME test server not suited for a production certificate authority”).
122 lines (102 loc) • 4.29 kB
JavaScript
/**
* Monkey patches the TLS module to accept the Pebble test certificate and the Pebble
* runtime-generated root and intermediary Certificate Authority (CA) certificates.
*
* Based on the method provided by David Barral at https://link.medium.com/6xHYLeUVq5.
*
* @module
* @copyright Copyright © 2020-2021 Aral Balkan, Small Technology Foundation.
* @license AGPLv3 or later.
*/
import fs from 'fs'
import path from 'path'
import https from 'https'
import tls from 'tls'
import Throws from './util/Throws.js'
const __dirname = new URL('.', import.meta.url).pathname
const throws = new Throws({
[Symbol.for('MonkeyPatchTls.certificateParseError')]:
(certificatePath, additionalCertificates) => `Could not parse certificate at path ${certificatePath}. Additional certificates: ${additionalCertificates}`
})
function httpsGetString (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
let str = ''
response.on('data', data => str += data)
response.on('end', () => { resolve(str) })
response.on('error', error => reject(error))
})
})
}
/**
* Monkey patches the TLS module to accept run-time root and intermediary Certificate Authority certificates.
*
* @alias module:lib/MonkeyPatchTls
*/
export default class MonkeyPatchTLS {
static #originalCreateSecureContext = null
/**
* Monkey patches TLS to accept all Pebble certificates (test certificate, root CA, and intermediary CA)
*
* @static
* @category async
*/
static async toAcceptAllPebbleCertificates() {
MonkeyPatchTLS.toAcceptPebbleTestCertificate()
const additionalCertificates = await this.downloadPebbleCaRootAndIntermediaryCertificates()
MonkeyPatchTLS.toAcceptPebbleTestCertificate(/* and */ additionalCertificates)
}
//
// Private.
//
/**
* Monkey patches Node’s TLS module to accept the certificate at the passed path as well as, optionally, any other
* certificates passed as a PEM-formatted string.
*
* @static
* @access private
* @param {String} [additionalCertificatesPem=''] Additional certificates to be added to the chain of trust.
*/
static toAcceptPebbleTestCertificate(additionalCertificatesPem = '') {
if (this.#originalCreateSecureContext === null) {
this.#originalCreateSecureContext = tls.createSecureContext
}
const originalCreateSecureContext = this.#originalCreateSecureContext
const certificatePath = path.join(__dirname, '..', 'bin', 'test', 'certs', 'pebble.minica.pem')
// Load the Pebbleserver’s own test CA certificate from disk.
// (See https://github.com/letsencrypt/pebble#avoiding-client-https-errors.)
// Note that this is not the the Pebble CA root or intermediary certificate (see below).
let pem = fs.readFileSync(certificatePath, { encoding: 'ascii' }).replace(/\r\n/g, "\n")
// Add any additional certificates that might have been provided to the PEM that’s loaded from disk.
// (e.g., to create the Pebble server’s chain of trust).
pem = `${pem}\n${additionalCertificatesPem}`
const certificates = pem.match(/-----BEGIN CERTIFICATE-----\n[\s\S]+?\n-----END CERTIFICATE-----/g)
if (!certificates) {
throws.error(Symbol.for('MonkeyPatchTls.certificateParseError'), certificatePath, additionalCertificatesPem)
}
tls.createSecureContext = options => {
const context = originalCreateSecureContext(options)
certificates.forEach(certificate => {
context.context.addCACert(certificate.trim())
})
return context
}
}
/**
* Downloads and returns the dynamically-generated local Pebble server’s Certificate Authority root
* and intermediary certificates.
*
* @static
* @category async
* @access private
* @returns {String} The Pebble server’s CA root and intermediary certificates as a single PEM-formatted string.
*/
static async downloadPebbleCaRootAndIntermediaryCertificates() {
const rootCaUrl = 'https://localhost:15000/roots/0'
const intermediaryCaUrl = 'https://localhost:15000/intermediates/0'
const rootCa = await httpsGetString(rootCaUrl)
const intermediaryCa = await httpsGetString(intermediaryCaUrl)
const pem = `${rootCa}\n${intermediaryCa}`
return pem
}
}