Skip to content

Commit

Permalink
refactor structure
Browse files Browse the repository at this point in the history
  • Loading branch information
davewasmer committed Jan 7, 2018
1 parent e641091 commit e0ad2d6
Show file tree
Hide file tree
Showing 16 changed files with 859 additions and 467 deletions.
88 changes: 42 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,70 +8,66 @@ devcert makes the process easy. Want a private key and certificate file to use
with your server? Just ask:

```js
import * as https from 'https';
import * as express from 'express';
import getDevelopmentCertificate from 'devcert';

let app = express();

app.get('/', function (req, res) {
res.send('Hello Secure World!');
});

getDevelopmentCertificate('my-app', { installCertutil: true }).then((ssl) => {
https.createServer(ssl, app).listen(3000);
let { key, cert } = await devcert.certificateFor('my-app.dev');
https.createServer({ key, cert }, app).listen(3000);
});
```

Now open https://localhost:3000 and voila - your page loads with no scary
Now open https://my-app.dev:3000 and voila - your page loads with no scary
warnings or hoops to jump through.

> Certificates are cached by name, so two calls for
`getDevelopmentCertificate('foo')` will return the same key and certificate.
`certificateFor('foo')` will return the same key and certificate.

## Options

### installCertutil option
### skipHostsFile

devcert currently takes a single option: `installCertutil`. If true, devcert
will attempt to install some software necessary to tell Firefox (and Chrome,
only on Linux) to trust your development certificates. This is not required,
but without it, you'll need to tell Firefox to trust these certificates
manually.
If you supply a custom domain name (i.e. any domain other than `localhost`) when requesting a certificate from devcert, it will attempt to modify your system to redirect requests for that domain to your local machine (rather than to the real domain). It does this by modifying your `/etc/hosts` file (or the equivalent file for Windows).

