├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── README.md
├── Vagrantfile
├── openssl-configurations
├── certificate-authority-self-signing.conf
├── domain-certificate-signing-requests.conf
└── domain-certificates.conf
├── package-lock.json
├── package.json
├── src
├── certificate-authority.ts
├── certificates.ts
├── constants.ts
├── index.ts
├── platforms
│ ├── darwin.ts
│ ├── index.ts
│ ├── linux.ts
│ ├── shared.ts
│ └── win32.ts
├── types.d.ts
├── user-interface.ts
└── utils.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | # [1.1.2](https://github.com/davewasmer/devcert/releases/tag/v1.1.2)
7 |
8 | ### Bug Fixes
9 |
10 | #### [#56](https://github.com/davewasmer/devcert/issue/56): localhost is not a valid domain name
11 |
12 | Regular expression fixed in [#57](https://github.com/davewasmer/devcert/issue/57).
13 |
14 |
15 | # [1.1.1](https://github.com/davewasmer/devcert/releases/tag/v1.1.1)
16 |
17 | ### Bug Fixes
18 |
19 | #### [#55](https://github.com/davewasmer/devcert/pull/55): Fix remote execution vulnerability by switching from execSync to execFileSync
20 |
21 | - Change `run()` to use `execFileSync`
22 | - Refactor codebase to use new signature of `run()`
23 | - Add an extra sanitizing step: test arguments passed to `certificateFor` with a (fairly permissive) regular expression limiting them to legal domain name chars
24 |
25 | ### ⚠️ This is a mandatory update! ⚠️
26 |
27 | This release fixes a security vulnerability in previous versions. Previous versions will be deprecated.
28 |
29 |
30 |
31 | # [1.1.0](https://github.com/davewasmer/devcert/releases/tag/v1.1.0)
32 |
33 | ### Features
34 |
35 | #### [#41](https://github.com/davewasmer/devcert/pull/41): Return CA certificate path/data
36 |
37 | - Make the CA certificate available to userland, but keep the key locked protected or encrypted
38 | - Add options `getCaPath` and `getCaBuffer`
39 | - [#48](https://github.com/davewasmer/devcert/pull/48): Enhance uninstallation and upgrade routines to revoke old certs and delete old files
40 |
41 | ### Bug Fixes
42 |
43 | * [#37](https://github.com/davewasmer/devcert/pull/37): Append to win32 hostfile, don't overwrite it
44 | * [#42](https://github.com/davewasmer/devcert/pull/42): Reorder SAN declarations to fix a bug in win32 Firefox
45 | * [#43](https://github.com/davewasmer/devcert/pull/43): Fix unquote paths in shell commands
46 | * [#45](https://github.com/davewasmer/devcert/pull/45): Set generated certificate to last 825 days, a limit imposed by OSX Catalina
47 |
48 | ### Chores
49 |
50 | * [#44](https://github.com/davewasmer/devcert/pull/44): Bump lodash from 4.17.4 to 4.17.13
51 | * [#46](https://github.com/davewasmer/devcert/pull/46): Bump handlebars from 4.0.6 to 4.5.3
52 | * [#47](https://github.com/davewasmer/devcert/pull/47): Bump lodash.template from 4.4.0 to 4.5.0
53 |
54 |
55 |
56 | # [1.0.2](https://github.com/davewasmer/devcert/releases/tag/v1.0.2)
57 |
58 | ### Bug Fixes
59 |
60 | * #20: Update `command-exists` dependency
61 | * #23: Fix issues related to Firefox on Windows and redirecting
62 | * #24: Update generated certificate to last 7000 days instead of 30
63 | * 30: Fix false positive on `nss` check
64 |
65 |
66 |
67 | # [1.0.0](https://github.com/davewasmer/devcert/compare/v0.3.2...v1.0.0) (2018-04-05)
68 |
69 | ### Features
70 | * refactor to use encrypted/secure root authority credentials to avoid exposing them to malicious userland processes
71 |
72 |
73 |
74 | ## [0.3.2](https://github.com/davewasmer/devcert/compare/v0.3.1...v0.3.2) (2017-04-28)
75 |
76 |
77 | ### Bug Fixes
78 |
79 | * add -d flag to security command, not sure why it ignores -p otherwise, but oh well ([842404f](https://github.com/davewasmer/devcert/commit/842404f))
80 |
81 |
82 |
83 |
84 | ## [0.3.1](https://github.com/davewasmer/devcert/compare/v0.3.0...v0.3.1) (2017-04-28)
85 |
86 |
87 | ### Bug Fixes
88 |
89 | * wrap NSS db dir paths with quotes ([69be0f7](https://github.com/davewasmer/devcert/commit/69be0f7))
90 |
91 |
92 |
93 |
94 | # [0.3.0](https://github.com/davewasmer/devcert/compare/v0.2.20...v0.3.0) (2017-04-28)
95 |
96 |
97 | ### Bug Fixes
98 |
99 | * fix waitForUser async usage ([9fd27c5](https://github.com/davewasmer/devcert/commit/9fd27c5))
100 |
101 |
102 | ### Features
103 |
104 | * add root CA setup versioning ([6c80805](https://github.com/davewasmer/devcert/commit/6c80805))
105 |
106 |
107 |
108 |
109 | ## [0.2.20](https://github.com/davewasmer/devcert/compare/v0.2.19...v0.2.20) (2017-04-28)
110 |
111 |
112 | ### Bug Fixes
113 |
114 | * eol import ([ff198f0](https://github.com/davewasmer/devcert/commit/ff198f0))
115 |
116 |
117 |
118 |
119 | ## [0.2.19](https://github.com/davewasmer/devcert/compare/v0.2.18...v0.2.19) (2017-04-28)
120 |
121 |
122 | ### Bug Fixes
123 |
124 | * warn user to quit firefox before root install ([8bb0271](https://github.com/davewasmer/devcert/commit/8bb0271))
125 |
126 |
127 |
128 |
129 | ## [0.2.18](https://github.com/davewasmer/devcert/compare/v0.2.17...v0.2.18) (2017-04-27)
130 |
131 |
132 | ### Bug Fixes
133 |
134 | * add required nickname arg to certutil command ([5bc9874](https://github.com/davewasmer/devcert/commit/5bc9874))
135 |
136 |
137 |
138 |
139 | ## [0.2.17](https://github.com/davewasmer/devcert/compare/v0.2.16...v0.2.17) (2017-04-27)
140 |
141 |
142 | ### Bug Fixes
143 |
144 | * trim newlines from discovered certutil path ([f45195e](https://github.com/davewasmer/devcert/commit/f45195e))
145 |
146 |
147 |
148 |
149 | ## [0.2.16](https://github.com/davewasmer/devcert/compare/v0.2.15...v0.2.16) (2017-04-27)
150 |
151 |
152 | ### Bug Fixes
153 |
154 | * do not use ~ for home dir, use $HOME instead ([faf1518](https://github.com/davewasmer/devcert/commit/faf1518))
155 |
156 |
157 |
158 |
159 | ## [0.2.15](https://github.com/davewasmer/devcert/compare/v0.2.14...v0.2.15) (2017-04-27)
160 |
161 |
162 |
163 |
164 | ## [0.2.14](https://github.com/davewasmer/devcert/compare/v0.2.13...v0.2.14) (2017-04-27)
165 |
166 |
167 |
168 |
169 | ## [0.2.13](https://github.com/davewasmer/devcert/compare/v0.2.12...v0.2.13) (2017-04-27)
170 |
171 |
172 | ### Bug Fixes
173 |
174 | * fix installCertutil handling ([1a571e1](https://github.com/davewasmer/devcert/commit/1a571e1))
175 | * silence openssl output ([f66f558](https://github.com/davewasmer/devcert/commit/f66f558))
176 |
177 |
178 |
179 |
180 | ## [0.2.12](https://github.com/davewasmer/devcert/compare/v0.2.11...v0.2.12) (2017-04-27)
181 |
182 |
183 |
184 |
185 | ## [0.2.11](https://github.com/davewasmer/devcert/compare/v0.2.10...v0.2.11) (2017-04-27)
186 |
187 |
188 | ### Bug Fixes
189 |
190 | * add eol conversion for openssl.conf on windows ([f854a0e](https://github.com/davewasmer/devcert/commit/f854a0e))
191 | * escape backslashes in conf template paths ([2354eb0](https://github.com/davewasmer/devcert/commit/2354eb0))
192 |
193 |
194 |
195 |
196 | ## [0.2.10](https://github.com/davewasmer/devcert/compare/v0.2.9...v0.2.10) (2017-04-04)
197 |
198 |
199 | ### Bug Fixes
200 |
201 | * use double quotes to avoid escaping issues on windows ([08f4362](https://github.com/davewasmer/devcert/commit/08f4362))
202 |
203 |
204 |
205 |
206 | ## [0.2.9](https://github.com/davewasmer/devcert/compare/v0.2.8...v0.2.9) (2017-04-04)
207 |
208 |
209 | ### Bug Fixes
210 |
211 | * don't hardcode path separators in conf template ([b7db54a](https://github.com/davewasmer/devcert/commit/b7db54a))
212 | * fix quote marks -> template string ([32f24f7](https://github.com/davewasmer/devcert/commit/32f24f7))
213 |
214 |
215 |
216 |
217 | ## [0.2.8](https://github.com/davewasmer/devcert/compare/v0.2.7...v0.2.8) (2017-03-31)
218 |
219 |
220 | ### Bug Fixes
221 |
222 | * add -batch flag to avoid prompting ([5ba2424](https://github.com/davewasmer/devcert/commit/5ba2424))
223 | * add root ca cert to /etc/ssl/certs on linux ([5dc37a4](https://github.com/davewasmer/devcert/commit/5dc37a4))
224 |
225 |
226 |
227 |
228 | ## [0.2.7](https://github.com/davewasmer/devcert/compare/v0.2.6...v0.2.7) (2017-03-31)
229 |
230 |
231 | ### Bug Fixes
232 |
233 | * do not block with execSync when launching firefox, template openssl conf to get config paths ([2600a89](https://github.com/davewasmer/devcert/commit/2600a89))
234 |
235 |
236 |
237 |
238 | ## [0.2.6](https://github.com/davewasmer/devcert/compare/v0.2.5...v0.2.6) (2017-03-31)
239 |
240 |
241 | ### Bug Fixes
242 |
243 | * separate commands so each gets sudo, improve debug output ([af40aca](https://github.com/davewasmer/devcert/commit/af40aca))
244 |
245 |
246 |
247 |
248 | ## [0.2.5](https://github.com/davewasmer/devcert/compare/v0.2.4...v0.2.5) (2017-03-31)
249 |
250 |
251 |
252 |
253 | ## [0.2.4](https://github.com/davewasmer/devcert/compare/v0.2.3...v0.2.4) (2017-03-30)
254 |
255 |
256 | ### Bug Fixes
257 |
258 | * fix root key path when generating root cert ([83c8672](https://github.com/davewasmer/devcert/commit/83c8672))
259 |
260 |
261 |
262 |
263 | ## [0.2.3](https://github.com/davewasmer/devcert/compare/v0.2.2...v0.2.3) (2017-03-30)
264 |
265 |
266 | ### Bug Fixes
267 |
268 | * make the config dir first ([fab033a](https://github.com/davewasmer/devcert/commit/fab033a))
269 |
270 |
271 |
272 |
273 | ## [0.2.2](https://github.com/davewasmer/devcert/compare/v0.2.1...v0.2.2) (2017-03-30)
274 |
275 |
276 | ### Bug Fixes
277 |
278 | * fix configDir for non-windows ([7457cde](https://github.com/davewasmer/devcert/commit/7457cde))
279 |
280 |
281 |
282 |
283 | ## [0.2.1](https://github.com/davewasmer/devcert/compare/v0.2.0...v0.2.1) (2017-03-30)
284 |
285 |
286 | ### Bug Fixes
287 |
288 | * don't ignore dist when publishing ([eef1738](https://github.com/davewasmer/devcert/commit/eef1738))
289 |
290 |
291 |
292 |
293 | # [0.2.0](https://github.com/davewasmer/devcert/compare/v0.1.0...v0.2.0) (2017-03-30)
294 |
295 |
296 | ### Features
297 |
298 | * improve Readme, return node.createServer compatible object, improve error messaging ([b760220](https://github.com/davewasmer/devcert/commit/b760220))
299 |
300 |
301 |
302 |
303 | # 0.1.0 (2017-03-29)
304 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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 | When it installs or upgrades, devcert creates a self-signed certificate
24 | authority (CA) which it uses to sign all certificates it creates. It will try
25 | to register this CA with OS keychains in OSX, Linux, and Windows. However,
26 | some HTTP clients (such as Firefox and NodeJS itself) use their own trusted
27 | certificate list instead of the operating system's keychain. The `getCaPath`
28 | and `getCaBuffer` options make the CA available in the `certificateFor()`
29 | return object itself, so that these programs can choose whether to trust it.
30 |
31 | ### getCaPath
32 |
33 | Set this option to `true` and the returned object will inlude a `caPath`
34 | property, set to the file path of the certificate authority file. Use this
35 | path to add the certificate to local trust stores which accept paths as
36 | arguments, such as NodeJS's builtin environment variable
37 | `NODE_EXTRA_CA_CERTS`..
38 |
39 | ### getCaBuffer
40 |
41 | Set this option to `true` and the returned object will inlude a `ca`
42 | property, set to the UTF-8-encoded contents of the certificate authority
43 | file. Use this path to add the certificate to local trust stores which don't
44 | use OS settings, lke the examples mentioned above.
45 |
46 | ### skipHostsFile
47 |
48 | If you supply a custom domain name (i.e. any domain other than `localhost`)
49 | when requesting a certificate from devcert, it will attempt to modify your
50 | system to redirect requests for that domain to your local machine (rather
51 | than to the real domain). It does this by modifying your `/etc/hosts` file.
52 |
53 | If you pass in the `skipHostsFile` option, devcert will skip this step. This
54 | means that if you ask for certificates for `my-app.test` (for example), and
55 | don't have some other DNS redirect method in place, that you won't be able to
56 | access your app at `https://my-app.test` because your computer wouldn't know
57 | that `my-app.test` should resolve your local machine.
58 |
59 | Keep in mind that SSL certificates are issued for _domains_, so if you ask
60 | for a certificate for `my-app.test`, and don't have any kind of DNS redirect
61 | in place (`/etc/hosts` or otherwise), trying to hit `localhost` won't work,
62 | even if the app you intended to serve via `my-app.test` is running on your
63 | local machine (since the SSL certificate won't say `localhost`).
64 |
65 | ### skipCertutil
66 |
67 | This option will tell devcert to avoid installing `certutil` tooling.
68 |
69 | `certutil` is a tooling package used to automated the installation of SSL
70 | certificates in certain circumstances; specifically, Firefox (for every OS)
71 | and Chrome (on Linux only).
72 |
73 | Normally, devcert will attempt to install `certutil` if it's need and not
74 | already present on your system. If don't want devcert to install this
75 | package, pass `skipCertutil: true`.
76 |
77 | If you decide to `skipCertutil`, the initial setup process for devcert
78 | changes in these two scenarios:
79 |
80 | * **Firefox on all platforms**: Thankully, Firefox makes this easy. There's a
81 | point-and-click wizard for importing and trusting a certificate, so if you
82 | specify `skipCertutil: true`, devcert will instead automatically open Firefox
83 | and kick off this wizard for you. Simply follow the prompts to trust the
84 | certificate. **Reminder: you'll only need to do this once per machine**
85 |
86 | * **Chrome on Linux**: Unfortunately, it appears that the **only** way to get
87 | Chrome to trust an SSL certificate on Linux is via the `certutil` tooling -
88 | there is no manual process for it. Thus, if you are using Chrome on Linux, do
89 | **not** supply `skipCertuil: true`. If you do, devcert certificates will not
90 | be trusted by Chrome.
91 |
92 | The `certutil` tooling is installed in OS-specific ways:
93 |
94 | * Mac: `brew install nss`
95 | * Linux: `apt install libnss3-tools`
96 | * Windows: N/A (there is no easy, hands-off way to install certutil on Windows,
97 | so devcert will simply fallback to the wizard approach for Firefox outlined
98 | above)
99 |
100 | ## Multiple domains (SAN)
101 | If you are developing a multi-tenant app or have many apps locally, you can generate a security
102 | certificate using `devcert` to also use the [Subject Alternative Name](https://en.wikipedia.org/wiki/Subject_Alternative_Name)
103 | extension, just pass an array of domains instead.
104 |
105 | ```js
106 | let ssl = await devcert.certificateFor([
107 | 'localhost',
108 | 'local.api.example.com',
109 | 'local.example.com',
110 | 'local.auth.example.com'
111 | ]);
112 | https.createServer(ssl, app).listen(3000);
113 | ```
114 |
115 | ## Docker and local development
116 | If you are developing with Docker, one option is to install `devcert` into a base folder in your home directory and
117 | generate certificates for all of your local Docker projects. See comments and caveats in [this issue](https://github.com/davewasmer/devcert/issues/17).
118 |
119 | While not elegant, you only really need to do this as often as you add new domains locally, which is probably not very often.
120 |
121 | The general script would look something like:
122 |
123 | ```js
124 | // example: make a directory in home directory such as ~/devcert-util
125 | // ~/devcert-util/generate.js
126 | const fs = require('fs');
127 | const devcert = require('devcert');
128 |
129 | // or if its just one domain - devcert.certificateFor('local.example.com')
130 | devcert.certificateFor([
131 | 'localhost',
132 | 'local.api.example.com',
133 | 'local.example.com',
134 | 'local.auth.example.com'
135 | ])
136 | .then(({key, cert}) => {
137 | fs.writeFileSync('./certs/tls.key', key);
138 | fs.writeFileSync('./certs/tls.cert', cert);
139 | })
140 | .catch(console.error);
141 | ```
142 |
143 | An easy way to use the files generated from above script is to copy the `~/devcert-util/certs` folder into your Docker projects:
144 | ```
145 | # local-docker-project-root/
146 | 🗀 certs/
147 | 🗎 tls.key
148 | 🗎 tls.cert
149 | ```
150 |
151 | And add this line to your `.gitignore`:
152 | ```
153 | certs/
154 | ```
155 |
156 | These two files can now easily be used by any project, be it Node.js or something else.
157 |
158 | In Node, within Docker, simply load the copied certificate files into your https server:
159 | ```js
160 | const fs = require('fs');
161 | const Express = require('express');
162 | const app = new Express();
163 | https
164 | .createServer({
165 | key: fs.readFileSync('./certs/tls.key'),
166 | cert: fs.readFileSync('./certs/tls.cert')
167 | }, app)
168 | .listen(3000);
169 | ```
170 |
171 | Also works with webpack dev server or similar technologies:
172 | ```js
173 | // webpack.config.js
174 | const fs = require('fs');
175 |
176 | module.exports = {
177 | //...
178 | devServer: {
179 | contentBase: join(__dirname, 'dist'),
180 | host: '0.0.0.0',
181 | public: 'local.api.example.com',
182 | port: 3000,
183 | publicPath: '/',
184 | https: {
185 | key: fs.readFileSync('./certs/tls.key'),
186 | cert: fs.readFileSync('./certs/tls.cert')
187 | }
188 | }
189 | };
190 | ```
191 |
192 | ## How it works
193 |
194 | When you ask for a development certificate, devcert will first check to see
195 | if it has run on this machine before. If not, it will create a root
196 | certificate authority and add it to your OS and various browser trust stores.
197 | You'll likely see password prompts from your OS at this point to authorize
198 | the new root CA.
199 |
200 | Since your machine now trusts this root CA, it will trust any certificates
201 | signed by it. So when you ask for a certificate for a new domain, devcert
202 | will use the root CA credentials to generate a certificate specific to the
203 | domain you requested, and returns the new certificate to you.
204 |
205 | If you request a domain that has already had certificates generated for it,
206 | devcert will simply return the cached certificates.
207 |
208 | This setup ensures that browsers won't show scary warnings about untrusted
209 | certificates, since your OS and browsers will now trust devcert's
210 | certificates.
211 |
212 | ## Security Concerns
213 |
214 | There's a reason that your OS prompts you for your root password when devcert
215 | attempts to install it's root certificate authority. By adding it to your
216 | machine's trust stores, your browsers will automatically trust _any_ certificate
217 | generated with it.
218 |
219 | This exposes a potential attack vector on your local machine: if someone else
220 | could use the devcert certificate authority to generate certificates, and if
221 | they could intercept / manipulate your network traffic, they could theoretically
222 | impersonate some websites, and your browser would not show any warnings (because
223 | it trusts the devcert authority).
224 |
225 | To prevent this, devcert takes steps to ensure that no one can access the
226 | devcert certificate authority credentials to generate malicious certificates
227 | without you knowing. The exact approach varies by platform:
228 |
229 | * **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
230 | `chmod 600 ca-cert.crt`). When devcert itself needs these, it shells out to
231 | `sudo` invocations to read / write the credentials.
232 | * **Windows**: because of my unfamiliarity with Windows file permissions, I
233 | wasn't confident I would be able to correctly set permissions to mimic the setup
234 | on macOS and Linux. So instead, devcert will prompt you for a password, and then
235 | use that to encrypt the credentials with an AES256 cipher. The password is never
236 | written to disk.
237 |
238 | To further protect these credentials, any time they are written to disk, they
239 | are written to temporary files, and are immediately deleted after they are no longer needed.
240 |
241 | Additionally, the root CA certificate is unique to your machine only: it's
242 | generated on-the-fly when it is first installed. ensuring there are no
243 | central / shared keys to crack across machines.
244 |
245 | ### Why install a root certificate authority at all?
246 |
247 | The root certificate authority makes it simpler to manage which domains are
248 | configured for SSL by devcert. The alternative is to generate and trust
249 | self-signed certificates for each domain. The problem is that while devcert
250 | is able to add a certificate to your machine's trust stores, the tooling to
251 | remove a certificate doesn't cover every case. So if you ever wanted to
252 | _untrust_ devcert's certificates, you'd have to manually remove each one from
253 | each trust store.
254 |
255 | By trusting only a single root CA, devcert is able to guarantee that when you
256 | want to _disable_ SSL for a domain, it can do so with no manual intervention
257 | - we just delete the domain-specific certificate files. Since these
258 | domain-specific files aren't installed in your trust stores, once they are
259 | gone, they are gone.
260 |
261 |
262 | ## Integration
263 |
264 | devcert has been designed from day one to work as low-level library that other
265 | tools can delegate to. The goal is to make HTTPS development easy for everyone,
266 | regardless of framework or library choice.
267 |
268 | With that in mind, if you'd like to use devcert in your library/framework/CLI,
269 | devcert makes that easy.
270 |
271 | In addition to the options above, devcert exposes a `ui` option. This option
272 | allows you to control all the points where devcert requries user interaction,
273 | substituting your own prompts and user interface. You can use this to brand
274 | the experience with your own tool's name, localize the messages, or integrate
275 | devcert into a larger existing workflow.
276 |
277 | The `ui` option should be an object with the following methods:
278 |
279 | ```ts
280 | {
281 | async getWindowsEncryptionPassword(): Promise {
282 | // Invoked when devcert needs the password used to encrypt the root
283 | // certificate authority credentials on Windows. May be invoked multiple
284 | // times if the user's supplied password is incorrect
285 | },
286 | async warnChromeOnLinuxWithoutCertutil(): Promise {
287 | // Invoked when devcert is run on Linux, detects that Chrome is installed,
288 | // and the `skipCertutil` option is `true`. Used to warn the user that
289 | // Chrome will not work with `skipCertutil: true` on Linux.
290 | },
291 | async closeFirefoxBeforeContinuing() {
292 | // Invoked when devcert detects that Firefox is running while it is trying
293 | // to programmatically install it's certificate authority in the Firefox
294 | // trust store. Firefox appears to overwrite changes to the trust store on
295 | // exit, so Firefox must be closed before devcert can continue. devcert will
296 | // wait for Firefox to exit - this is just to prompt the user that they
297 | // need to close the application.
298 | },
299 | async startFirefoxWizard(certificateHost: string) {
300 | // Invoked when devcert detects a Firefox installation and `skipCertutil:
301 | // true` was specified. This is invoked right before devcert launches the
302 | // Firefox certificate import wizard GUI. Used to give the user a heads up
303 | // as to why they are about to see Firefox pop up.
304 | //
305 | // The certificateHost provided is the URL for the temporary server that
306 | // devcert has spun up in order to trigger the wizard(Firefox needs try to
307 | // "download" the cert to trigger the wizard). This URL will load the page
308 | // supplied in the `firefoxWizardPromptPage()` method below.
309 | //
310 | // Normally, devcert will automatically open this URL, but in case it fails
311 | // you may want to print it out to the console with an explanatory message
312 | // so the user isn't left hanging wondering what's happening.
313 | },
314 | async firefoxWizardPromptPage(certificateURL: string): Promise {
315 | // When devcert starts the Firefox certificate installation wizard GUI, it
316 | // first loads an HTML page in Firefox. The template used for that page is
317 | // the return value of this method. The supplied certificateURL is the path
318 | // to the actual certificate. The Firefox tab must attempt to load this URL
319 | // to trigger the wizard.
320 | //
321 | // The default implemenation is a simple redirect to that URL. But you could
322 | // supply your own branded template here, with a button that says "Install
323 | // certificate" that is linked to the certificateURL, along with a more in
324 | // depth explanation of what is happening for example.
325 | }
326 | async waitForFirefoxWizard() {
327 | // Invoked _after_ the Firefox certificate import wizard is kicked off. This
328 | // method should not resolve until the user indicates that the wizard is
329 | // complete (unfortunately, we have no way of determining that
330 | // programmatically)
331 | }
332 | }
333 | ```
334 |
335 | You can supply any or all of these methods - ones you do not supply will fall
336 | back to the default implemenation.
337 |
338 | ## Testing
339 |
340 | Testing a tool like devcert can be a pain. I haven't found a good automated
341 | solution for cross platform GUI testing (the GUI part is necessary to test
342 | each browser's handling of devcert certificates, as well as test the Firefox
343 | wizard flow).
344 |
345 | 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 .
346 |
347 | You can also use the snapshotted state of the VMs to roll them back to a
348 | pristine state for another round of testing.
349 |
350 | > **Note**: Be aware that the macOS license terms prohibit running it on
351 | > non-Apple hardware, so you must own a Mac to test that platform. If you don't
352 | > own a Mac - that's okay, just mention in the PR that you were unable to test
353 | > on a Mac and we're happy to test it for you.
354 |
355 | ### Virtual Machine Snapshots
356 |
357 | * [macOS](https://s3-us-west-1.amazonaws.com/devcert-test-snapshots/macOS.pvm.zip)
358 | * [Windows](https://s3-us-west-1.amazonaws.com/devcert-test-snapshots/MSEdge+-+Win10.zip)
359 | * [Ubuntu](https://s3-us-west-1.amazonaws.com/devcert-test-snapshots/Ubuntu+Linux.zip)
360 |
361 | ## License
362 |
363 | MIT © [Dave Wasmer](http://davewasmer.com)
364 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | Vagrant.configure("2") do |config|
2 |
3 | config.vm.define "mac" do |mac|
4 | config.vm.box = "devcert/macos"
5 | config.vm.network "public_network"
6 |
7 | config.vm.define "linux" do |linux|
8 | config.vm.box = "devcert/linux"
9 | config.vm.network "public_network"
10 |
11 | config.vm.define "windows" do |windows|
12 | config.vm.box = "devcert/windows"
13 | config.vm.network "public_network"
14 |
15 | config.vm.provider "virtualbox" do |vb|
16 | # Display the VirtualBox GUI when booting the machine
17 | vb.gui = true
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/openssl-configurations/certificate-authority-self-signing.conf:
--------------------------------------------------------------------------------
1 | [ req ]
2 | # Which algorithm to use
3 | default_md = sha256
4 | # Don't prompt the TTY for input, just use the config file values
5 | prompt = no
6 | # Interpret strings as utf8, not ASCII
7 | utf8 = yes
8 | # This specifies the section containing the distinguished name fields to
9 | # prompt for when generating a certificate request.
10 | distinguished_name = req_distinguished_name
11 | # This specifies the configuration file section containing a list of extensions
12 | # to add to the certificate request.
13 | x509_extensions = v3_ca
14 | # How long is the CA valid for
15 | default_days = 7000
16 |
17 | [ req_distinguished_name ]
18 | CN = devcert
19 |
20 | [ v3_ca ]
21 | subjectKeyIdentifier = hash
22 | authorityKeyIdentifier = keyid:always,issuer
23 | # Mark our CA as a CA, and only allow it to issue server certificates - no intermediate certificates allowed
24 | basicConstraints = critical, CA:true, pathlen:0
25 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign
26 |
--------------------------------------------------------------------------------
/openssl-configurations/domain-certificate-signing-requests.conf:
--------------------------------------------------------------------------------
1 | [ req ]
2 | # Which algorithm to use
3 | default_md = sha256
4 | # Don't prompt the TTY for input, just use the config file values
5 | prompt = no
6 | # Interpret strings as utf8, not ASCII
7 | utf8 = yes
8 | # This specifies the section containing the distinguished name fields to
9 | # prompt for when generating a certificate request.
10 | distinguished_name = req_distinguished_name
11 | # This specifies the configuration file section containing a list of extensions
12 | # to add to the certificate request.
13 | req_extensions = req_extensions
14 |
15 | [ req_distinguished_name ]
16 | CN = <%= domain %>
17 |
18 | [ req_extensions ]
19 | basicConstraints = CA:FALSE
20 | subjectAltName = @subject_alt_names
21 | subjectKeyIdentifier = hash
22 |
23 | [ subject_alt_names ]
24 | <%= subjectAltNames %>
25 |
--------------------------------------------------------------------------------
/openssl-configurations/domain-certificates.conf:
--------------------------------------------------------------------------------
1 | [ ca ]
2 | default_ca = devcert_ca
3 |
4 | [ devcert_ca ]
5 | # Serial file that counts up unique IDs for each cert issued
6 | serial = <%= serialFile.replace(/\\/g, '\\\\') %>
7 | # Database file that tracks all issued certs
8 | database = <%= databaseFile.replace(/\\/g, '\\\\') %>
9 | # Where to put the new cert
10 | new_certs_dir = <%= domainDir.replace(/\\/g, '\\\\') %>
11 | # Which algorithm to use
12 | default_md = sha256
13 | # Don't prompt the TTY for input, just use the config file values
14 | prompt = no
15 | # Interpret strings as utf8, not ASCII
16 | utf8 = yes
17 | # This specifies the configuration file section containing a list of extensions
18 | # to add to the certificate request.
19 | req_extensions = req_extensions
20 | x509_extensions = domain_certificate_extensions
21 | # How long is the domain cert good for
22 | default_days = 7000
23 | # What do CSRs need to supply?
24 | policy = loose_policy
25 |
26 | [ loose_policy ]
27 | commonName = supplied
28 |
29 | [ domain_certificate_extensions ]
30 | basicConstraints = critical, CA:FALSE
31 | subjectKeyIdentifier = hash
32 | authorityKeyIdentifier = keyid,issuer:always
33 | keyUsage = critical, digitalSignature, keyEncipherment
34 | extendedKeyUsage = serverAuth
35 | subjectAltName = @subject_alt_names
36 |
37 | [ subject_alt_names ]
38 | <%= subjectAltNames %>
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "devcert",
3 | "version": "1.2.2",
4 | "description": "Generate trusted local SSL/TLS certificates for local SSL development",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "prepublishOnly": "npm run build",
10 | "test": "echo \"Ha.\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/davewasmer/devcert.git"
15 | },
16 | "keywords": [
17 | "ssl",
18 | "certificate",
19 | "openssl",
20 | "trust"
21 | ],
22 | "author": "Dave Wasmer",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/davewasmer/devcert/issues"
26 | },
27 | "homepage": "https://github.com/davewasmer/devcert#readme",
28 | "devDependencies": {
29 | "standard-version": "^9.5.0",
30 | "typescript": "4.3.2"
31 | },
32 | "dependencies": {
33 | "@types/configstore": "^2.1.1",
34 | "@types/debug": "^0.0.30",
35 | "@types/get-port": "^3.2.0",
36 | "@types/glob": "^5.0.34",
37 | "@types/lodash": "^4.14.92",
38 | "@types/mkdirp": "^0.5.2",
39 | "@types/node": "^8.5.7",
40 | "@types/rimraf": "^2.0.2",
41 | "@types/tmp": "^0.0.33",
42 | "application-config-path": "^0.1.0",
43 | "command-exists": "^1.2.4",
44 | "debug": "^3.1.0",
45 | "eol": "^0.9.1",
46 | "get-port": "^3.2.0",
47 | "glob": "^7.1.2",
48 | "is-valid-domain": "^0.1.6",
49 | "lodash": "^4.17.4",
50 | "mkdirp": "^0.5.1",
51 | "password-prompt": "^1.0.4",
52 | "rimraf": "^2.6.2",
53 | "sudo-prompt": "^8.2.0",
54 | "tmp": "^0.0.33",
55 | "tslib": "^1.10.0"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/certificate-authority.ts:
--------------------------------------------------------------------------------
1 | import {
2 | unlinkSync as rm,
3 | readFileSync as readFile,
4 | writeFileSync as writeFile
5 | } from 'fs';
6 | import createDebug from 'debug';
7 |
8 | import {
9 | domainsDir,
10 | rootCADir,
11 | ensureConfigDirs,
12 | getLegacyConfigDir,
13 | rootCAKeyPath,
14 | rootCACertPath,
15 | caSelfSignConfig,
16 | opensslSerialFilePath,
17 | opensslDatabaseFilePath,
18 | caVersionFile
19 | } from './constants';
20 | import currentPlatform from './platforms';
21 | import { openssl, mktmp } from './utils';
22 | import { generateKey } from './certificates';
23 | import { Options } from './index';
24 |
25 | const debug = createDebug('devcert:certificate-authority');
26 |
27 | /**
28 | * Install the once-per-machine trusted root CA. We'll use this CA to sign
29 | * per-app certs.
30 | */
31 | export default async function installCertificateAuthority(options: Options = {}): Promise {
32 | debug(`Uninstalling existing certificates, which will be void once any existing CA is gone`);
33 | uninstall();
34 | ensureConfigDirs();
35 |
36 | debug(`Making a temp working directory for files to copied in`);
37 | let rootKeyPath = mktmp();
38 |
39 | debug(`Generating the OpenSSL configuration needed to setup the certificate authority`);
40 | seedConfigFiles();
41 |
42 | debug(`Generating a private key`);
43 | generateKey(rootKeyPath);
44 |
45 | debug(`Generating a CA certificate`);
46 | openssl(['req', '-new', '-x509', '-config', caSelfSignConfig, '-key', rootKeyPath, '-out', rootCACertPath, '-days', '825']);
47 |
48 | debug('Saving certificate authority credentials');
49 | await saveCertificateAuthorityCredentials(rootKeyPath);
50 |
51 | debug(`Adding the root certificate authority to trust stores`);
52 | await currentPlatform.addToTrustStores(rootCACertPath, options);
53 | }
54 |
55 | /**
56 | * Initializes the files OpenSSL needs to sign certificates as a certificate
57 | * authority, as well as our CA setup version
58 | */
59 | function seedConfigFiles() {
60 | // This is v2 of the devcert certificate authority setup
61 | writeFile(caVersionFile, '2');
62 | // OpenSSL CA files
63 | writeFile(opensslDatabaseFilePath, '');
64 | writeFile(opensslSerialFilePath, '01');
65 | }
66 |
67 | export async function withCertificateAuthorityCredentials(cb: ({ caKeyPath, caCertPath }: { caKeyPath: string, caCertPath: string }) => Promise | void) {
68 | debug(`Retrieving devcert's certificate authority credentials`);
69 | let tmpCAKeyPath = mktmp();
70 | let caKey = await currentPlatform.readProtectedFile(rootCAKeyPath);
71 | writeFile(tmpCAKeyPath, caKey);
72 | await cb({ caKeyPath: tmpCAKeyPath, caCertPath: rootCACertPath });
73 | rm(tmpCAKeyPath);
74 | }
75 |
76 | async function saveCertificateAuthorityCredentials(keypath: string) {
77 | debug(`Saving devcert's certificate authority credentials`);
78 | let key = readFile(keypath, 'utf-8');
79 | await currentPlatform.writeProtectedFile(rootCAKeyPath, key);
80 | }
81 |
82 |
83 | function certErrors(): string {
84 | try {
85 | openssl(['x509', '-in', rootCACertPath, '-noout']);
86 | return '';
87 | } catch (e) {
88 | return e.toString();
89 | }
90 | }
91 |
92 | // This function helps to migrate from v1.0.x to >= v1.1.0.
93 | /**
94 | * Smoothly migrate the certificate storage from v1.0.x to >= v1.1.0.
95 | * In v1.1.0 there are new options for retrieving the CA cert directly,
96 | * to help third-party Node apps trust the root CA.
97 | *
98 | * If a v1.0.x cert already exists, then devcert has written it with
99 | * platform.writeProtectedFile(), so an unprivileged readFile cannot access it.
100 | * Pre-detect and remedy this; it should only happen once per installation.
101 | */
102 | export async function ensureCACertReadable(options: Options = {}): Promise {
103 | if (!certErrors()) {
104 | return;
105 | }
106 | /**
107 | * on windows, writeProtectedFile left the cert encrypted on *nix, the cert
108 | * has no read permissions either way, openssl will fail and that means we
109 | * have to fix it
110 | */
111 | try {
112 | const caFileContents = await currentPlatform.readProtectedFile(rootCACertPath);
113 | currentPlatform.deleteProtectedFiles(rootCACertPath);
114 | writeFile(rootCACertPath, caFileContents);
115 | } catch (e) {
116 | return installCertificateAuthority(options);
117 | }
118 |
119 | // double check that we have a live one
120 | const remainingErrors = certErrors();
121 | if (remainingErrors) {
122 | return installCertificateAuthority(options);
123 | }
124 | }
125 |
126 | /**
127 | * Remove as much of the devcert files and state as we can. This is necessary
128 | * when generating a new root certificate, and should be available to API
129 | * consumers as well.
130 | *
131 | * Not all of it will be removable. If certutil is not installed, we'll leave
132 | * Firefox alone. We try to remove files with maximum permissions, and if that
133 | * fails, we'll silently fail.
134 | *
135 | * It's also possible that the command to untrust will not work, and we'll
136 | * silently fail that as well; with no existing certificates anymore, the
137 | * security exposure there is minimal.
138 | */
139 | export function uninstall(): void {
140 | currentPlatform.removeFromTrustStores(rootCACertPath);
141 | currentPlatform.deleteProtectedFiles(domainsDir);
142 | currentPlatform.deleteProtectedFiles(rootCADir);
143 | currentPlatform.deleteProtectedFiles(getLegacyConfigDir());
144 | }
--------------------------------------------------------------------------------
/src/certificates.ts:
--------------------------------------------------------------------------------
1 | // import path from 'path';
2 | import createDebug from 'debug';
3 | import { sync as mkdirp } from 'mkdirp';
4 | import { chmodSync as chmod } from 'fs';
5 | import { openssl } from './utils';
6 | import { withCertificateAuthorityCredentials } from './certificate-authority';
7 | import {pathForDomain, getStableDomainPath, withDomainSigningRequestConfig, withDomainCertificateConfig} from './constants';
8 |
9 | const debug = createDebug('devcert:certificates');
10 |
11 | /**
12 | * Generate a domain certificate signed by the devcert root CA. Domain
13 | * certificates are cached in their own directories under
14 | * CONFIG_ROOT/domains/, and reused on subsequent requests. Because the
15 | * individual domain certificates are signed by the devcert root CA (which was
16 | * added to the OS/browser trust stores), they are trusted.
17 | */
18 | export default async function generateDomainCertificate(domains: string[]): Promise {
19 | const domainPath = getStableDomainPath(domains);
20 | mkdirp(pathForDomain(domainPath));
21 |
22 | debug(`Generating private key for ${domains}`);
23 | let domainKeyPath = pathForDomain(domainPath, 'private-key.key');
24 | generateKey(domainKeyPath);
25 |
26 | debug(`Generating certificate signing request for ${domains}`);
27 | let csrFile = pathForDomain(domainPath, `certificate-signing-request.csr`);
28 | withDomainSigningRequestConfig(domains, (configpath) => {
29 | openssl(['req', '-new', '-config', configpath, '-key', domainKeyPath, '-out', csrFile]);
30 | });
31 |
32 | debug(`Generating certificate for ${domains} from signing request and signing with root CA`);
33 | let domainCertPath = pathForDomain(domainPath, `certificate.crt`);
34 |
35 | await withCertificateAuthorityCredentials(({caKeyPath, caCertPath}) => {
36 | withDomainCertificateConfig(domains, (domainCertConfigPath) => {
37 | openssl(['ca', '-config', domainCertConfigPath, '-in', csrFile, '-out', domainCertPath, '-keyfile', caKeyPath, '-cert', caCertPath, '-days', '825', '-batch'])
38 | });
39 | });
40 | }
41 |
42 | // Generate a cryptographic key, used to sign certificates or certificate signing requests.
43 | export function generateKey(filename: string): void {
44 | debug(`generateKey: ${ filename }`);
45 | openssl(['genrsa', '-out', filename, '2048']);
46 | chmod(filename, 400);
47 | }
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { unlinkSync as rm, writeFileSync as writeFile, readFileSync as readFile } from 'fs';
3 | import { sync as mkdirp } from 'mkdirp';
4 | import { template as makeTemplate } from 'lodash';
5 | import applicationConfigPath = require('application-config-path');
6 | import eol from 'eol';
7 | import {mktmp, numericHash} from './utils';
8 |
9 | // Platform shortcuts
10 | export const isMac = process.platform === 'darwin';
11 | export const isLinux = process.platform === 'linux';
12 | export const isWindows = process.platform === 'win32';
13 |
14 | // Common paths
15 | export const configDir = applicationConfigPath('devcert');
16 | export const configPath: (...pathSegments: string[]) => string = path.join.bind(path, configDir);
17 |
18 | const getFilteredDomains = (domains: string[]) =>
19 | Array.from(
20 | domains
21 | .sort((a, b) => b.length - a.length)
22 | .reduce((filteredList, domain) =>
23 | Array.from(filteredList)
24 | .reduce((matches, item) => {
25 | if (item.indexOf(domain) > -1) {
26 | matches.add(domain);
27 | } else if (domain.indexOf(item) === -1 && item.indexOf(domain) === -1) {
28 | matches.add(item);
29 | matches.add(domain);
30 | } else {
31 | matches.add(item);
32 | }
33 |
34 | return matches;
35 | }, new Set()
36 | ), new Set([domains[0]])
37 | )
38 | ).sort();
39 |
40 | export const getStableDomainPath = (domains: string[]) =>
41 | domains.length === 1 ?
42 | domains[0] :
43 | 'san-' + numericHash(getFilteredDomains(domains).join(''));
44 | export const domainsDir = configPath('domains');
45 | export const pathForDomain: (domain: string, ...pathSegments: string[]) => string = path.join.bind(path, domainsDir)
46 |
47 | export const caVersionFile = configPath('devcert-ca-version');
48 | export const opensslSerialFilePath = configPath('certificate-authority', 'serial');
49 | export const opensslDatabaseFilePath = configPath('certificate-authority', 'index.txt');
50 | export const caSelfSignConfig = path.join(__dirname, '../openssl-configurations/certificate-authority-self-signing.conf');
51 |
52 | function generateSubjectAltNames(domains: string[]): string {
53 | return domains
54 | .reduce((dnsEntries, domain) =>
55 | dnsEntries.concat([
56 | `DNS.${dnsEntries.length + 1} = ${domain}`,
57 | `DNS.${dnsEntries.length + 2} = *.${domain}`,
58 | ]), [] as string[])
59 | .join("\r\n");
60 | }
61 |
62 | export function withDomainSigningRequestConfig(domains: string[], cb: (filepath: string) => void) {
63 | const domain = domains[0];
64 | const subjectAltNames = generateSubjectAltNames(domains);
65 | let tmpFile = mktmp();
66 | let source = readFile(path.join(__dirname, '../openssl-configurations/domain-certificate-signing-requests.conf'), 'utf-8');
67 | let template = makeTemplate(source);
68 | let result = template({domain, subjectAltNames});
69 | writeFile(tmpFile, eol.auto(result));
70 | cb(tmpFile);
71 | rm(tmpFile);
72 | }
73 |
74 | export function withDomainCertificateConfig(domains: string[], cb: (filepath: string) => void) {
75 | const domainPath = getStableDomainPath(domains);
76 | const subjectAltNames = generateSubjectAltNames(domains);
77 | let tmpFile = mktmp();
78 | let source = readFile(path.join(__dirname, '../openssl-configurations/domain-certificates.conf'), 'utf-8');
79 | let template = makeTemplate(source);
80 | let result = template({
81 | subjectAltNames,
82 | serialFile: opensslSerialFilePath,
83 | databaseFile: opensslDatabaseFilePath,
84 | domainDir: pathForDomain(domainPath)
85 | });
86 | writeFile(tmpFile, eol.auto(result));
87 | cb(tmpFile);
88 | rm(tmpFile);
89 | }
90 |
91 | // confTemplate = confTemplate.replace(/DATABASE_PATH/, configPath('index.txt').replace(/\\/g, '\\\\'));
92 | // confTemplate = confTemplate.replace(/SERIAL_PATH/, configPath('serial').replace(/\\/g, '\\\\'));
93 | // confTemplate = eol.auto(confTemplate);
94 |
95 | export const rootCADir = configPath('certificate-authority');
96 | export const rootCAKeyPath = configPath('certificate-authority', 'private-key.key');
97 | export const rootCACertPath = configPath('certificate-authority', 'certificate.cert');
98 |
99 |
100 |
101 | // Exposed for uninstallation purposes.
102 | export function getLegacyConfigDir(): string {
103 | if (isWindows && process.env.LOCALAPPDATA) {
104 | return path.join(process.env.LOCALAPPDATA, 'devcert', 'config');
105 | } else {
106 | let uid = process.getuid && process.getuid();
107 | let userHome = (isLinux && uid === 0) ? path.resolve('/usr/local/share') : require('os').homedir();
108 | return path.join(userHome, '.config', 'devcert');
109 | }
110 | }
111 |
112 | export function ensureConfigDirs() {
113 | mkdirp(configDir);
114 | mkdirp(domainsDir);
115 | mkdirp(rootCADir);
116 | }
117 |
118 | ensureConfigDirs();
119 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync as readFile, readdirSync as readdir, existsSync as exists } from 'fs';
2 | import createDebug from 'debug';
3 | import { sync as commandExists } from 'command-exists';
4 | import rimraf from 'rimraf';
5 | import {
6 | isMac,
7 | isLinux,
8 | isWindows,
9 | pathForDomain,
10 | getStableDomainPath,
11 | domainsDir,
12 | rootCAKeyPath,
13 | rootCACertPath,
14 | } from './constants';
15 | import currentPlatform from './platforms';
16 | import installCertificateAuthority, { ensureCACertReadable, uninstall } from './certificate-authority';
17 | import generateDomainCertificate from './certificates';
18 | import UI, { UserInterface } from './user-interface';
19 | import isValidDomain from 'is-valid-domain';
20 | export { uninstall };
21 |
22 | const debug = createDebug('devcert');
23 |
24 | export interface Options /* extends Partial */ {
25 | /** Return the CA certificate data? */
26 | getCaBuffer?: boolean;
27 | /** Return the path to the CA certificate? */
28 | getCaPath?: boolean;
29 | /** If `certutil` is not installed already (for updating nss databases; e.g. firefox), do not attempt to install it */
30 | skipCertutilInstall?: boolean,
31 | /** Do not update your systems host file with the domain name of the certificate */
32 | skipHostsFile?: boolean,
33 | /** User interface hooks */
34 | ui?: UserInterface
35 | }
36 |
37 | interface ICaBuffer {
38 | ca: Buffer;
39 | }
40 | interface ICaPath {
41 | caPath: string;
42 | }
43 | interface IDomainData {
44 | key: Buffer;
45 | cert: Buffer;
46 | }
47 | type IReturnCa = O['getCaBuffer'] extends true ? ICaBuffer : false;
48 | type IReturnCaPath = O['getCaPath'] extends true ? ICaPath : false;
49 | type IReturnData = (IDomainData) & (IReturnCa) & (IReturnCaPath);
50 |
51 | /**
52 | * Request an SSL certificate for the given app name signed by the devcert root
53 | * certificate authority. If devcert has previously generated a certificate for
54 | * that app name on this machine, it will reuse that certificate.
55 | *
56 | * If this is the first time devcert is being run on this machine, it will
57 | * generate and attempt to install a root certificate authority.
58 | *
59 | * Returns a promise that resolves with { key, cert }, where `key` and `cert`
60 | * are Buffers with the contents of the certificate private key and certificate
61 | * file, respectively
62 | *
63 | * If `options.getCaBuffer` is true, return value will include the ca certificate data
64 | * as { ca: Buffer }
65 | *
66 | * If `options.getCaPath` is true, return value will include the ca certificate path
67 | * as { caPath: string }
68 | */
69 | export async function certificateFor(requestedDomains: string | string[], options: O = {} as O): Promise> {
70 | const domains = Array.isArray(requestedDomains) ? requestedDomains : [requestedDomains];
71 | domains.forEach((domain) => {
72 | if (domain !== "localhost" && !isValidDomain(domain, { subdomain: true, wildcard: false, allowUnicode: true, topLevel: false })) {
73 | throw new Error(`"${domain}" is not a valid domain name.`);
74 | }
75 | });
76 |
77 | const domainPath = getStableDomainPath(domains);
78 | debug(`Certificate requested for ${domains}. Skipping certutil install: ${Boolean(options.skipCertutilInstall)}. Skipping hosts file: ${Boolean(options.skipHostsFile)}`);
79 |
80 | if (options.ui) {
81 | Object.assign(UI, options.ui);
82 | }
83 |
84 | if (!isMac && !isLinux && !isWindows) {
85 | throw new Error(`Platform not supported: "${process.platform}"`);
86 | }
87 |
88 | if (!commandExists('openssl')) {
89 | throw new Error('OpenSSL not found: OpenSSL is required to generate SSL certificates - make sure it is installed and available in your PATH');
90 | }
91 |
92 | let domainKeyPath = pathForDomain(domainPath, `private-key.key`);
93 | let domainCertPath = pathForDomain(domainPath, `certificate.crt`);
94 |
95 | if (!exists(rootCAKeyPath)) {
96 | debug('Root CA is not installed yet, so it must be our first run. Installing root CA ...');
97 | await installCertificateAuthority(options);
98 | } else if (options.getCaBuffer || options.getCaPath) {
99 | debug('Root CA is not readable, but it probably is because an earlier version of devcert locked it. Trying to fix...');
100 | await ensureCACertReadable(options);
101 | }
102 |
103 | if (!exists(pathForDomain(domainPath, `certificate.crt`))) {
104 | debug(`Can't find certificate file for ${domains}, so it must be the first request for ${domains}. Generating and caching ...`);
105 | await generateDomainCertificate(domains);
106 | }
107 |
108 | if (!options.skipHostsFile) {
109 | domains.forEach(async (domain) => {
110 | await currentPlatform.addDomainToHostFileIfMissing(domain);
111 | })
112 | }
113 |
114 | debug(`Returning domain certificate`);
115 |
116 | const ret = {
117 | key: readFile(domainKeyPath),
118 | cert: readFile(domainCertPath)
119 | } as IReturnData;
120 | if (options.getCaBuffer) (ret as unknown as ICaBuffer).ca = readFile(rootCACertPath);
121 | if (options.getCaPath) (ret as unknown as ICaPath).caPath = rootCACertPath;
122 |
123 | return ret;
124 | }
125 |
126 | export function hasCertificateFor(requestedDomains: string | string[]) {
127 | const domains = Array.isArray(requestedDomains) ? requestedDomains : [requestedDomains];
128 | const domainPath = getStableDomainPath(domains);
129 | return exists(pathForDomain(domainPath, `certificate.crt`));
130 | }
131 |
132 | export function configuredDomains() {
133 | return readdir(domainsDir);
134 | }
135 |
136 | export function removeDomain(requestedDomains: string | string[]) {
137 | const domains = Array.isArray(requestedDomains) ? requestedDomains : [requestedDomains];
138 | const domainPath = getStableDomainPath(domains);
139 | return rimraf.sync(pathForDomain(domainPath));
140 | }
141 |
--------------------------------------------------------------------------------
/src/platforms/darwin.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { writeFileSync as writeFile, existsSync as exists, readFileSync as read } from 'fs';
3 | import createDebug from 'debug';
4 | import { sync as commandExists } from 'command-exists';
5 | import { run, sudoAppend } from '../utils';
6 | import { Options } from '../index';
7 | import { addCertificateToNSSCertDB, assertNotTouchingFiles, openCertificateInFirefox, closeFirefox, removeCertificateFromNSSCertDB } from './shared';
8 | import { Platform } from '.';
9 |
10 | const debug = createDebug('devcert:platforms:macos');
11 |
12 | const getCertUtilPath = () => path.join(run('brew', ['--prefix', 'nss']).toString().trim(), 'bin', 'certutil');
13 |
14 | export default class MacOSPlatform implements Platform {
15 |
16 | private FIREFOX_BUNDLE_PATH = '/Applications/Firefox.app';
17 | private FIREFOX_BIN_PATH = path.join(this.FIREFOX_BUNDLE_PATH, 'Contents/MacOS/firefox');
18 | private FIREFOX_NSS_DIR = path.join(process.env.HOME, 'Library/Application Support/Firefox/Profiles/*');
19 |
20 | private HOST_FILE_PATH = '/etc/hosts';
21 |
22 | /**
23 | * macOS is pretty simple - just add the certificate to the system keychain,
24 | * and most applications will delegate to that for determining trusted
25 | * certificates. Firefox, of course, does it's own thing. We can try to
26 | * automatically install the cert with Firefox if we can use certutil via the
27 | * `nss` Homebrew package, otherwise we go manual with user-facing prompts.
28 | */
29 | async addToTrustStores(certificatePath: string, options: Options = {}): Promise {
30 |
31 | // Chrome, Safari, system utils
32 | debug('Adding devcert root CA to macOS system keychain');
33 | run('sudo', [
34 | 'security',
35 | 'add-trusted-cert',
36 | '-d',
37 | '-r',
38 | 'trustRoot',
39 | '-k',
40 | '/Library/Keychains/System.keychain',
41 | '-p',
42 | 'ssl',
43 | '-p',
44 | 'basic',
45 | certificatePath
46 | ]);
47 |
48 | if (this.isFirefoxInstalled()) {
49 | // Try to use certutil to install the cert automatically
50 | debug('Firefox install detected. Adding devcert root CA to Firefox trust store');
51 | if (!this.isNSSInstalled()) {
52 | if (!options.skipCertutilInstall) {
53 | if (commandExists('brew')) {
54 | debug(`certutil is not already installed, but Homebrew is detected. Trying to install certutil via Homebrew...`);
55 | try {
56 | run('brew', ['install', 'nss'], { stdio: 'ignore' });
57 | } catch (e) {
58 | debug(`brew install nss failed`);
59 | }
60 | } else {
61 | debug(`Homebrew didn't work, so we can't try to install certutil. Falling back to manual certificate install`);
62 | return await openCertificateInFirefox(this.FIREFOX_BIN_PATH, certificatePath);
63 | }
64 | } else {
65 | debug(`certutil is not already installed, and skipCertutilInstall is true, so we have to fall back to a manual install`)
66 | return await openCertificateInFirefox(this.FIREFOX_BIN_PATH, certificatePath);
67 | }
68 | }
69 | await closeFirefox();
70 | await addCertificateToNSSCertDB(this.FIREFOX_NSS_DIR, certificatePath, getCertUtilPath());
71 | } else {
72 | debug('Firefox does not appear to be installed, skipping Firefox-specific steps...');
73 | }
74 | }
75 |
76 | removeFromTrustStores(certificatePath: string) {
77 | debug('Removing devcert root CA from macOS system keychain');
78 | try {
79 | run('sudo', [
80 | 'security',
81 | 'remove-trusted-cert',
82 | '-d',
83 | certificatePath
84 | ], {
85 | stdio: 'ignore'
86 | });
87 | } catch(e) {
88 | debug(`failed to remove ${ certificatePath } from macOS cert store, continuing. ${ e.toString() }`);
89 | }
90 | if (this.isFirefoxInstalled() && this.isNSSInstalled()) {
91 | debug('Firefox install and certutil install detected. Trying to remove root CA from Firefox NSS databases');
92 | removeCertificateFromNSSCertDB(this.FIREFOX_NSS_DIR, certificatePath, getCertUtilPath());
93 | }
94 | }
95 |
96 | async addDomainToHostFileIfMissing(domain: string) {
97 | const trimDomain = domain.trim().replace(/[\s;]/g,'')
98 | let hostsFileContents = read(this.HOST_FILE_PATH, 'utf8');
99 | if (!hostsFileContents.includes(trimDomain)) {
100 | sudoAppend(this.HOST_FILE_PATH, `127.0.0.1 ${trimDomain}\n`);
101 | }
102 | }
103 |
104 | deleteProtectedFiles(filepath: string) {
105 | assertNotTouchingFiles(filepath, 'delete');
106 | run('sudo', ['rm', '-rf', filepath]);
107 | }
108 |
109 | async readProtectedFile(filepath: string) {
110 | assertNotTouchingFiles(filepath, 'read');
111 | return (await run('sudo', ['cat', filepath])).toString().trim();
112 | }
113 |
114 | async writeProtectedFile(filepath: string, contents: string) {
115 | assertNotTouchingFiles(filepath, 'write');
116 | if (exists(filepath)) {
117 | await run('sudo', ['rm', filepath]);
118 | }
119 | writeFile(filepath, contents);
120 | await run('sudo', ['chown', '0', filepath]);
121 | await run('sudo', ['chmod', '600', filepath]);
122 | }
123 |
124 | private isFirefoxInstalled() {
125 | return exists(this.FIREFOX_BUNDLE_PATH);
126 | }
127 |
128 | private isNSSInstalled() {
129 | try {
130 | return run('brew', ['list', '-1']).toString().includes('\nnss\n');
131 | } catch (e) {
132 | return false;
133 | }
134 | }
135 |
136 | };
137 |
--------------------------------------------------------------------------------
/src/platforms/index.ts:
--------------------------------------------------------------------------------
1 | import { Options } from '../index';
2 |
3 |
4 | export interface Platform {
5 | addToTrustStores(certificatePath: string, options?: Options): Promise;
6 | removeFromTrustStores(certificatePath: string): void;
7 | addDomainToHostFileIfMissing(domain: string): Promise;
8 | deleteProtectedFiles(filepath: string): void;
9 | readProtectedFile(filepath: string): Promise;
10 | writeProtectedFile(filepath: string, contents: string): Promise;
11 | }
12 |
13 | const PlatformClass = require(`./${ process.platform }`).default;
14 | export default new PlatformClass() as Platform;
15 |
--------------------------------------------------------------------------------
/src/platforms/linux.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { existsSync as exists, readFileSync as read, writeFileSync as writeFile } from 'fs';
3 | import createDebug from 'debug';
4 | import { sync as commandExists } from 'command-exists';
5 | import { addCertificateToNSSCertDB, assertNotTouchingFiles, openCertificateInFirefox, closeFirefox, removeCertificateFromNSSCertDB } from './shared';
6 | import { run, sudoAppend } from '../utils';
7 | import { Options } from '../index';
8 | import UI from '../user-interface';
9 | import { Platform } from '.';
10 |
11 | const debug = createDebug('devcert:platforms:linux');
12 |
13 | export default class LinuxPlatform implements Platform {
14 |
15 | private FIREFOX_NSS_DIR = path.join(process.env.HOME, '.mozilla/firefox/*');
16 | private CHROME_NSS_DIR = path.join(process.env.HOME, '.pki/nssdb');
17 | private FIREFOX_BIN_PATH = '/usr/bin/firefox';
18 | private CHROME_BIN_PATH = '/usr/bin/google-chrome';
19 |
20 | private HOST_FILE_PATH = '/etc/hosts';
21 |
22 | /**
23 | * Linux is surprisingly difficult. There seems to be multiple system-wide
24 | * repositories for certs, so we copy ours to each. However, Firefox does it's
25 | * usual separate trust store. Plus Chrome relies on the NSS tooling (like
26 | * Firefox), but uses the user's NSS database, unlike Firefox (which uses a
27 | * separate Mozilla one). And since Chrome doesn't prompt the user with a GUI
28 | * flow when opening certs, if we can't use certutil to install our certificate
29 | * into the user's NSS database, we're out of luck.
30 | */
31 | async addToTrustStores(certificatePath: string, options: Options = {}): Promise {
32 |
33 | debug('Adding devcert root CA to Linux system-wide trust stores');
34 | // run(`sudo cp ${ certificatePath } /etc/ssl/certs/devcert.crt`);
35 | run('sudo', ['cp', certificatePath, '/usr/local/share/ca-certificates/devcert.crt']);
36 | // run(`sudo bash -c "cat ${ certificatePath } >> /etc/ssl/certs/ca-certificates.crt"`);
37 | run('sudo', ['update-ca-certificates']);
38 |
39 | if (this.isFirefoxInstalled()) {
40 | // Firefox
41 | debug('Firefox install detected: adding devcert root CA to Firefox-specific trust stores ...');
42 | if (!commandExists('certutil')) {
43 | if (options.skipCertutilInstall) {
44 | debug('NSS tooling is not already installed, and `skipCertutil` is true, so falling back to manual certificate install for Firefox');
45 | openCertificateInFirefox(this.FIREFOX_BIN_PATH, certificatePath);
46 | } else {
47 | debug('NSS tooling is not already installed. Trying to install NSS tooling now with `apt install`');
48 | run('sudo', ['apt', 'install', 'libnss3-tools']);
49 | debug('Installing certificate into Firefox trust stores using NSS tooling');
50 | await closeFirefox();
51 | await addCertificateToNSSCertDB(this.FIREFOX_NSS_DIR, certificatePath, 'certutil');
52 | }
53 | }
54 | } else {
55 | debug('Firefox does not appear to be installed, skipping Firefox-specific steps...');
56 | }
57 |
58 | if (this.isChromeInstalled()) {
59 | debug('Chrome install detected: adding devcert root CA to Chrome trust store ...');
60 | if (!commandExists('certutil')) {
61 | UI.warnChromeOnLinuxWithoutCertutil();
62 | } else {
63 | await closeFirefox();
64 | await addCertificateToNSSCertDB(this.CHROME_NSS_DIR, certificatePath, 'certutil');
65 | }
66 | } else {
67 | debug('Chrome does not appear to be installed, skipping Chrome-specific steps...');
68 | }
69 | }
70 |
71 | removeFromTrustStores(certificatePath: string) {
72 | try {
73 | run('sudo', ['rm', '/usr/local/share/ca-certificates/devcert.crt']);
74 | run('sudo', ['update-ca-certificates']);
75 | } catch (e) {
76 | debug(`failed to remove ${ certificatePath } from /usr/local/share/ca-certificates, continuing. ${ e.toString() }`);
77 | }
78 | if (commandExists('certutil')) {
79 | if (this.isFirefoxInstalled()) {
80 | removeCertificateFromNSSCertDB(this.FIREFOX_NSS_DIR, certificatePath, 'certutil');
81 | }
82 | if (this.isChromeInstalled()) {
83 | removeCertificateFromNSSCertDB(this.CHROME_NSS_DIR, certificatePath, 'certutil');
84 | }
85 | }
86 | }
87 |
88 | async addDomainToHostFileIfMissing(domain: string) {
89 | const trimDomain = domain.trim().replace(/[\s;]/g,'')
90 | let hostsFileContents = read(this.HOST_FILE_PATH, 'utf8');
91 | if (!hostsFileContents.includes(trimDomain)) {
92 | sudoAppend(this.HOST_FILE_PATH, `127.0.0.1 ${trimDomain}\n`);
93 | }
94 | }
95 |
96 | deleteProtectedFiles(filepath: string) {
97 | assertNotTouchingFiles(filepath, 'delete');
98 | run('sudo', ['rm', '-rf', filepath]);
99 | }
100 |
101 | async readProtectedFile(filepath: string) {
102 | assertNotTouchingFiles(filepath, 'read');
103 | return (await run('sudo', ['cat', filepath])).toString().trim();
104 | }
105 |
106 | async writeProtectedFile(filepath: string, contents: string) {
107 | assertNotTouchingFiles(filepath, 'write');
108 | if (exists(filepath)) {
109 | await run('sudo', ['rm', filepath]);
110 | }
111 | writeFile(filepath, contents);
112 | await run('sudo', ['chown', '0', filepath]);
113 | await run('sudo', ['chmod', '600', filepath]);
114 | }
115 |
116 | private isFirefoxInstalled() {
117 | return exists(this.FIREFOX_BIN_PATH);
118 | }
119 |
120 | private isChromeInstalled() {
121 | return exists(this.CHROME_BIN_PATH);
122 | }
123 |
124 | }
--------------------------------------------------------------------------------
/src/platforms/shared.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import url from 'url';
3 | import createDebug from 'debug';
4 | import assert from 'assert';
5 | import getPort from 'get-port';
6 | import http from 'http';
7 | import { sync as glob } from 'glob';
8 | import { readFileSync as readFile, existsSync as exists } from 'fs';
9 | import { run } from '../utils';
10 | import { isMac, isLinux , configDir, getLegacyConfigDir } from '../constants';
11 | import UI from '../user-interface';
12 | import { execSync as exec } from 'child_process';
13 |
14 | const debug = createDebug('devcert:platforms:shared');
15 |
16 | /**
17 | * Given a directory or glob pattern of directories, run a callback for each db
18 | * directory, with a version argument.
19 | */
20 | function doForNSSCertDB(nssDirGlob: string, callback: (dir: string, version: "legacy" | "modern") => void): void {
21 | glob(nssDirGlob).forEach((potentialNSSDBDir) => {
22 | debug(`checking to see if ${ potentialNSSDBDir } is a valid NSS database directory`);
23 | if (exists(path.join(potentialNSSDBDir, 'cert8.db'))) {
24 | debug(`Found legacy NSS database in ${ potentialNSSDBDir }, running callback...`)
25 | callback(potentialNSSDBDir, 'legacy');
26 | }
27 | if (exists(path.join(potentialNSSDBDir, 'cert9.db'))) {
28 | debug(`Found modern NSS database in ${ potentialNSSDBDir }, running callback...`)
29 | callback(potentialNSSDBDir, 'modern');
30 | }
31 | });
32 | }
33 |
34 | /**
35 | * Given a directory or glob pattern of directories, attempt to install the
36 | * CA certificate to each directory containing an NSS database.
37 | */
38 | export function addCertificateToNSSCertDB(nssDirGlob: string, certPath: string, certutilPath: string): void {
39 | debug(`trying to install certificate into NSS databases in ${ nssDirGlob }`);
40 | doForNSSCertDB(nssDirGlob, (dir, version) => {
41 | const dirArg = version === 'modern' ? `sql:${ dir }` : dir;
42 | run(certutilPath, ['-A', '-d', dirArg, '-t', 'C,,', '-i', certPath, '-n', 'devcert']);
43 | });
44 | debug(`finished scanning & installing certificate in NSS databases in ${ nssDirGlob }`);
45 | }
46 |
47 | export function removeCertificateFromNSSCertDB(nssDirGlob: string, certPath: string, certutilPath: string): void {
48 | debug(`trying to remove certificates from NSS databases in ${ nssDirGlob }`);
49 | doForNSSCertDB(nssDirGlob, (dir, version) => {
50 | const dirArg = version === 'modern' ? `sql:${ dir }` : dir;
51 | try {
52 | run(certutilPath, ['-A', '-d', dirArg, '-t', 'C,,', '-i', certPath, '-n', 'devcert']);
53 | } catch (e) {
54 | debug(`failed to remove ${ certPath } from ${ dir }, continuing. ${ e.toString() }`)
55 | }
56 | });
57 | debug(`finished scanning & installing certificate in NSS databases in ${ nssDirGlob }`);
58 | }
59 |
60 | /**
61 | * Check to see if Firefox is still running, and if so, ask the user to close
62 | * it. Poll until it's closed, then return.
63 | *
64 | * This is needed because Firefox appears to load the NSS database in-memory on
65 | * startup, and overwrite on exit. So we have to ask the user to quite Firefox
66 | * first so our changes don't get overwritten.
67 | */
68 | export async function closeFirefox(): Promise {
69 | if (isFirefoxOpen()) {
70 | await UI.closeFirefoxBeforeContinuing();
71 | while(isFirefoxOpen()) {
72 | await sleep(50);
73 | }
74 | }
75 | }
76 |
77 | /**
78 | * Check if Firefox is currently open
79 | */
80 | function isFirefoxOpen() {
81 | // NOTE: We use some Windows-unfriendly methods here (ps) because Windows
82 | // never needs to check this, because it doesn't update the NSS DB
83 | // automaticaly.
84 | assert(isMac || isLinux, 'checkForOpenFirefox was invoked on a platform other than Mac or Linux');
85 | return exec('ps aux').indexOf('firefox') > -1;
86 | }
87 |
88 | async function sleep(ms: number) {
89 | return new Promise((resolve) => setTimeout(resolve, ms));
90 | }
91 |
92 | /**
93 | * Firefox manages it's own trust store for SSL certificates, which can be
94 | * managed via the certutil command (supplied by NSS tooling packages). In the
95 | * event that certutil is not already installed, and either can't be installed
96 | * (Windows) or the user doesn't want to install it (skipCertutilInstall:
97 | * true), it means that we can't programmatically tell Firefox to trust our
98 | * root CA certificate.
99 | *
100 | * There is a recourse though. When a Firefox tab is directed to a URL that
101 | * responds with a certificate, it will automatically prompt the user if they
102 | * want to add it to their trusted certificates. So if we can't automatically
103 | * install the certificate via certutil, we instead start a quick web server
104 | * and host our certificate file. Then we open the hosted cert URL in Firefox
105 | * to kick off the GUI flow.
106 | *
107 | * This method does all this, along with providing user prompts in the terminal
108 | * to walk them through this process.
109 | */
110 | export async function openCertificateInFirefox(firefoxPath: string, certPath: string): Promise {
111 | debug('Adding devert to Firefox trust stores manually. Launching a webserver to host our certificate temporarily ...');
112 | let port = await getPort();
113 | let server = http.createServer(async (req, res) => {
114 | let { pathname } = url.parse(req.url);
115 | if (pathname === '/certificate') {
116 | res.writeHead(200, { 'Content-type': 'application/x-x509-ca-cert' });
117 | res.write(readFile(certPath));
118 | res.end();
119 | } else {
120 | res.writeHead(200);
121 | res.write(await UI.firefoxWizardPromptPage(`http://localhost:${ port }/certificate`));
122 | res.end();
123 | }
124 | }).listen(port);
125 | debug('Certificate server is up. Printing instructions for user and launching Firefox with hosted certificate URL');
126 | await UI.startFirefoxWizard(`http://localhost:${ port }`);
127 | run(firefoxPath, [`http://localhost:${ port }`]);
128 | await UI.waitForFirefoxWizard();
129 | server.close();
130 | }
131 |
132 | export function assertNotTouchingFiles(filepath: string, operation: string): void {
133 | if (!filepath.startsWith(configDir) && !filepath.startsWith(getLegacyConfigDir())) {
134 | throw new Error(`Devcert cannot ${ operation } ${ filepath }; it is outside known devcert config directories!`);
135 | }
136 | }
--------------------------------------------------------------------------------
/src/platforms/win32.ts:
--------------------------------------------------------------------------------
1 | import createDebug from 'debug';
2 | import crypto from 'crypto';
3 | import { writeFileSync as write, readFileSync as read } from 'fs';
4 | import { sync as rimraf } from 'rimraf';
5 | import { Options } from '../index';
6 | import { assertNotTouchingFiles, openCertificateInFirefox } from './shared';
7 | import { Platform } from '.';
8 | import { run, sudo } from '../utils';
9 | import UI from '../user-interface';
10 |
11 | const debug = createDebug('devcert:platforms:windows');
12 |
13 | let encryptionKey: string;
14 |
15 | export default class WindowsPlatform implements Platform {
16 |
17 | private HOST_FILE_PATH = 'C:\\Windows\\System32\\Drivers\\etc\\hosts';
18 |
19 | /**
20 | * Windows is at least simple. Like macOS, most applications will delegate to
21 | * the system trust store, which is updated with the confusingly named
22 | * `certutil` exe (not the same as the NSS/Mozilla certutil). Firefox does it's
23 | * own thing as usual, and getting a copy of NSS certutil onto the Windows
24 | * machine to try updating the Firefox store is basically a nightmare, so we
25 | * don't even try it - we just bail out to the GUI.
26 | */
27 | async addToTrustStores(certificatePath: string, options: Options = {}): Promise {
28 | // IE, Chrome, system utils
29 | debug('adding devcert root to Windows OS trust store')
30 | try {
31 | run('certutil', ['-addstore', '-user', 'root', certificatePath]);
32 | } catch (e) {
33 | e.output.map((buffer: Buffer) => {
34 | if (buffer) {
35 | console.log(buffer.toString());
36 | }
37 | });
38 | }
39 | debug('adding devcert root to Firefox trust store')
40 | // Firefox (don't even try NSS certutil, no easy install for Windows)
41 | try {
42 | await openCertificateInFirefox('start firefox', certificatePath);
43 | } catch {
44 | debug('Error opening Firefox, most likely Firefox is not installed');
45 | }
46 | }
47 |
48 | removeFromTrustStores(certificatePath: string) {
49 | debug('removing devcert root from Windows OS trust store');
50 | try {
51 | console.warn('Removing old certificates from trust stores. You may be prompted to grant permission for this. It\'s safe to delete old devcert certificates.');
52 | run('certutil', ['-delstore', '-user', 'root', 'devcert']);
53 | } catch (e) {
54 | debug(`failed to remove ${ certificatePath } from Windows OS trust store, continuing. ${ e.toString() }`)
55 | }
56 | }
57 |
58 | async addDomainToHostFileIfMissing(domain: string) {
59 | let hostsFileContents = read(this.HOST_FILE_PATH, 'utf8');
60 | if (!hostsFileContents.includes(domain)) {
61 | await sudo(`echo 127.0.0.1 ${ domain } >> ${ this.HOST_FILE_PATH }`);
62 | }
63 | }
64 |
65 | deleteProtectedFiles(filepath: string) {
66 | assertNotTouchingFiles(filepath, 'delete');
67 | rimraf(filepath);
68 | }
69 |
70 | async readProtectedFile(filepath: string): Promise {
71 | assertNotTouchingFiles(filepath, 'read');
72 | if (!encryptionKey) {
73 | encryptionKey = await UI.getWindowsEncryptionPassword();
74 | }
75 | // Try to decrypt the file
76 | try {
77 | return this.decrypt(read(filepath, 'utf8'), encryptionKey);
78 | } catch (e) {
79 | // If it's a bad password, clear the cached copy and retry
80 | if (e.message.indexOf('bad decrypt') >= -1) {
81 | encryptionKey = null;
82 | return await this.readProtectedFile(filepath);
83 | }
84 | throw e;
85 | }
86 | }
87 |
88 | async writeProtectedFile(filepath: string, contents: string) {
89 | assertNotTouchingFiles(filepath, 'write');
90 | if (!encryptionKey) {
91 | encryptionKey = await UI.getWindowsEncryptionPassword();
92 | }
93 | let encryptedContents = this.encrypt(contents, encryptionKey);
94 | write(filepath, encryptedContents);
95 | }
96 |
97 | private encrypt(text: string, key: string) {
98 | let cipher = crypto.createCipher('aes256', new Buffer(key));
99 | return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
100 | }
101 |
102 | private decrypt(encrypted: string, key: string) {
103 | let decipher = crypto.createDecipher('aes256', new Buffer(key));
104 | return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
105 | }
106 |
107 | }
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module "command-exists";
2 | declare module "eol";
3 | declare module "sudo-prompt";
4 | declare module "password-prompt";
5 | declare module "application-config-path" {
6 | export = (appName: string) => string;
7 | }
--------------------------------------------------------------------------------
/src/user-interface.ts:
--------------------------------------------------------------------------------
1 | import passwordPrompt from 'password-prompt';
2 | import { waitForUser } from './utils';
3 |
4 | export interface UserInterface {
5 | getWindowsEncryptionPassword(): Promise;
6 | warnChromeOnLinuxWithoutCertutil(): Promise;
7 | closeFirefoxBeforeContinuing(): Promise;
8 | startFirefoxWizard(certificateHost: string): Promise;
9 | firefoxWizardPromptPage(certificateURL: string): Promise;
10 | waitForFirefoxWizard(): Promise;
11 | }
12 |
13 | const DefaultUI: UserInterface = {
14 | async getWindowsEncryptionPassword() {
15 | return await passwordPrompt('devcert password (http://bit.ly/devcert-what-password?):');
16 | },
17 | async warnChromeOnLinuxWithoutCertutil() {
18 | console.warn(`
19 | WARNING: It looks like you have Chrome installed, but you specified
20 | 'skipCertutilInstall: true'. Unfortunately, without installing
21 | certutil, it's impossible get Chrome to trust devcert's certificates
22 | The certificates will work, but Chrome will continue to warn you that
23 | they are untrusted.
24 | `);
25 | },
26 | async closeFirefoxBeforeContinuing() {
27 | console.log('Please close Firefox before continuing');
28 | },
29 | async startFirefoxWizard(certificateHost) {
30 | console.log(`
31 | devcert was unable to automatically configure Firefox. You'll need to
32 | complete this process manually. Don't worry though - Firefox will walk
33 | you through it.
34 |
35 | When you're ready, hit any key to continue. Firefox will launch and
36 | display a wizard to walk you through how to trust the devcert
37 | certificate. When you are finished, come back here and we'll finish up.
38 |
39 | (If Firefox doesn't start, go ahead and start it and navigate to
40 | ${ certificateHost } in a new tab.)
41 |
42 | If you are curious about why all this is necessary, check out
43 | https://github.com/davewasmer/devcert#how-it-works
44 |
45 |
46 | `);
47 | await waitForUser();
48 | },
49 | async firefoxWizardPromptPage(certificateURL: string) {
50 | return `
51 |
52 |
53 |
54 |
55 |
56 | `;
57 | },
58 | async waitForFirefoxWizard() {
59 | console.log(`
60 | Launching Firefox ...
61 |
62 | Great! Once you've finished the Firefox wizard for adding the devcert
63 | certificate, just hit any key here again and we'll wrap up.
64 |
65 |
66 | `)
67 | await waitForUser();
68 | }
69 | }
70 |
71 | export default DefaultUI;
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { execFileSync, ExecFileSyncOptions } from 'child_process';
2 | import tmp from 'tmp';
3 | import createDebug from 'debug';
4 | import path from 'path';
5 | import sudoPrompt from 'sudo-prompt';
6 |
7 | import { configPath } from './constants';
8 |
9 | const debug = createDebug('devcert:util');
10 |
11 | export function openssl(args: string[]) {
12 | return run('openssl', args, {
13 | stdio: 'pipe',
14 | env: Object.assign({
15 | RANDFILE: path.join(configPath('.rnd'))
16 | }, process.env)
17 | });
18 | }
19 |
20 | export function run(cmd: string, args: string[], options: ExecFileSyncOptions = {}) {
21 | debug(`execFileSync: \`${ cmd } ${args.join(' ')}\``);
22 | return execFileSync(cmd, args, options);
23 | }
24 |
25 | export function sudoAppend(file: string, input: ExecFileSyncOptions["input"]) {
26 | run('sudo', ['tee', '-a', file], {
27 | input
28 | });
29 | }
30 |
31 | export function waitForUser() {
32 | return new Promise((resolve) => {
33 | process.stdin.resume();
34 | process.stdin.on('data', resolve);
35 | });
36 | }
37 |
38 | export function reportableError(message: string) {
39 | return new Error(`${message} | This is a bug in devcert, please report the issue at https://github.com/davewasmer/devcert/issues`);
40 | }
41 |
42 | export function mktmp() {
43 | // discardDescriptor because windows complains the file is in use if we create a tmp file
44 | // and then shell out to a process that tries to use it
45 | return tmp.fileSync({ discardDescriptor: true }).name;
46 | }
47 |
48 | export function sudo(cmd: string): Promise {
49 | return new Promise((resolve, reject) => {
50 | sudoPrompt.exec(cmd, { name: 'devcert' }, (err: Error | null, stdout: string | null, stderr: string | null) => {
51 | let error = err || (typeof stderr === 'string' && stderr.trim().length > 0 && new Error(stderr)) ;
52 | error ? reject(error) : resolve(stdout);
53 | });
54 | });
55 | }
56 |
57 | export const numericHash = (str: string): number => {
58 | let hash = 5381;
59 | let i = str.length;
60 |
61 | while (i) {
62 | hash = hash * 33 ^ str.charCodeAt(--i);
63 | }
64 |
65 | return hash >>> 0;
66 | };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "declaration": true,
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "target": "ES2016",
8 | "noImplicitAny": true,
9 | "sourceMap": false,
10 | "importHelpers": true,
11 | "inlineSourceMap": true,
12 | "inlineSources": true,
13 | "outDir": "dist",
14 | "baseUrl": ".",
15 | "skipLibCheck": true,
16 | "sourceRoot": ".",
17 | "noUnusedLocals": true,
18 | "esModuleInterop": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------