Commit c5f71a76 authored by Aral Balkan's avatar Aral Balkan
Browse files

Root CA and certificate creation now also happens at post-install

parent 322b147e
......@@ -12,8 +12,8 @@ This version is optimised for use on development machines via npm install. It ca
### Changed
- Uses ECMAScript Modules (ESM; es6 modules)
- (Breaking) mkcert binary is now downloaded during installation.
- (Breaking) No longer copies the mkcert binary to the settings path.
- (Breaking) mkcert binary is now downloaded during installation and the root certificate authority and TLS certificates are created at this time also.
- (Breaking) The settings path is no longer configurable and is shared by all installations of this package. This means whenever you install this package, it will update to the latest version of mkcert and recreate the root certificate authorities and local certificates and these will be used by all instances of Auto Encrypt Localhost on your dev machine (and this is most likely the behaviour you want).
## [6.1.0] - 2020-11-04
......
#!/usr/bin/env node
import AutoEncryptLocalhost from '../index.js'
AutoEncryptLocalhost.https.createServer()
#!/usr/bin/env node
////////////////////////////////////////////////////////////////////////////////
//
// Downloads and installs the version of mkcert specified in lib/mkcert.js
// for the platform this script is running on.
//
////////////////////////////////////////////////////////////////////////////////
import https from 'https'
import fs from 'fs-extra'
import path from 'path'
import { version, binaryName } from '../lib/mkcert.js'
const __dirname = new URL('.', import.meta.url).pathname
async function secureGet (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
const statusCode = response.statusCode
const location = response.headers.location
// Reject if it’s not one of the status codes we are testing.
if (statusCode !== 200 && statusCode !== 302) {
reject({statusCode})
}
let body = ''
response.on('data', _ => body += _)
response.on('end', () => {
resolve({statusCode, location, body})
})
})
})
}
async function secureStreamToFile (url, filePath) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(filePath)
https.get(url, response => {
response.pipe(fileStream)
fileStream.on('finish', () => {
fileStream.close()
resolve()
})
fileStream.on('error', error => {
fs.unlinkSync(filePath)
reject(error)
})
})
})
}
//
// Install the latest mkcert binary.
//
process.stdout.write(`> Installing mkcert v${version} binary… `)
// This is the directory we will save the binary into.
// (Placing it here for backwards compatibility with < v7.0.0 releases).
const mkcertBinariesDirectory = path.resolve(path.join(__dirname, '..', 'mkcert-bin'))
// Delete and recreate the mkcert-bin folder.
fs.removeSync(mkcertBinariesDirectory)
fs.mkdirpSync(mkcertBinariesDirectory)
const mkcertBinaryUrl = `https://github.com/FiloSottile/mkcert/releases/download/v${version}/${binaryName}`
const binaryRedirectUrl = (await secureGet(mkcertBinaryUrl)).location
const binaryPath = path.join(mkcertBinariesDirectory, binaryName)
await secureStreamToFile(binaryRedirectUrl, binaryPath)
// Make the binary executable.
fs.chmodSync(binaryPath, 0o755)
process.stdout.write('done.\n')
#!/usr/bin/env node
////////////////////////////////////////////////////////////////////////////////
//
// npm post-install script
//
// 1. Downloads and installs the version of mkcert specified in lib/mkcert.js
// for the platform this script is running on.
//
// 2. Attempts to install certutil if it isn’t already installed and
// if it can.
//
// 3. Creates the local root certificate authority using mkcert.
//
// 4. Generates TLS certificates for localhost as well as any IP addresses
// that the machine is reachable from on the network (if you
// change networks and you want to be reachable by IP, re-run npm i).
//
////////////////////////////////////////////////////////////////////////////////
import https from 'https'
import os from 'os'
import path from 'path'
import childProcess from 'child_process'
import { binaryPath as mkcertBinary } from '../lib/mkcert.js'
import installCertutil from '../lib/installCertutil.js'
import { version, binaryName } from '../lib/mkcert.js'
import fs from 'fs-extra'
async function secureGet (url) {
return new Promise((resolve, reject) => {
https.get(url, response => {
const statusCode = response.statusCode
const location = response.headers.location
// Reject if it’s not one of the status codes we are testing.
if (statusCode !== 200 && statusCode !== 302) {
reject({statusCode})
}
let body = ''
response.on('data', _ => body += _)
response.on('end', () => {
resolve({statusCode, location, body})
})
})
})
}
async function secureStreamToFile (url, filePath) {
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(filePath)
https.get(url, response => {
response.pipe(fileStream)
fileStream.on('finish', () => {
fileStream.close()
resolve()
})
fileStream.on('error', error => {
fs.unlinkSync(filePath)
reject(error)
})
})
})
}
//
// Install the mkcert binary, create the host, and the certificates.
// This is done after every npm install. (Better to always have the
// latest and greatest mkcert available to all projects on an account
// that make use of it.)
//
const settingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
console.log(' 🔒️ Auto Encrypt Localhost (postinstall)')
console.log(' ────────────────────────────────────────────────────────────────────────')
process.stdout.write(` ╰─ Installing mkcert v${version} binary… `)
// Delete and recreate the mkcert-bin folder.
fs.removeSync(settingsPath)
fs.mkdirpSync(settingsPath)
const mkcertBinaryUrl = `https://github.com/FiloSottile/mkcert/releases/download/v${version}/${binaryName}`
const binaryRedirectUrl = (await secureGet(mkcertBinaryUrl)).location
const binaryPath = path.join(settingsPath, binaryName)
await secureStreamToFile(binaryRedirectUrl, binaryPath)
// Make the binary executable.
fs.chmodSync(binaryPath, 0o755)
process.stdout.write('done.\n')
//
// Create the root certificate authority and certificates.
//
const keyFilePath = path.join(settingsPath, 'localhost-key.pem')
const certFilePath = path.join(settingsPath, 'localhost.pem')
const allOK = () => {
return fs.existsSync(path.join(settingsPath, 'rootCA.pem')) && fs.existsSync(path.join(settingsPath, 'rootCA-key.pem')) && fs.existsSync(path.join(settingsPath, 'localhost.pem')) && fs.existsSync(path.join(settingsPath, 'localhost-key.pem'))
}
// On Linux and on macOS, mkcert uses the Mozilla nss library.
// Try to install this automatically and warn the person if we can’t so
// that they can do it manually themselves.
process.stdout.write(` ╰─ Installing certutil if necessary… `)
installCertutil()
process.stdout.write('done.\n')
// mkcert uses the CAROOT environment variable to know where to create/find the certificate authority.
// We also pass the rest of the system environment to the spawned processes.
const mkcertProcessOptions = {
env: process.env,
stdio: 'pipe' // suppress output
}
mkcertProcessOptions.env.CAROOT = settingsPath
// Create the local certificate authority.
process.stdout.write(` ╰─ Creating local certificate authority (local CA) using mkcert… `)
childProcess.execFileSync(mkcertBinary, ['-install'], mkcertProcessOptions)
process.stdout.write('done.\n')
// Create the local certificate.
process.stdout.write(' ╰─ Creating local TLS certificates using mkcert… ')
// Support all local interfaces so that the machine can be reached over the local network via IPv4.
// This is very useful for testing with multiple devices over the local area network without needing to expose
// the machine over the wide area network/Internet using a service like ngrok.
const localIPv4Addresses =
Object.entries(os.networkInterfaces())
.map(iface =>
iface[1].filter(addresses =>
addresses.family === 'IPv4')
.map(addresses => addresses.address)).flat()
const certificateDetails = [
`-key-file=${keyFilePath}`,
`-cert-file=${certFilePath}`,
'localhost'
].concat(localIPv4Addresses)
childProcess.execFileSync(mkcertBinary, certificateDetails, mkcertProcessOptions)
process.stdout.write('done.\n')
// This should never happen as an error in the above, if there is one,
// should exit the process, but just in case.
if (!allOK()) {
console.log(' ╰─ ❌️ Certificate creation failed. Panic!')
process.exit(1)
} else {
console.log(' ────────────────────────────────────────────────────────────────────────')
}
......@@ -11,12 +11,8 @@ import os from 'os'
import fs from 'fs-extra'
import path from 'path'
import https from 'https'
import childProcess from 'child_process'
import syswidecas from 'syswide-cas'
import { binaryPath as mkcertBinary } from './lib/mkcert.js'
import installCertutil from './lib/installCertutil.js'
import HttpServer from './lib/HttpServer.js'
import { log } from './lib/util/log.js'
/**
* Auto Encrypt Localhost is a static class. Please do not instantiate.
......@@ -28,7 +24,7 @@ import { log } from './lib/util/log.js'
export default class AutoEncryptLocalhost {
/**
* By aliasing the https property to the AutoEncryptLocalhost static class itself, we enable
* people to add AutoEncryptLocalhost to their existing apps by requiring the module
* people to add AutoEncryptLocalhost to their existing apps by importing the module
* and prefixing their https.createServer(…) line with AutoEncryptLocalhost:
*
* @example import AutoEncryptLocalhost from '@small-tech/auto-encrypt-localhost'
......@@ -38,6 +34,8 @@ export default class AutoEncryptLocalhost {
*/
static get https () { return AutoEncryptLocalhost }
static settingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
/**
* Automatically provisions trusted development-time (localhost) certificates in Node.js via mkcert.
*
......@@ -55,80 +53,26 @@ export default class AutoEncryptLocalhost {
_options = {}
}
const defaultSettingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
const options = _options || {}
const listener = _listener || null
const settingsPath = options.settingsPath || defaultSettingsPath
const settingsPath = AutoEncryptLocalhost.settingsPath
this.settingsPath = settingsPath
const options = _options || {}
const listener = _listener || null
const keyFilePath = path.join(settingsPath, 'localhost-key.pem')
const certFilePath = path.join(settingsPath, 'localhost.pem')
const rootCAKeyFilePath = path.join(settingsPath, 'rootCA-key.pem')
const rootCACertFilePath = path.join(settingsPath, 'rootCA.pem')
const allOK = () => {
return fs.existsSync(path.join(settingsPath, 'rootCA.pem')) && fs.existsSync(path.join(settingsPath, 'rootCA-key.pem')) && fs.existsSync(path.join(settingsPath, 'localhost.pem')) && fs.existsSync(path.join(settingsPath, 'localhost-key.pem'))
}
// Ensure the Auto Encrypt Localhost directory exists.
fs.ensureDirSync(settingsPath)
this.settingsPath = settingsPath
const allOK = fs.existsSync(rootCACertFilePath) && fs.existsSync(rootCAKeyFilePath) && fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)
// Create certificates.
if (!allOK()) {
log(' 📜 ❨auto-encrypt-localhost❩ Setting up…')
// On Linux and on macOS, mkcert uses the Mozilla nss library.
// Try to install this automatically and warn the person if we can’t so
// that they can do it manually themselves.
installCertutil()
// mkcert uses the CAROOT environment variable to know where to create/find the certificate authority.
// We also pass the rest of the system environment to the spawned processes.
const mkcertProcessOptions = {
env: process.env,
stdio: 'pipe' // suppress output
}
mkcertProcessOptions.env.CAROOT = settingsPath
// Create the local certificate authority.
log(' 📜 ❨auto-encrypt-localhost❩ Creating local certificate authority (local CA) using mkcert…')
childProcess.execFileSync(mkcertBinary, ['-install'], mkcertProcessOptions)
log(' 📜 ❨auto-encrypt-localhost❩ Local certificate authority created.')
// Create the local certificate.
log(' 📜 ❨auto-encrypt-localhost❩ Creating local TLS certificates using mkcert…')
// Support all local interfaces so that the machine can be reached over the local network via IPv4.
// This is very useful for testing with multiple devices over the local area network without needing to expose
// the machine over the wide area network/Internet using a service like ngrok.
const localIPv4Addresses =
Object.entries(os.networkInterfaces())
.map(iface =>
iface[1].filter(addresses =>
addresses.family === 'IPv4')
.map(addresses => addresses.address)).flat()
const certificateDetails = [
`-key-file=${keyFilePath}`,
`-cert-file=${certFilePath}`,
'localhost'
].concat(localIPv4Addresses)
childProcess.execFileSync(mkcertBinary, certificateDetails, mkcertProcessOptions)
log(' 📜 ❨auto-encrypt-localhost❩ Local TLS certificates created.')
// This should never happen as an error in the above, if there is one,
// should exit the process, but just in case.
if (!allOK()) {
console.log('Could not find all necessary certificate information. Panic!')
process.exit(1)
}
} else {
log(' 📜 ❨auto-encrypt-localhost❩ Local development TLS certificate exists.')
if (!allOK) {
console.log('Could not find all necessary certificate information. Panic!')
process.exit(1)
}
// Add root store to Node to ensure Node recognises the certificates (e.g., when using https.get(), etc.)
const rootCA = path.join(settingsPath, 'rootCA.pem')
syswidecas.addCAs(rootCA)
syswidecas.addCAs(rootCACertFilePath)
// Load in and return the certificates in an object that can be passed
// directly to https.createServer() if required.
......
......@@ -10,6 +10,7 @@
import os from 'os'
import path from 'path'
import AutoEncryptLocalhost from '../index.js'
export const version = '1.4.3'
......@@ -34,5 +35,4 @@ if (platform === undefined) throw new Error('Unsupported platform', os.platform(
if (architecture === undefined) throw new Error('Unsupported architecture', os.arch())
export const binaryName = `mkcert-v${version}-${platform}-${architecture}${platform === 'windows' ? '.exe' : ''}`
export const binaryPath = path.resolve(path.join(__dirname, '..', 'mkcert-bin', binaryName))
export const binaryPath = path.join(AutoEncryptLocalhost.settingsPath, binaryName)
......@@ -22,7 +22,7 @@
"lib"
],
"scripts": {
"postinstall": "node bin/install-mkcert.js",
"postinstall": "node bin/post-install.js",
"start": "node index.js",
"test": "QUIET=true esm-tape-runner 'test/**/*.js' | tap-monkey",
"coverage": "QUIET=true c8 esm-tape-runner 'test/**/*.js' | tap-monkey",
......
......@@ -5,6 +5,8 @@ import bent from 'bent'
import test from 'tape'
import AutoEncryptLocalhost from '../index.js'
import '../bin/post-install.js'
const downloadString = bent('GET', 'string')
const downloadBuffer = bent('GET', 'buffer')
......@@ -15,24 +17,19 @@ async function asyncForEach(array, callback) {
}
test('certificate creation', async t => {
t.plan(16)
const defaultSettingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
const keyFilePath = path.join(defaultSettingsPath, 'localhost-key.pem')
const certFilePath = path.join(defaultSettingsPath, 'localhost.pem')
const settingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost')
// Remove the settings path in case it already exists.
fs.removeSync(defaultSettingsPath)
const keyFilePath = path.join(settingsPath, 'localhost-key.pem')
const certFilePath = path.join(settingsPath, 'localhost.pem')
// Run Auto Encrypt Localhost.
const server = AutoEncryptLocalhost.https.createServer((request, response) => {
response.end('ok')
})
t.ok(fs.existsSync(path.join(defaultSettingsPath)), 'Main settings path exists.')
t.ok(fs.existsSync(path.join(defaultSettingsPath, 'rootCA.pem')), 'Local certificate authority exists.')
t.ok(fs.existsSync(path.join(defaultSettingsPath, 'rootCA-key.pem')), 'Local certificate authority private key exists.')
t.ok(fs.existsSync(path.join(settingsPath)), 'Main settings path exists.')
t.ok(fs.existsSync(path.join(settingsPath, 'rootCA.pem')), 'Local certificate authority exists.')
t.ok(fs.existsSync(path.join(settingsPath, 'rootCA-key.pem')), 'Local certificate authority private key exists.')
t.ok(fs.existsSync(certFilePath), 'Local certificate exists.')
t.ok(fs.existsSync(keyFilePath), 'Local certificate private key exists.')
......@@ -76,26 +73,6 @@ test('certificate creation', async t => {
// Wait the for the first server to close.
await new Promise((resolve, reject) => { server.close(() => { resolve() }) })
//
// Custom settings path.
//
const customSettingsPath = path.join(os.homedir(), '.small-tech.org', 'auto-encrypt-localhost-custom-directory-test', 'second-level-directory')
// Remove the custom settings path in case it already exists.
fs.removeSync(customSettingsPath)
const server2 = AutoEncryptLocalhost.https.createServer({ settingsPath: customSettingsPath })
t.ok(fs.existsSync(path.join(customSettingsPath)), '(Custom settings path) Main directory exists.')
t.ok(fs.existsSync(path.join(customSettingsPath, 'rootCA.pem')), '(Custom settings path) Local certificate authority exists.')
t.ok(fs.existsSync(path.join(customSettingsPath, 'rootCA-key.pem')), '(Custom settings path) Local certificate authority private key exists.')
t.ok(fs.existsSync(path.join(customSettingsPath, 'localhost.pem')), '(Custom settings path) Local certificate exists.')
t.ok(fs.existsSync(path.join(customSettingsPath, 'localhost-key.pem')), '(Custom settings path) Local certificate private key exists.')
// Wait for the second server to close.
await new Promise((resolve, reject) => { server2.close(() => { resolve() }) })
t.end()
})
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment