├── .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 | --------------------------------------------------------------------------------