Skip to content

Commit

Permalink
Initial pass
Browse files Browse the repository at this point in the history
  • Loading branch information
davewasmer committed Mar 29, 2017
0 parents commit 967e4c1
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules
49 changes: 49 additions & 0 deletions openssl.conf
@@ -0,0 +1,49 @@
[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
policy = policy_loose
database = index.txt
serial = serial
prompt = no

[ policy_loose ]
# Only require minimal information for development certificates
commonName = supplied

[ req ]
# Options for the `req` tool (`man req`).
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only

# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256

# Extension to add when the -x509 option is used.
x509_extensions = v3_ca

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
commonName = Common Name

[ v3_ca ]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
41 changes: 41 additions & 0 deletions package.json
@@ -0,0 +1,41 @@
{
"name": "devcert",
"version": "1.0.0",
"description": "Generate trusted local SSL/TLS certificates for local SSL development",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/davewasmer/devcert.git"
},
"keywords": [
"ssl",
"certificate",
"openssl",
"trust"
],
"author": "Dave Wasmer",
"license": "MIT",
"bugs": {
"url": "https://github.com/davewasmer/devcert/issues"
},
"homepage": "https://github.com/davewasmer/devcert#readme",
"devDependencies": {
"typescript": "^2.2.2"
},
"dependencies": {
"@types/configstore": "^2.1.1",
"@types/es6-promise": "^0.0.32",
"@types/get-port": "^0.0.4",
"@types/glob": "^5.0.30",
"@types/node": "^7.0.11",
"@types/tmp": "^0.0.32",
"command-exists": "^1.2.2",
"configstore": "^3.0.0",
"get-port": "^3.0.0",
"glob": "^7.1.1",
"tmp": "^0.0.31"
}
}
195 changes: 195 additions & 0 deletions src/index.ts
@@ -0,0 +1,195 @@
import {
readFileSync,
readdirSync,
writeFileSync,
unlinkSync,
chmodSync,
existsSync
} from 'fs';
import * as path from 'path';
import * as getPort from 'get-port';
import * as http from 'http';
import { execSync } from 'child_process';
import * as tmp from 'tmp';
import * as glob from 'glob';
import * as Configstore from 'configstore';
import { sync as commandExists } from 'command-exists';

const isMac = process.platform === 'darwin';
const isLinux = process.platform === 'linux';
const isWindows = process.platform === 'win32';

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

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

interface Options {
installCertutil?: boolean;
}

interface Certificate {
key: string;
cert: string;
}

export default function devcert(appName: string, options: Options = {}) {

// Fail fast on unsupported platforms (PRs welcome!)
if (!isMac && !isLinux && !isWindows) {
throw new Error(`devcert: "${ process.platform }" platform not supported`);
}
if (!commandExists('openssl')) {
throw new Error('Unable to find openssl - make sure it is installed and available in your PATH');
}

if (!existsSync(configPath('devcert-ca-root.key'))) {
installCertificateAuthority(options.installCertutil);
}

// Load our root CA and sign a new app cert with it.
let appKeyPath = generateKey(appName);
let appCertificatePath = generateSignedCertificate(appName, appKeyPath);

return {
keyPath: appKeyPath,
certificatePath: appCertificatePath,
key: readFileSync(appKeyPath),
certificate: readFileSync(appCertificatePath)
};

}

// Install the once-per-machine trusted root CA. We'll use this CA to sign per-app certs, allowing
// us to minimize the need for elevated permissions while still allowing for per-app certificates.
function installCertificateAuthority(installCertutil) {
let rootKeyPath = generateKey('devcert-ca-root');
execSync(`openssl req -config ${ opensslConfPath } -key ${ rootKeyPath } -out ${ rootCertPath } -new -subj '/CN=devcert' -x509 -days 7000 -extensions v3_ca`);
addCertificateToTrustStores(installCertutil);
}

// Generate a cryptographic key, used to sign certificates or certificate signing requests.
function generateKey(name: string): string {
let filename = configPath(`${ name }.key`);
execSync(`openssl genrsa -out ${ filename } 2048`);
chmodSync(filename, 400);
return filename;
}

// Generate a certificate signed by the devcert root CA
function generateSignedCertificate(name: string, keyPath: string): string {
let csrFile = configPath(`${ name }.csr`)
execSync(`openssl req -config ${ opensslConfPath } -subj '/CN=${ name }' -key ${ keyPath } -out ${ csrFile } -new`);
let certPath = configPath(`${ name }.crt`);
execSync(`openssl ca -config ${ opensslConfPath } -in ${ csrFile } -out ${ certPath } -keyfile ${ rootKeyPath } -cert ${ rootCertPath } -notext -md sha256 -days 7000 -extensions server_cert`)
return certPath;
}

