Skip to content

Commit

Permalink
fix(gatsby): Add self-signed cert to node trust store (https) (#18703)
Browse files Browse the repository at this point in the history
1. It upgrades `devcert` to `v1.1.0` to fix https issues.
    - ~The version is currently pointing to the release branch of my `devcert` fork, as a proof of concept~
    - ~If/when `devcert` gets patched and published, I will update the version spec to point to the new version~
2. It adds a little functionality to trust self-signed, and privately signed, certs
    - If  only flags `--cert-file` and `--key-file` are specified, it assumes it's self-signed, and tells node to trust it.
    - I've included a new CLI flag, `--ca-file`.  If the certificate is signed by a private ca, then include that ca's certificate using this flag in order for node to trust the cert/key pair
3. It adds functionality to collect the ca certificate path from `devcert` during the automatic setup, and tells node to trust it.
4. It updates the documentation to reflect the new changes/process.

Co-authored-by: Jeremy Albright <[email protected]>
Co-authored-by: Ward Peeters <[email protected]>
Co-authored-by: LB <[email protected]>
Co-authored-by: Laurie Barth <[email protected]>
  • Loading branch information
5 people authored Apr 17, 2020
1 parent 2ea2a8e commit 4fd8f8e
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 45 deletions.
148 changes: 128 additions & 20 deletions docs/docs/local-https.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,158 @@ Gatsby provides an easy way to use a local HTTPS server during development, than

Start the development server using `npm run develop` as usual, and add either the `-S` or `--https` flag.

$ npm run develop -- --https
```shell
npm run develop -- --https
```

## Setup

When setting up a development SSL certificate for the first time, you may be asked to type in your password after starting the development environment:

info setting up SSL certificate (may require sudo)
```text
info setting up SSL certificate (may require elevated permissions/sudo)
Password:
```

On windows, the prompt will differ:

```text
A password is required to access the secure certificate authority key
used for signing certificates.
If this is the first time this has run, then this is to set the password
for future use. If any new certificates are signed later, you will need
to use this same password.
Please enter the CA password:
```

The password is _only_ required the first time you are using Gatsby's HTTPS feature on your machine, or when you are creating a brand new certificate.

## Using `Certutil`

After typing in your password, `devcert` will install the CA certificate in your operating system trusted certs store. A utility called `certutil` will be needed to update the trust store for various browsers; specifically: Firefox, and Chrome (when it's running on Linux).

`devcert` is configured to install `certutil` automatically, unless you're running Windows. If an automatic install is not successful, you may need to install it manually.

### Manual Installation of `Certutil`

To install `certutil`, you need to install the `nss tools` package(s). The exact procedure will differ depending on your operating system.

#### Linux

On a linux OS, you should be able to run one of the following, depending on your Linux distro:

```shell
# Debian based (Ubuntu)
sudo apt install libnss3-tools

# RHEL based (Fedora)
sudo yum install nss-tools

# OpenSuse
sudo zypper install mozilla-nss-tools
```

#### MacOS

Run the following command:

```shell
brew install nss
```

#### Windows

Pre-compiled libraries are rare, so you may need to compile it yourself. Because of how difficult Windows makes it, `devcert` will not attempt to update the Firefox trust store automatically; instead, it will fall back to using the "Firefox wizard", detailed below.

Password:
### Debugging Installation

This is _only_ required the first time you are using Gatsby's HTTPS feature on your machine. After that, certificates will be created on the fly.
If you choose not to install `certutil`, or the automatic install is not successful, you may get the following errors/prompts:

#### Chrome on Linux

```text
WARNING: It looks like you have Chrome installed, but you specified
'skipCertutilInstall: true'. Unfortunately, without installing
certutil, it's impossible get Chrome to trust devcert's certificates
The certificates will work, but Chrome will continue to warn you that
they are untrusted.
```

#### Firefox

If you have Firefox installed, `devcert` will try to utilize Firefox itself to trust the certificate

```text
devcert was unable to automatically configure Firefox. You'll need to
complete this process manually. Don't worry though - Firefox will walk
you through it.
When you're ready, hit any key to continue. Firefox will launch and
display a wizard to walk you through how to trust the devcert
certificate. When you are finished, come back here and we'll finish up.
(If Firefox doesn't start, go ahead and start it and navigate to
http://localhost:52175 in a new tab.)
If you are curious about why all this is necessary, check out
https://github.com/davewasmer/devcert#how-it-works
<Press any key to launch Firefox wizard>
```

After typing in your password, `devcert` will attempt to install some software necessary to tell Firefox (and Chrome, only on Linux) to trust your development certificates.
Your options are as follows:

Unable to automatically install SSL certificate - please follow the
prompts at http://localhost:52175 in Firefox to trust the root certificate
See https://github.com/davewasmer/devcert#how-it-works for more details
-- Press <Enter> once you finish the Firefox prompts --
- Press enter and it will launch Firefox for you.

If you wish to support Firefox (or Chrome on Linux), visit `http://localhost:52175` in Firefox and follow the point-and-click wizard. Otherwise, you may press enter without following the prompts. **Reminder: you'll only need to do this once per machine.**
- If you wish to have trust support on Firefox, tell the point-and-click wizard `this certificate can identify websites`, and click OK. Otherwise, you may hit cancel and close the browser, then key return to finish building. **Reminder: you'll only need to do this once per machine.**

Now open the development server at `https://localhost:8000` and enjoy the HTTPS goodness ✨. Of course, you may change the port according to your setup.
## After `devcert` setup process

You can open the development server at `https://localhost:8000` and enjoy the HTTPS goodness ✨. Of course, you may change the port according to your setup.

Find out more about [how devcert works](https://github.com/davewasmer/devcert#how-it-works).

## Management of certificates generated by devcert

If you want to do some maintenance/cleanup of the certificates generated by `devcert`, please refer to the [devcert-cli](https://github.com/davewasmer/devcert-cli/blob/master/README.md)

## Custom Key and Certificate Files

You may find that you need a custom key and certificate file for https if you use multiple
You may find that you need a custom key and certificate file for HTTPS if you use multiple
machines for development (or if your dev environment is containerized in Docker).

If you need to use a custom https setup, you can pass the `--https`, `--key-file` and
`--cert-file` flags to `npm run develop`.
If you need to use a custom HTTPS setup, you can pass the `--https`, `--key-file`,
`--cert-file`, and `--ca-file` flags to `npm run develop`.

- `--cert-file` [relative/absolute path to ssl certificate file]
- `--key-file` [relative/absolute path to ssl key file]
- `--ca-file` [relative/absolute path to ssl certificate authority file]

### Using `npm run develop`

```shell
npm run develop -- --https --key-file ../relative/path/to/key.key --cert-file ../relative/path/to/cert.crt --ca-file ../relative/path/to/ca.crt
```

- `--cert-file` [relative path to ssl certificate file]
- `--key-file` [relative path to ssl key file]
> Note: You can use relative or absolute paths with this command
See the example command:
### Using the Gatsby CLI

```shell
gatsby develop --https --key-file ../relative/path/to/key.key --cert-file ../relative/path/to/cert.crt
gatsby develop --https --key-file ../relative/path/to/key.key --cert-file ../relative/path/to/cert.crt --ca-file ../relative/path/to/ca.crt
```

in most cases, the `--https` passed by itself is easier and more convenient to get local https.
> Note: You can use relative or absolute paths with this command
### Flag usage

Usage of the `--ca-file` flag is only required if your certificate is signed by a certificate authority.

If your certificate is self-signed, then do not include the `--ca-file` flag. Also, if you want your browser to trust a self-signed certificate, you will need to add it to your operating system (or browser's, in Firefox's case) root certificate store for your browser to trust it.

In most cases, the `--https` passed by itself is easier and more convenient to get local HTTPS.

---

Keep in mind that the automatic certificates issued with the `--https` flag are explicitly issued to `localhost` and will only be accepted there. Using it together with the `--host` option will likely result in browser warnings.
Automatic certificates issued with the `--https` flag are issued to `localhost` by default, unless you have used the `--host` flag. If you have, a record in your hosts file will automatically be configured to point the defined host to `127.0.0.1`. At this time, ip addresses defined by `--host` are not supported.
9 changes: 7 additions & 2 deletions packages/gatsby-cli/src/create-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,18 @@ function buildLocalCommands(cli, isLocalSite) {
alias: `cert-file`,
type: `string`,
default: ``,
describe: `Custom HTTPS cert file (relative path; also required: --https, --key-file). See https://www.gatsbyjs.org/docs/local-https/`,
describe: `Custom HTTPS cert file (also required: --https, --key-file). See https://www.gatsbyjs.org/docs/local-https/`,
})
.option(`k`, {
alias: `key-file`,
type: `string`,
default: ``,
describe: `Custom HTTPS key file (relative path; also required: --https, --cert-file). See https://www.gatsbyjs.org/docs/local-https/`,
describe: `Custom HTTPS key file (also required: --https, --cert-file). See https://www.gatsbyjs.org/docs/local-https/`,
})
.option(`ca-file`, {
type: `string`,
default: ``,
describe: `Custom HTTPS CA certificate file (also required: --https, --cert-file, --key-file). See https://www.gatsbyjs.org/docs/local-https/`,
})
.option(`open-tracing-config-file`, {
type: `string`,
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/src/commands/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ module.exports = async (program: IProgram): Promise<void> => {

program.ssl = await getSslCert({
name: sslHost,
caFile: program[`ca-file`],
certFile: program[`cert-file`],
keyFile: program[`key-file`],
directory: program.directory,
Expand Down
2 changes: 0 additions & 2 deletions packages/gatsby/src/commands/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { PackageJson, Reporter } from "gatsby"

export interface ICert {
keyPath: string
certPath: string
key: string
cert: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`gets ssl certs Custom SSL certificate loads a cert from a absolute paths 1`] = `
exports[`gets ssl certs Custom SSL certificate loads a cert from absolute paths 1`] = `
Object {
"cert": "/foo.crt::file",
"certPath": "/foo.crt",
"key": "/foo.key::file",
"keyPath": "/foo.key",
}
`;

exports[`gets ssl certs Custom SSL certificate loads a cert relative to a directory 1`] = `
Object {
"cert": "/app/directory/foo.crt::file",
"certPath": "/app/directory/foo.crt",
"key": "/app/directory/foo.key::file",
"keyPath": "/app/directory/foo.key",
}
`;

Expand Down Expand Up @@ -57,7 +53,7 @@ Array [
exports[`gets ssl certs automatic SSL certificate sets up dev cert 1`] = `
Array [
Array [
"setting up automatic SSL certificate (may require sudo)
"setting up automatic SSL certificate (may require elevated permissions/sudo)
",
],
]
Expand Down
16 changes: 10 additions & 6 deletions packages/gatsby/src/utils/__tests__/get-ssl-cert.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ jest.mock(`devcert`, () => {
}
})

const { certificateFor } = require(`devcert`)
const getDevCert = require(`devcert`).certificateFor
const reporter = require(`gatsby-cli/lib/reporter`)
const getSslCert = require(`../get-ssl-cert`)

describe(`gets ssl certs`, () => {
beforeEach(() => {
reporter.panic.mockClear()
reporter.info.mockClear()
certificateFor.mockClear()
getDevCert.mockClear()
})
describe(`Custom SSL certificate`, () => {
it.each([[{ certFile: `foo` }], [{ keyFile: `bar` }]])(
Expand All @@ -46,7 +46,7 @@ describe(`gets ssl certs`, () => {
})
).resolves.toMatchSnapshot()
})
it(`loads a cert from a absolute paths`, () => {
it(`loads a cert from absolute paths`, () => {
expect(
getSslCert({
name: `mock-cert`,
Expand All @@ -60,13 +60,17 @@ describe(`gets ssl certs`, () => {
describe(`automatic SSL certificate`, () => {
it(`sets up dev cert`, () => {
getSslCert({ name: `mock-cert` })
expect(certificateFor).toBeCalledWith(`mock-cert`, {
installCertutil: true,
expect(getDevCert).toBeCalledWith(`mock-cert`, {
getCaPath: true,
skipCertutilInstall: false,
ui: {
getWindowsEncryptionPassword: expect.any(Function),
},
})
expect(reporter.info.mock.calls).toMatchSnapshot()
})
it(`panics if certificate can't be created`, () => {
certificateFor.mockImplementation(() => {
getDevCert.mockImplementation(() => {
throw new Error(`mock error message`)
})
getSslCert({ name: `mock-cert` })
Expand Down
50 changes: 43 additions & 7 deletions packages/gatsby/src/utils/get-ssl-cert.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const report = require(`gatsby-cli/lib/reporter`)
const fs = require(`fs`)
const path = require(`path`)
const os = require(`os`)
const prompts = require(`prompts`)

const absoluteOrDirectory = (directory, filePath) => {
// Support absolute paths
Expand All @@ -11,7 +12,28 @@ const absoluteOrDirectory = (directory, filePath) => {
return path.join(directory, filePath)
}

module.exports = async ({ name, certFile, keyFile, directory }) => {
const getWindowsEncryptionPassword = async () => {
report.info(
[
`A password is required to access the secure certificate authority key`,
`used for signing certificates.`,
``,
`If this is the first time this has run, then this is to set the password`,
`for future use. If any new certificates are signed later, you will need`,
`to use this same password.`,
``,
].join(`\n`)
)
const results = await prompts({
type: `password`,
name: `value`,
message: `Please enter the CA password`,
validate: input => input.length > 0 || `You must enter a password.`,
})
return results.value
}

module.exports = async ({ name, certFile, keyFile, caFile, directory }) => {
// check that cert file and key file are both true or both false, if they are both
// false, it defaults to the automatic ssl
if (certFile ? !keyFile : keyFile) {
Expand All @@ -25,15 +47,18 @@ module.exports = async ({ name, certFile, keyFile, directory }) => {
const keyPath = absoluteOrDirectory(directory, keyFile)
const certPath = absoluteOrDirectory(directory, certFile)

process.env.NODE_EXTRA_CA_CERTS = caFile
? absoluteOrDirectory(directory, caFile)
: certPath
return await {
keyPath,
certPath,
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
}
}

report.info(`setting up automatic SSL certificate (may require sudo)\n`)
report.info(
`setting up automatic SSL certificate (may require elevated permissions/sudo)\n`
)
try {
if ([`linux`, `darwin`].includes(os.platform()) && !process.env.HOME) {
// this is a total hack to ensure process.env.HOME is set on linux and mac
Expand All @@ -46,10 +71,21 @@ module.exports = async ({ name, certFile, keyFile, directory }) => {
const mkdtemp = fs.mkdtempSync(path.join(os.tmpdir(), `home-`))
process.env.HOME = mkdtemp
}
const certificateFor = require(`devcert`).certificateFor
return await certificateFor(name, {
installCertutil: true,
const getDevCert = require(`devcert`).certificateFor
const { caPath, key, cert } = await getDevCert(name, {
getCaPath: true,
skipCertutilInstall: false,
ui: {
getWindowsEncryptionPassword,
},
})
if (caPath) {
process.env.NODE_EXTRA_CA_CERTS = caPath
}
return {
key,
cert,
}
} catch (err) {
report.panic({
id: `11522`,
Expand Down
Loading

0 comments on commit 4fd8f8e

Please sign in to comment.