1 | # devcert - Development SSL made easy
|
2 |
|
3 | So, running a local HTTPS server usually sucks. There's a range of
|
4 | approaches, each with their own tradeoff. The common one, using self-signed
|
5 | certificates, means having to ignore scary browser warnings for each project.
|
6 |
|
7 | devcert makes the process easy. Want a private key and certificate file to
|
8 | use with your server? Just ask:
|
9 |
|
10 | ```js
|
11 | let ssl = await devcert.certificateFor('my-app.test');
|
12 | https.createServer(ssl, app).listen(3000);
|
13 | ```
|
14 |
|
15 | Now open https://my-app.test:3000 and voila - your page loads with no scary
|
16 | warnings or hoops to jump through.
|
17 |
|
18 | > Certificates are cached by name, so two calls for
|
19 | `certificateFor('foo')` will return the same key and certificate.
|
20 |
|
21 | ## Options
|
22 |
|
23 | ### skipHostsFile
|
24 |
|
25 | If you supply a custom domain name (i.e. any domain other than `localhost`)
|
26 | when requesting a certificate from devcert, it will attempt to modify your
|
27 | system to redirect requests for that domain to your local machine (rather
|
28 | than to the real domain). It does this by modifying your `/etc/hosts` file.
|
29 |
|
30 | If you pass in the `skipHostsFile` option, devcert will skip this step. This
|
31 | means that if you ask for certificates for `my-app.test` (for example), and
|
32 | don't have some other DNS redirect method in place, that you won't be able to
|
33 | access your app at `https://my-app.test` because your computer wouldn't know
|
34 | that `my-app.test` should resolve your local machine.
|
35 |
|
36 | Keep in mind that SSL certificates are issued for _domains_, so if you ask
|
37 | for a certificate for `my-app.test`, and don't have any kind of DNS redirect
|
38 | in place (`/etc/hosts` or otherwise), trying to hit `localhost` won't work,
|
39 | even if the app you intended to serve via `my-app.test` is running on your
|
40 | local machine (since the SSL certificate won't say `localhost`).
|
41 |
|
42 | ### skipCertutil
|
43 |
|
44 | This option will tell devcert to avoid installing `certutil` tooling.
|
45 |
|
46 | `certutil` is a tooling package used to automated the installation of SSL
|
47 | certificates in certain circumstances; specifically, Firefox (for every OS)
|
48 | and Chrome (on Linux only).
|
49 |
|
50 | Normally, devcert will attempt to install `certutil` if it's need and not
|
51 | already present on your system. If don't want devcert to install this
|
52 | package, pass `skipCertutil: true`.
|
53 |
|
54 | If you decide to `skipCertutil`, the initial setup process for devcert
|
55 | changes in these two scenarios:
|
56 |
|
57 | * **Firefox on all platforms**: Thankully, Firefox makes this easy. There's a
|
58 | point-and-click wizard for importing and trusting a certificate, so if you
|
59 | specify `skipCertutil: true`, devcert will instead automatically open Firefox
|
60 | and kick off this wizard for you. Simply follow the prompts to trust the
|
61 | certificate. **Reminder: you'll only need to do this once per machine**
|
62 |
|
63 | * **Chrome on Linux**: Unfortunately, it appears that the **only** way to get
|
64 | Chrome to trust an SSL certificate on Linux is via the `certutil` tooling -
|
65 | there is no manual process for it. Thus, if you are using Chrome on Linux, do
|
66 | **not** supply `skipCertuil: true`. If you do, devcert certificates will not
|
67 | be trusted by Chrome.
|
68 |
|
69 | The `certutil` tooling is installed in OS-specific ways:
|
70 |
|
71 | * Mac: `brew install nss`
|
72 | * Linux: `apt install libnss3-tools`
|
73 | * Windows: N/A (there is no easy, hands-off way to install certutil on Windows,
|
74 | so devcert will simply fallback to the wizard approach for Firefox outlined
|
75 | above)
|
76 |
|
77 | ## How it works
|
78 |
|
79 | When you ask for a development certificate, devcert will first check to see
|
80 | if it has run on this machine before. If not, it will create a root
|
81 | certificate authority and add it to your OS and various browser trust stores.
|
82 | You'll likely see password prompts from your OS at this point to authorize
|
83 | the new root CA.
|
84 |
|
85 | Since your machine now trusts this root CA, it will trust any certificates
|
86 | signed by it. So when you ask for a certificate for a new domain, devcert
|
87 | will use the root CA credentials to generate a certificate specific to the
|
88 | domain you requested, and returns the new certificate to you.
|
89 |
|
90 | If you request a domain that has already had certificates generated for it,
|
91 | devcert will simply return the cached certificates.
|
92 |
|
93 | This setup ensures that browsers won't show scary warnings about untrusted
|
94 | certificates, since your OS and browsers will now trust devcert's
|
95 | certificates.
|
96 |
|
97 | ## Security Concerns
|
98 |
|
99 | There's a reason that your OS prompts you for your root password when devcert
|
100 | attempts to install it's root certificate authority. By adding it to your
|
101 | machine's trust stores, your browsers will automatically trust _any_ certificate
|
102 | generated with it.
|
103 |
|
104 | This exposes a potential attack vector on your local machine: if someone else
|
105 | could use the devcert certificate authority to generate certificates, and if
|
106 | they could intercept / manipulate your network traffic, they could theoretically
|
107 | impersonate some websites, and your browser would not show any warnings (because
|
108 | it trusts the devcert authority).
|
109 |
|
110 | To prevent this, devcert takes steps to ensure that no one can access the
|
111 | devcert certificate authority credentials to generate malicious certificates
|
112 | without you knowing. The exact approach varies by platform:
|
113 |
|
114 | * **macOS and Linux**: the certificate authority's credentials are written to files that are only readable by the root user (i.e. `chown 0 ca-cert.crt` and
|
115 | `chmod 600 ca-cert.crt`). When devcert itself needs these, it shells out to
|
116 | `sudo` invocations to read / write the credentials.
|
117 | * **Windows**: because of my unfamiliarity with Windows file permissions, I
|
118 | wasn't confident I would be able to correctly set permissions to mimic the setup
|
119 | on macOS and Linux. So instead, devcert will prompt you for a password, and then
|
120 | use that to encrypt the credentials with an AES256 cipher. The password is never
|
121 | written to disk.
|
122 |
|
123 | To further protect these credentials, any time they are written to disk, they
|
124 | are written to temporary files, and are immediately deleted after they are no longer needed.
|
125 |
|
126 | Additionally, the root CA certificate is unique to your machine only: it's
|
127 | generated on-the-fly when it is first installed. ensuring there are no
|
128 | central / shared keys to crack across machines.
|
129 |
|
130 | ### Why install a root certificate authority at all?
|
131 |
|
132 | The root certificate authority makes it simpler to manage which domains are
|
133 | configured for SSL by devcert. The alternative is to generate and trust
|
134 | self-signed certificates for each domain. The problem is that while devcert
|
135 | is able to add a certificate to your machine's trust stores, the tooling to
|
136 | remove a certificate doesn't cover every case. So if you ever wanted to
|
137 | _untrust_ devcert's certificates, you'd have to manually remove each one from
|
138 | each trust store.
|
139 |
|
140 | By trusting only a single root CA, devcert is able to guarantee that when you
|
141 | want to _disable_ SSL for a domain, it can do so with no manual intervention
|
142 | - we just delete the domain-specific certificate files. Since these
|
143 | domain-specific files aren't installed in your trust stores, once they are
|
144 | gone, they are gone.
|
145 |
|
146 |
|
147 | ## Integration
|
148 |
|
149 | devcert has been designed from day one to work as low-level library that other
|
150 | tools can delegate to. The goal is to make HTTPS development easy for everyone,
|
151 | regardless of framework or library choice.
|
152 |
|
153 | With that in mind, if you'd like to use devcert in your library/framework/CLI,
|
154 | devcert makes that easy.
|
155 |
|
156 | In addition to the options above, devcert exposes a `ui` option. This option
|
157 | allows you to control all the points where devcert requries user interaction,
|
158 | substituting your own prompts and user interface. You can use this to brand
|
159 | the experience with your own tool's name, localize the messages, or integrate
|
160 | devcert into a larger existing workflow.
|
161 |
|
162 | The `ui` option should be an object with the following methods:
|
163 |
|
164 | ```ts
|
165 | {
|
166 | async getWindowsEncryptionPassword(): Promise<string> {
|
167 | // Invoked when devcert needs the password used to encrypt the root
|
168 | // certificate authority credentials on Windows. May be invoked multiple
|
169 | // times if the user's supplied password is incorrect
|
170 | },
|
171 | async warnChromeOnLinuxWithoutCertutil(): Promise<string> {
|
172 | // Invoked when devcert is run on Linux, detects that Chrome is installed,
|
173 | // and the `skipCertutil` option is `true`. Used to warn the user that
|
174 | // Chrome will not work with `skipCertutil: true` on Linux.
|
175 | },
|
176 | async closeFirefoxBeforeContinuing() {
|
177 | // Invoked when devcert detects that Firefox is running while it is trying
|
178 | // to programmatically install it's certificate authority in the Firefox
|
179 | // trust store. Firefox appears to overwrite changes to the trust store on
|
180 | // exit, so Firefox must be closed before devcert can continue. devcert will
|
181 | // wait for Firefox to exit - this is just to prompt the user that they
|
182 | // need to close the application.
|
183 | },
|
184 | async startFirefoxWizard(certificateHost: string) {
|
185 | // Invoked when devcert detects a Firefox installation and `skipCertutil:
|
186 | // true` was specified. This is invoked right before devcert launches the
|
187 | // Firefox certificate import wizard GUI. Used to give the user a heads up
|
188 | // as to why they are about to see Firefox pop up.
|
189 | //
|
190 | // The certificateHost provided is the URL for the temporary server that
|
191 | // devcert has spun up in order to trigger the wizard(Firefox needs try to
|
192 | // "download" the cert to trigger the wizard). This URL will load the page
|
193 | // supplied in the `firefoxWizardPromptPage()` method below.
|
194 | //
|
195 | // Normally, devcert will automatically open this URL, but in case it fails
|
196 | // you may want to print it out to the console with an explanatory message
|
197 | // so the user isn't left hanging wondering what's happening.
|
198 | },
|
199 | async firefoxWizardPromptPage(certificateURL: string): Promise<string> {
|
200 | // When devcert starts the Firefox certificate installation wizard GUI, it
|
201 | // first loads an HTML page in Firefox. The template used for that page is
|
202 | // the return value of this method. The supplied certificateURL is the path
|
203 | // to the actual certificate. The Firefox tab must attempt to load this URL
|
204 | // to trigger the wizard.
|
205 | //
|
206 | // The default implemenation is a simple redirect to that URL. But you could
|
207 | // supply your own branded template here, with a button that says "Install
|
208 | // certificate" that is linked to the certificateURL, along with a more in
|
209 | // depth explanation of what is happening for example.
|
210 | }
|
211 | async waitForFirefoxWizard() {
|
212 | // Invoked _after_ the Firefox certificate import wizard is kicked off. This
|
213 | // method should not resolve until the user indicates that the wizard is
|
214 | // complete (unfortunately, we have no way of determining that
|
215 | // programmatically)
|
216 | }
|
217 | }
|
218 | ```
|
219 |
|
220 | You can supply any or all of these methods - ones you do not supply will fall
|
221 | back to the default implemenation.
|
222 |
|
223 | ## Testing
|
224 |
|
225 | Testing a tool like devcert can be a pain. I haven't found a good automated
|
226 | solution for cross platform GUI testing (the GUI part is necessary to test
|
227 | each browser's handling of devcert certificates, as well as test the Firefox
|
228 | wizard flow).
|
229 |
|
230 | To make things easier, devcert comes with a series of virtual machine images. Each one is a snapshot taken right before running a test - just launch the machine and hit <Enter>.
|
231 |
|
232 | You can also use the snapshotted state of the VMs to roll them back to a
|
233 | pristine state for another round of testing.
|
234 |
|
235 | > **Note**: Be aware that the macOS license terms prohibit running it on
|
236 | > non-Apple hardware, so you must own a Mac to test that platform. If you don't
|
237 | > own a Mac - that's okay, just mention in the PR that you were unable to test
|
238 | > on a Mac and we're happy to test it for you.
|
239 |
|
240 | ### Virtual Machine Snapshots
|
241 |
|
242 | * [macOS](https://s3-us-west-1.amazonaws.com/devcert-test-snapshots/macOS.pvm.zip)
|
243 | * [Windows](https://s3-us-west-1.amazonaws.com/devcert-test-snapshots/MSEdge+-+Win10.zip)
|
244 | * [Ubuntu](https://s3-us-west-1.amazonaws.com/devcert-test-snapshots/Ubuntu+Linux.zip)
|
245 |
|
246 | ## License
|
247 |
|
248 | MIT © [Dave Wasmer](http://davewasmer.com)
|