Thankully, Firefox makes this easy. There's a point-and-click wizard for
importing and trusting a certificate, so if you don't provide `installCertutil:
true` to devcert, devcert will instead automatically open Firefox and kick off
this wizard for you. Simply follow the prompts to trust the certificate.
**Reminder: you'll only need to do this once per machine**
If you pass in the `skipHostsFile` option, devcert will skip this step. This means that if you ask for certificates for `my-app.dev` (for example), and don't have some other DNS redirect method in place, that you won't be able to access your app at `my-app.dev`.

**Note:** Chrome on Linux **requires** `installCertutil: true`, or else you'll
face the scary browser warnings every time. Unfortunately, there's no way to
tell Chrome on Linux to trust a certificate without install certutil.
### skipCertutil

The software installed varies by OS:
This option will tell devcert to avoid installing `certutil` tooling.

`certutil` is a tooling package used to automated the installation of SSL certificates in certain circumstances; specifically, Firefox (for every OS) and Chrome (on Linux only). Without it, the install process changes for those two scenarios:

**Firefox**: Thankully, Firefox makes this easy. There's a point-and-click wizard for importing and trusting a certificate, so if you don't provide `installCertutil: true` to devcert, devcert will instead automatically open Firefox and kick off this wizard for you. Simply follow the prompts to trust the certificate. **Reminder: you'll only need to do this once per machine**

**Chrome on Linux**: Unfortunately, it appears that the **only** way to get Chrome to trust an SSL certificate on Linux is via the `certutil` tooling - there is no manual process for it. Thus, if you are using Chrome on Linux, do **not** supply `skipCertuil: true`.

The `certutil` tooling is installed in OS-specific ways:

* Mac: `brew install nss`
* Linux: `apt install libnss3-tools`
* Windows: N/A

## How it works

When you ask for a development certificate, devcert will first check to see if
it has run on this machine before. If not, it will create a root certificate
authority and add it to your OS and various browser trust stores. You'll likely
see password prompts from your OS at this point to authorize the new root CA.
This is the only time you'll see these prompts.

This root certificate authority allows devcert to create a new SSL certificate
whenever you want without needing to ask for elevated permissions again. It also
ensures that browsers won't show scary warnings about untrusted certificates,
since your OS and browsers will now trust devcert's certificates. The root CA
certificate is unique to your machine only, and is generated on-the-fly when it
is first installed.

Once devcert is sure that it has a root certificate authority installed, it will
create a new SSL certificate & key pair for your app, signed by this root
certificate authority. Since your browser & OS now trust the root authority,
they'll trust the certificate for your app - no more scary warnings!
When you ask for a development certificate, devcert will first check to see if it has run on this machine before. If not, it will create a root certificate authority and add it to your OS and various browser trust stores. You'll likely see password prompts from your OS at this point to authorize the new root CA. Once this root CA is trusted by your machine, devcert will safely store the root CA credentials used to sign certificates in the operating system's secret store. This prevents malicious processes from access those keys to generated trusted certificates.

Since your machine now trusts this root CA, it will trust any certificates signed by it. So when you ask for a certificate, devcert will pull the root CA credentials out of the operating system secret storage (triggering a root password as it does). It then uses those credentials to generate a certificate specific to the domain you requested, and returns the new certificate to you.

If you request a domain that has already had certificates generated for it, devcert will simply return the cached certificates - no additional root password prompting needed.

This setup ensures that browsers won't show scary warnings about untrusted certificates, since your OS and browsers will now trust devcert's certificates. The root CA certificate is unique to your machine only, is generated on-the-fly when it is first installed, and stored in the system secret storage, so attackers should not be able to compromise it to generate their own certificates.

### Why install a root certificate authority?

The root certificate authority makes it slightly simpler to manage which domains are configured for SSL by devcert. The alternative is to generate and trust self-signed certificates for each domain. The problem is that while devcert is able to add a certificate to your machine's trust stores, the tooling to remove a certificate doesn't cover every case.

By trusting only a single root CA, devcert is able to guarantee that when you want to _disable_ SSL for a domain, it can do so with no manual intervention - we just delete the certificate files and that's it.

## Testing

If you want to test a contribution to devcert, it comes packaged with a Vagrantfile to help make testing easier. The Vagrantfile will spin up three virtual machines, one for each supported platform: macOS, Linux, and Windows.

Launch the VMs with `vagrant up`, which should start all three in GUI mode. Each VM is a snapshot, with instructions for testing on screen already. Just follow the instructions to test each.

You can also use snapshots of the VMs to roll them back to a pristine state for another round of testing. Just `vagrant snapshot push` on the intial bootup of the VMs, and `vagrant snapshot pop` to roll it back to the pristine state later.

**Note**: Be aware that the macOS license terms prohibit running it on non-Apple hardware, so you must own a Mac to test that platform. If you don't own a Mac - that's okay, just mention in the PR that you were unable to test on a Mac and we're happy to test it for you.

## License

Expand Down
20 changes: 20 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Vagrant.configure("2") do |config|

config.vm.define "mac" do |mac|
config.vm.box = "devcert/macos"
config.vm.network "public_network"

config.vm.define "linux" do |linux|
config.vm.box = "devcert/linux"
config.vm.network "public_network"

config.vm.define "windows" do |windows|
config.vm.box = "devcert/windows"
config.vm.network "public_network"

config.vm.provider "virtualbox" do |vb|
# Display the VirtualBox GUI when booting the machine
vb.gui = true
end

end
33 changes: 18 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "dist/index.js",
"scripts": {
"test": "echo \"Ha.\" && exit 1",
"prepublish": "tsc"
"prepublishOnly": "tsc"
},
"repository": {
"type": "git",
Expand All @@ -24,25 +24,28 @@
},
"homepage": "https://github.com/davewasmer/devcert#readme",
"devDependencies": {
"standard-version": "^4.0.0",
"typescript": "^2.2.2"
"standard-version": "^4.3.0",
"typescript": "^2.6.2"
},
"dependencies": {
"@types/configstore": "^2.1.1",
"@types/debug": "^0.0.29",
"@types/get-port": "^0.0.4",
"@types/glob": "^5.0.30",
"@types/mkdirp": "^0.3.29",
"@types/node": "^7.0.11",
"@types/tmp": "^0.0.32",
"@types/debug": "^0.0.30",
"@types/get-port": "^3.2.0",
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/node": "^8.5.7",
"@types/rimraf": "^2.0.2",
"@types/tmp": "^0.0.33",
"application-config-path": "^0.1.0",
"command-exists": "^1.2.2",
"configstore": "^3.0.0",
"debug": "^2.6.3",
"eol": "^0.8.1",
"get-port": "^3.0.0",
"glob": "^7.1.1",
"debug": "^3.1.0",
"eol": "^0.9.1",
"get-port": "^3.2.0",
"glob": "^7.1.2",
"mkdirp": "^0.5.1",
"tmp": "^0.0.31",
"tslib": "^1.6.0"
"rimraf": "^2.6.2",
"tmp": "^0.0.33",
"tslib": "^1.8.1"
}
}
66 changes: 66 additions & 0 deletions src/certificate-authority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { readFileSync as readFile, writeFileSync as writeFile } from 'fs';
import * as createDebug from 'debug';
import * as eol from 'eol';

import {
isMac,
isLinux,
configPath,
rootKeyPath,
rootCertPath,
opensslConfPath,
opensslConfTemplate
} from './constants';
import addToMacTrustStores from './platforms/macos';
import addToLinuxTrustStores from './platforms/linux';
import addToWindowsTrustStores from './platforms/windows';
import { openssl } from './utils';
import { generateKey } from './certificates';
import { Options } from './index';

const debug = createDebug('devcert:certificate-authority');

/**
* Install the once-per-machine trusted root CA. We'll use this CA to sign
* per-app certs.
*/
export default async function installCertificateAuthority(options: Options = {}): Promise<void> {
debug(`Generating a root certificate authority`);

debug(`Generating the OpenSSL configuration needed to setup the certificate authority`);
generateOpenSSLConfFiles();

debug(`Generating a private key`);
generateKey(rootKeyPath);

debug(`Generating a CA certificate`);
openssl(`req -config ${ opensslConfPath } -key ${ rootKeyPath } -out ${ rootCertPath } -new -subj "/CN=devcert" -x509 -days 7000 -extensions v3_ca`);

debug(`Adding the root certificate authority to trust stores`);
if (isMac) {
await addToMacTrustStores(rootCertPath, options);
} else if (isLinux) {
await addToLinuxTrustStores(rootCertPath, options);
} else {
await addToWindowsTrustStores(rootCertPath, options);
}
}

/**
* Copy our OpenSSL conf template to the local devcert config folder, and
* update the paths inside that config file to be OS specific. Also initializes
* the files OpenSSL needs to sign certificates as a certificate authority
*/
function generateOpenSSLConfFiles() {
let confTemplate = readFile(opensslConfTemplate, 'utf-8');
confTemplate = confTemplate.replace(/DATABASE_PATH/, configPath('index.txt').replace(/\\/g, '\\\\'));
confTemplate = confTemplate.replace(/SERIAL_PATH/, configPath('serial').replace(/\\/g, '\\\\'));
confTemplate = eol.auto(confTemplate);
writeFile(opensslConfPath, confTemplate);
writeFile(configPath('index.txt'), '');
writeFile(configPath('serial'), '01');
// This version number lets us write code in the future that intelligently upgrades an existing
// devcert installation. This "ca-version" is independent of the devcert package version, and
// tracks changes to the root certificate setup only.
writeFile(configPath('devcert-ca-version'), '1');
}
29 changes: 29 additions & 0 deletions src/certificates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as path from 'path';
import * as createDebug from 'debug';
import { chmodSync as chmod } from 'fs';
import { pathForDomain, opensslConfPath, rootKeyPath, rootCertPath } from './constants';
import { openssl } from './utils';

const debug = createDebug('devcert:certificates');

// Generate an app certificate signed by the devcert root CA
export default function generateSignedCertificate(domain: string): void {
debug(`Generating private key for ${ domain }`);
let keyPath = pathForDomain(domain, 'private-key.key');
generateKey(keyPath);

debug(`Generating certificate signing request for ${ domain }`);
let csrFile = pathForDomain(domain, `${ domain }.csr`);
openssl(`req -config ${ opensslConfPath } -subj "/CN=${ domain }" -key ${ keyPath } -out ${ csrFile } -new`);

debug(`Generating certificate for ${ domain } from signing request and signing with root CA`);
let certPath = pathForDomain(`${ domain }.crt`);
openssl(`ca -config ${ opensslConfPath } -in ${ csrFile } -out ${ path.basename(certPath) } -outdir ${ path.dirname(certPath) } -keyfile ${ rootKeyPath } -cert ${ rootCertPath } -notext -md sha256 -days 7000 -batch -extensions server_cert`)
}

// Generate a cryptographic key, used to sign certificates or certificate signing requests.
export function generateKey(filename: string): void {
debug(`generateKey: ${ filename }`);
openssl(`genrsa -out ${ filename } 2048`);
chmod(filename, 400);
}
19 changes: 8 additions & 11 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import * as path from 'path';
import * as mkdirp from 'mkdirp';
import applicationConfigPath = require('application-config-path');

// Platform shortcuts
export const isMac = process.platform === 'darwin';
export const isLinux = process.platform === 'linux';
export const isWindows = process.platform === 'win32';

// use %LOCALAPPDATA%/devcert on Windows otherwise use ~/.config/devcert
export let configDir: string;
if (isWindows && process.env.LOCALAPPDATA) {
configDir = path.join(process.env.LOCALAPPDATA, 'devcert', 'config');
} else {
let uid = process.getuid && process.getuid();
let userHome = (isLinux && uid === 0) ? path.resolve('/usr/local/share') : require('os').homedir();
configDir = path.join(userHome, '.config', 'devcert');
}
// Common paths
export const configDir = applicationConfigPath('devcert');
export const configPath: (...pathSegments: string[]) => string = path.join.bind(path, configDir);

export const domainsDir = configPath('domains');
export const pathForDomain: (domain: string, ...pathSegments: string[]) => string = path.join.bind(path, domainsDir)

export const opensslConfTemplate = path.join(__dirname, '..', 'openssl.conf');
export const opensslConfPath = configPath('openssl.conf');
export const rootKeyPath = configPath('devcert-ca-root.key');
export const rootCertPath = configPath('devcert-ca-root.crt');
export const caCertsDir = configPath('certs');

mkdirp.sync(configDir);
mkdirp.sync(caCertsDir);
mkdirp.sync(domainsDir);
Loading

0 comments on commit e0ad2d6

Please sign in to comment.