// Add the devcert root CA certificate to the trust stores for this machine. Adds to OS level trust
// stores, and where possible, to browser specific trust stores
async function addCertificateToTrustStores(installCertutil: boolean): Promise<void> {

if (isMac) {
// Chrome, Safari, system utils
execSync(`sudo security add-trusted-cert -r trustRoot -k /Library/Keychains/System.keychain -p ssl "${ rootCertPath }"`);
// Firefox
try {
// Try to use certutil to install the cert automatically
addCertificateToNSSCertDB('~/Library/Application Support/Firefox/Profiles/*', installCertutil);
} catch (e) {
// Otherwise, open the cert in Firefox to install it
await openCertificateInFirefox('/Applications/Firefox.app/Contents/MacOS/firefox');
}

} else if (isLinux) {
// system utils
execSync(`sudo cp ${ rootCertPath } /usr/local/share/ca-certificates/devcert.cer && update-ca-certificates`);
// Firefox
try {
// Try to use certutil to install the cert automatically
addCertificateToNSSCertDB('~/.mozilla/firefox/*', installCertutil);
} catch (e) {
// Otherwise, open the cert in Firefox to install it
await openCertificateInFirefox('firefox');
}
// Chrome
// No try..catch, since there's no alternative here. Chrome won't prompt to add a cert to the
// store if opened as a URL
addCertificateToNSSCertDB('~/.pki/nssdb', installCertutil);

// Windows
} else if (isWindows) {
// IE, Chrome, system utils
execSync(`certutil -addstore -user root ${ rootCertPath }`);
// Firefox (don't even try NSS certutil, no easy install for Windows)
await openCertificateInFirefox('start firefox');
}

}

// Try to use certutil to add the root cert to an NSS database
function addCertificateToNSSCertDB(nssDirGlob: string, installCertutil: boolean): void {
let certutilPath = lookupOrInstallCertutil(installCertutil);
if (!certutilPath) {
throw new Error('certutil not available, and `installCertutil` was false');
}
glob.sync(nssDirGlob).forEach((potentialNSSDBDir) => {
if (existsSync(path.join(potentialNSSDBDir, 'cert8.db'))) {
execSync(`${ certutilPath } -A -d ${ potentialNSSDBDir } -t 'C,,' -i ${ rootCertPath }`);
} else if (existsSync(path.join(potentialNSSDBDir, 'cert9.db'))) {
execSync(`${ certutilPath } -A -d sql:${ potentialNSSDBDir } -t 'C,,' -i ${ rootCertPath }`);
}
});
}

// Launch a web server and open the root cert in Firefox. Useful for when certutil isn't available
async function openCertificateInFirefox(firefoxPath: string) {
let port = await getPort();
let server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-type': 'application/x-x509-ca-cert' });
res.write(readFileSync(rootCertPath));
res.end();
}).listen(port);
execSync(`${ firefoxPath } http://localhost:${ port }`);
await new Promise((resolve) => {
process.stdin.resume();
process.stdin.on('data', resolve);
});
}

// Try to install certutil if it's not already available, and return the path to the executable
function lookupOrInstallCertutil(options: Options): boolean | string {
if (isMac) {
if (commandExists('brew')) {
let nssPath: string;
try {
return path.join(execSync('brew --prefix nss').toString(), 'bin', 'certutil');
} catch (e) {
if (options.installCertutil) {
execSync('brew install nss');
return path.join(execSync('brew --prefix nss').toString(), 'bin', 'certutil');
}
}
}
} else if (isLinux) {
if (!commandExists('certutil')) {
if (options.installCertutil) {
execSync('sudo apt install libnss3-tools');
} else {
return false;
}
}
return execSync('which certutil').toString();
}
return false;
}
18 changes: 18 additions & 0 deletions tsconfig.json
@@ -0,0 +1,18 @@
{
"compileOnSave": false,
"compilerOptions": {
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "ES2016",
"noImplicitAny": true,
"sourceMap": false,
"importHelpers": true,
"inlineSourceMap": true,
"inlineSources": true,
"outDir": "dist",
"baseUrl": ".",
"skipLibCheck": true,
"sourceRoot": ".",
}
}

0 comments on commit 967e4c1

Please sign in to comment.