UNPKG

10.6 kBPlain TextView Raw
1import { readFileSync, writeFileSync, existsSync } from 'fs';
2import { exec } from 'child_process';
3import * as http from 'http';
4import * as path from 'path';
5import * as getPort from 'get-port';
6import * as createDebug from 'debug';
7import { sync as commandExists } from 'command-exists';
8import * as glob from 'glob';
9import * as eol from 'eol';
10
11import {
12 isMac,
13 isLinux,
14 isWindows,
15 configPath,
16 rootKeyPath,
17 rootCertPath,
18 opensslConfPath,
19 opensslConfTemplate
20} from './constants';
21import {
22 openssl,
23 generateKey,
24 run,
25 waitForUser
26} from './utils';
27
28const debug = createDebug('devcert');
29
30// Install the once-per-machine trusted root CA. We'll use this CA to sign per-app certs, allowing
31// us to minimize the need for elevated permissions while still allowing for per-app certificates.
32export default async function installCertificateAuthority(installCertutil: boolean): Promise<void> {
33 debug(`generating openssl configuration`);
34 generateOpenSSLConfFiles();
35
36 debug(`generating root certificate authority key`);
37 generateKey(rootKeyPath);
38
39 debug(`generating root certificate authority certificate`);
40 openssl(`req -config ${ opensslConfPath } -key ${ rootKeyPath } -out ${ rootCertPath } -new -subj "/CN=devcert" -x509 -days 7000 -extensions v3_ca`);
41
42 debug(`adding root certificate authority to trust stores`)
43 if (isMac) {
44 await addToMacTrustStores(installCertutil);
45 } else if (isLinux) {
46 await addToLinuxTrustStores(installCertutil);
47 } else {
48 await addToWindowsTrustStores();
49 }
50}
51
52// Copy our openssl conf template to the local config folder, and update the paths to be OS
53// specific. Also initializes the files openssl needs to sign certificates as a certificate
54// authority
55function generateOpenSSLConfFiles() {
56 let confTemplate = readFileSync(opensslConfTemplate, 'utf-8');
57 confTemplate = confTemplate.replace(/DATABASE_PATH/, configPath('index.txt').replace(/\\/g, '\\\\'));
58 confTemplate = confTemplate.replace(/SERIAL_PATH/, configPath('serial').replace(/\\/g, '\\\\'));
59 confTemplate = eol.auto(confTemplate);
60 writeFileSync(opensslConfPath, confTemplate);
61 writeFileSync(configPath('index.txt'), '');
62 writeFileSync(configPath('serial'), '01');
63 // This version number lets us write code in the future that intelligently upgrades an existing
64 // devcert installation. This "ca-version" is independent of the devcert package version, and
65 // tracks changes to the root certificate setup only.
66 writeFileSync(configPath('devcert-ca-version'), '1');
67}
68
69// macOS is pretty simple - just add the certificate to the system keychain, and most applications
70// will delegate to that for determining trusted certificates. Firefox, of course, does it's own
71// thing. We can try to automatically install the cert with Firefox if we can use certutil via the
72// `nss` Homebrew package, otherwise we go manual with user-facing prompts.
73async function addToMacTrustStores(installCertutil: boolean): Promise<void> {
74 // Chrome, Safari, system utils
75 debug('adding devcert root CA to macOS system keychain');
76 run(`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain -p ssl -p basic "${ rootCertPath }"`);
77 // Firefox
78 try {
79 // Try to use certutil to install the cert automatically
80 debug('adding devcert root CA to firefox');
81 await addCertificateToNSSCertDB(path.join(process.env.HOME, 'Library/Application Support/Firefox/Profiles/*'), {
82 installCertutil,
83 checkForOpenFirefox: true
84 });
85 } catch (e) {
86 // Otherwise, open the cert in Firefox to install it
87 await openCertificateInFirefox('/Applications/Firefox.app/Contents/MacOS/firefox');
88 }
89}
90
91// Linux is surprisingly difficult. There seems to be multiple system-wide repositories for certs,
92// so we copy ours to each. However, Firefox does it's usual separate trust store. Plus Chrome
93// relies on the NSS tooling (like Firefox), but uses the user's NSS database, unlike Firefox which
94// uses a separate Mozilla one. And since Chrome doesn't prompt the user with a GUI flow when
95// opening certs, if we can't use certutil, we're out of luck.
96async function addToLinuxTrustStores(installCertutil: boolean): Promise<void> {
97 // system utils
98 debug('adding devcert root CA to linux system-wide certificates');
99 run(`sudo cp ${ rootCertPath } /etc/ssl/certs/devcert.pem`);
100 run(`sudo cp ${ rootCertPath } /usr/local/share/ca-certificates/devcert.cer`);
101 run(`sudo update-ca-certificates`);
102 // Firefox
103 try {
104 // Try to use certutil to install the cert automatically
105 debug('adding devcert root CA to firefox');
106 await addCertificateToNSSCertDB(path.join(process.env.HOME, '.mozilla/firefox/*'), {
107 installCertutil,
108 checkForOpenFirefox: true
109 });
110 } catch (e) {
111 // Otherwise, open the cert in Firefox to install it
112 await openCertificateInFirefox('firefox');
113 }
114 // Chrome
115 try {
116 debug('adding devcert root CA to chrome');
117 await addCertificateToNSSCertDB(path.join(process.env.HOME, '.pki/nssdb'), { installCertutil });
118 } catch (e) {
119 console.warn(`
120WARNING: Because you did not pass in \`installCertutil: true\` to devcert, we
121are unable to update Chrome to automatically trust generated development
122certificates. The certificates will work, but Chrome will continue to warn you
123that they are untrusted.`);
124 }
125}
126
127// Windows is at least simple. Like macOS, most applications will delegate to the system trust
128// store, which is updated with the confusingly named `certutil` exe (not the same as the
129// NSS/Mozilla certutil). Firefox does it's own thing as usual, and getting a copy of NSS certutil
130// onto the Windows machine to try updating the Firefox store is basically a nightmare, so we don't
131// even try it - we just bail out to the GUI.
132async function addToWindowsTrustStores(): Promise<void> {
133 // IE, Chrome, system utils
134 debug('adding devcert root to Windows OS trust store')
135 run(`certutil -addstore -user root ${ rootCertPath }`);
136 // Firefox (don't even try NSS certutil, no easy install for Windows)
137 await openCertificateInFirefox('start firefox');
138}
139
140// Given a directory or glob pattern of directories, attempt to install the certificate to each
141// directory containing an NSS database.
142async function addCertificateToNSSCertDB(nssDirGlob: string, options: { installCertutil?: boolean, checkForOpenFirefox?: boolean } = {}): Promise<void> {
143 let certutilPath = lookupOrInstallCertutil(options.installCertutil);
144 if (!certutilPath) {
145 throw new Error('certutil not available, and `installCertutil` was false');
146 }
147 // Firefox appears to load the NSS database in-memory on startup, and overwrite on exit. So we
148 // have to ask the user to quite Firefox first so our changes don't get overwritten.
149 if (options.checkForOpenFirefox) {
150 let runningProcesses = run('ps aux');
151 if (runningProcesses.indexOf('firefox') > -1) {
152 console.log('Please close Firefox before continuing (Press <Enter> when ready)');
153 await waitForUser();
154 }
155 }
156 debug(`trying to install certificate into NSS databases in ${ nssDirGlob }`);
157 glob.sync(nssDirGlob).forEach((potentialNSSDBDir) => {
158 debug(`checking to see if ${ potentialNSSDBDir } is a valid NSS database directory`);
159 if (existsSync(path.join(potentialNSSDBDir, 'cert8.db'))) {
160 debug(`Found legacy NSS database in ${ potentialNSSDBDir }, adding devcert ...`)
161 run(`${ certutilPath } -A -d "${ potentialNSSDBDir }" -t 'C,,' -i ${ rootCertPath } -n devcert`);
162 } else if (existsSync(path.join(potentialNSSDBDir, 'cert9.db'))) {
163 debug(`Found modern NSS database in ${ potentialNSSDBDir }, adding devcert ...`)
164 run(`${ certutilPath } -A -d "sql:${ potentialNSSDBDir }" -t 'C,,' -i ${ rootCertPath } -n devcert`);
165 }
166 });
167}
168
169// When a Firefox tab is directed to a URL that returns a certificate, it will automatically prompt
170// the user if they want to add it to their trusted certificates. This is handy since Firefox is by
171// far the most troublesome to handle. If we can't automatically install the certificate (because
172// certutil is not available / installable), we instead start a quick web server and host our
173// certificate file. Then we open the hosted cert URL in Firefox to kick off the GUI flow.
174async function openCertificateInFirefox(firefoxPath: string): Promise<void> {
175 debug('adding devert to firefox manually - launch webserver for certificate hosting');
176 let port = await getPort();
177 let server = http.createServer((req, res) => {
178 res.writeHead(200, { 'Content-type': 'application/x-x509-ca-cert' });
179 res.write(readFileSync(rootCertPath));
180 res.end();
181 }).listen(port);
182 debug('certificate is hosted, starting firefox at hosted URL');
183 console.log(`Unable to automatically install SSL certificate - please follow the prompts at http://localhost:${ port } in Firefox to trust the root certificate`);
184 console.log('See https://github.com/davewasmer/devcert#how-it-works for more details');
185 console.log('-- Press <Enter> once you finish the Firefox prompts --');
186 exec(`${ firefoxPath } http://localhost:${ port }`);
187 await waitForUser();
188}
189
190// Try to install certutil if it's not already available, and return the path to the executable
191function lookupOrInstallCertutil(installCertutil: boolean): boolean | string {
192 debug('looking for nss tooling ...')
193 if (isMac) {
194 debug('on mac, looking for homebrew (the only method to install nss that is currently supported by devcert');
195 if (commandExists('brew')) {
196 let nssPath: string;
197 let certutilPath: string;
198 try {
199 certutilPath = path.join(run('brew --prefix nss').toString().trim(), 'bin', 'certutil');
200 } catch (e) {
201 debug('brew was found, but nss is not installed');
202 if (installCertutil) {
203 debug('attempting to install nss via brew');
204 run('brew install nss');
205 certutilPath = path.join(run('brew --prefix nss').toString().trim(), 'bin', 'certutil');
206 } else {
207 return false;
208 }
209 }
210 debug(`Found nss installed at ${ certutilPath }`);
211 return certutilPath;
212 }
213 } else if (isLinux) {
214 debug('on linux, checking is nss is already installed');
215 if (!commandExists('certutil')) {
216 if (installCertutil) {
217 debug('not already installed, installing it ourselves');
218 run('sudo apt install libnss3-tools');
219 } else {
220 debug('not installed and do not want to install');
221 return false;
222 }
223 }
224 debug('looks like nss is installed');
225 return run('which certutil').toString().trim();
226 }
227 // Windows? Ha!
228 return false;
229}
\No newline at end of file