SSL Trust Configuration

Background

Previously we discussed the basics of our HTTP Proxy Setup, and next we will deal with HTTPS connections securely. All future code samples will then use SSL for all connections. I use ‘SSL’ as the most widely used term, and the actual connections will usually always use TLS 1.3.

Goal: Developer Productivity

When working with OAuth secured UIs and APIs, SSL messages tend to be required in development environments:

  • Messages to the Authorization Server often must use HTTPS
  • Some vendors also require HTTPS URLs for Web Redirect URIs

In some setups, being able to view HTTPS messages in flight can be highly useful, both for learning  OAuth standards, and troubleshooting failed messages.

Goal: Real World Development URLs

For the first code sample we created custom domain names for local PC hosting, and in the next sample these will be updated to HTTPS URLs:

  • https://web.mycompany.com/spa
  • https://api.mycompany.com/api

Code samples will download and use development certificates precreated by the openssl tool, and stored in a GitHub repository. We will take a closer look at how the certificate creation script works at the end of this post.

SSL Trust Model

I avoid standalone SSL certificates for local development. Instead I follow a public key infrastructure approach, in the same way as for real internet certificates. Each development certificate has a root authority, whose public key can be distributed and which components can be configured to trust.

Technical Challenges

Getting SSL Trust working can be problematic in some cases, since it requires different settings across tools and technologies. We will describe configuration for all of the following clients:

  • Chrome / Safari Browsers
  • Microsoft Browser
  • Firefox Browser
  • Electron Desktop Apps
  • Node.js APIs
  • .NET APIs
  • Java APIs
  • Kotlin Android Apps
  • Swift iOS Apps
  • Command Line Tools

In this blog we will run multiple UI and API code samples, then view OAuth messages for all of them, across these operating systems:

  • Windows
  • macOS
  • Linux
  • Android
  • iOS

HTTP Proxy Root Certificates

HTTP proxy tools use a man in the middle technique to replace the root of the target URL’s SSL certificate. This enables the traffic to be viewed as HTTP, then re-encrypted using the proxy’s own certificate:

Some corporate networks use their own SSL Proxy / Firewall Filter that works in a similar manner way, injecting root certificates that replace the root authority of any non-whitelisted external URLs.

Development SSL Certificates

The next code sample will use a Wildcard SSL Certificate, whose root is a Development Root Certificate Authority. The wildcard certificate will use this blog’s custom domain names as Subject Alternative Names:

Identity the Root Authority to Trust

When running this blog’s code samples, the root authority to trust when tracing HTTPS requests will depend whether an HTTP proxy is being used.

Scenario Trust Configuration
HTTP Proxy Running Trust the proxy root certificate
HTTP Proxy Not Running Trust the development root certificate

The following sections describe how to trust the proxy root certificate across various components. The steps to trust the development root certificate are equivalent.

Configure Operating System Trust

On Windows we must run the Microsoft Management Console (MMC.EXE), then select the Certificate Snap In and Import the Root Certificate under Local Computer / Trusted Root Certification Authorities:

On macOS we instead add the Root Certificate to the Keychain Access application under System / Certificates, then set the Always Trust option:

On Linux I configure a root certificate to be trusted by copying it to one of the following folders, depending on which distribution I am using. I also ensure that the file uses a .crt extension rather than .pem:

  • /usr/local/share/ca-certificates
  • /etc/pki/ca-trust/source/anchors

Then I run one of these commands, and the one to use again depends on the Linux distribution:

  • sudo update-ca-certificates
  • sudo update-ca-trust

Trusting a Root Certificate in Browsers

Browsers generally inherit SSL trust from one of the Operating System Default Trust Stores above. There may be some exceptions though, such as Google Chrome on Linux having its own certificate store, similar to that described for Firefox below.

Trusting a Root Certificate in the Firefox Browser

The Firefox browser has its own trust store, under Options / Privacy & Security / Certificates / View Certificates / Authorities / Import, and we can import root certificates here:

Trusting a Root Certificate in Node.js APIs

In the previous post we used a hack to get Node.js to trust a root certificate by setting this environment variable:

  • NODE_TLS_REJECT_UNAUTHORIZED=0

A more correct option is to only trust whitelisted root certificates, via the following environment variable, which I set permanently on my PC:

  • NODE_EXTRA_CA_CERTS=<path to bundle file>

A CA Bundle File contains the public keys of multiple root certificates, and I use a local file containing a number of entries:

# mycompany.com
-----BEGIN CERTIFICATE-----
MIIDZzCCAk+gAwIBAgIJAN7ZkzJkHq8DMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNV
BAMTIFNlbGYgU2lnbmVkIENBIGZvciBteWNvbXBhbnkuY29tMB4XDTIxMTIwNjE4
MTU0MVoXDTMxMTIwNDE4MTU0MVowKzEpMCcGA1UEAxMgU2VsZiBTaWduZWQgQ0Eg
Zm9yIG15Y29tcGFueS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCcstoGEwgoCNp4uJjyquvbFtMjlFEMrRBXKiiC/cF5u/qWUaOnCc2Yg6kyD9rf
eyQTVhjvxt4CrlZJqDeW/+EUY1NdMaztji9lGMv3YqtKRlYL8JzpiWQNHn7qeQhx
OmJJKEPF8XhriqjOBXVA2RefgKngbq6bv3xOvpplEjXBaiEC8ukHU7T0itplWEsu
RV9aY+o2odslkoTITEqkW9WFrNXwLwVH05tNb+5f7Jw/WE8uF+vPY5/9D98/LgZM
8wYQ0tvv+37FdJl25vk29zFaLEQf5iNO+IH1WtLDHdesCNYHq8CIluFC0o3Jf9ti
kz6Z5p2wiXrN+hkCnDsvOI29AgMBAAGjgY0wgYowHQYDVR0OBBYEFJ1scXlGbEsP
/+P6XNOq3x39eLqvMFsGA1UdIwRUMFKAFJ1scXlGbEsP/+P6XNOq3x39eLqvoS+k
LTArMSkwJwYDVQQDEyBTZWxmIFNpZ25lZCBDQSBmb3IgbXljb21wYW55LmNvbYIJ
AN7ZkzJkHq8DMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFnAxVG9
28nMEh3oYkvWCodGrAR8r0Vb2ck0FVOEeper78YhtvzzhW1mMhwnONo83PVpKmIf
6YlQC3XAb3y/0bVc/FtoN40Qh+zHL+WkOx8zqdE4fyRhq1AcGhFQ5Ccx7g+2GbcL
wVO3T+8FjR8Zc+9CWxdE4sDx9RB+34fq6f/6fJTIYp97F/A1FjLGS19rmm09VadL
c708yePaEAOjDCo47t3Y2myO6gGZCPgPsvdP3osxf6QfXLkTwGlrO07+DL0LNDZz
hfnsSjWkqnYf/gqyt00xplaP8vGsoidaSrwAXjGsqwt7rPFQRSoi8elE+3m5/Zaw
+Wmf3vgbiKxlTwY=
-----END CERTIFICATE-----

# Charles Proxy
...

Some Windows tools provide root certificates in a binary DER format. These will need to be translated to PEM format, via the below command, then added to the bundle file:

openssl x509 -inform der -in myRoot.cer -out myRoot.pem

Trusting a Root Certificate in .NET APIs

Later we will provide an OAuth Secured .Net Core API, and trust will be inherited from the Operating System SSL Trust Store. On Linux you will need to use a SSL_CERT_FILE environment variable that works in an equivalent way to the Node.js environment variable.

Trusting a Root Certificate in Java APIs

Later we will provide a OAuth Secured Sample Java API, and each Java installation has its own certificate trust store in a cacerts file. This is located under the Java install location in a lib/security folder. On macOS this is at the following type of location, for a Java 17 installation:

/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/lib/security

Operations to manage certificates use the keytool utility, and I use the following commands to add, find and remove a trusted root:

sudo "$JAVA_HOME/bin/keytool" -import -alias charlesroot -cacerts -file ~/Desktop/charlesroot.pem -storepass changeit -noprompt
sudo "$JAVA_HOME/bin/keytool" -list -alias charlesroot -cacerts -storepass changeit -noprompt
sudo "$JAVA_HOME/bin/keytool" -delete -alias charlesroot -cacerts -storepass changeit -noprompt

Trusting a Root Certificate in Electron Apps

Later we will provide some OAuth Secured Desktop Apps, where there are two processes, which determine certificate trust differently:

Area Trust Behaviour
Main Process For requests from the main side of the app, extra root CAs cannot be trusted using NODE_EXTRA_CA_CERTS. For development purposes you have to use the environment variable  NODE_TLS_REJECT_UNAUTHORIZED.
Renderer Process The Chromium Browser uses the system trust store on macOS or Windows, or you must import certificates into the browser on Linux desktops

Trusting a Root Certificate in Command Line Tools

Command line tools usually use the default operating system trust store, so that these commands work once root certificates are trusted:

curl https://api.mycompany.com/api/companies
openssl s_client -connect api.mycompany.com:443

It is also possible to use a different certificate bundle file via an override, which may be required in some cases, and is useful for troubleshooting:

curl --cacert ~/certs/trusted.ca.pem https://api.mycompany.com/api/companies
openssl s_client -CAfile ~/certs/trusted.ca.pem -connect api.mycompany.com:443

Trusting a Root Certificate in iOS and Android

Mobile SSL Trust as its own complexities, which are covered in the later posts on mobile setup:

Creating Development Certificates with OpenSSL

Our first code sample uses port 80 but all subsequent samples listen on SSL port 443. The server will use a wildcard certificate whose root is the below self issued authority:

If required, the domain names listed in the extended/server.ext file can be replaced with your own values and you can then run the makeCerts.sh script to create your own certificates:

The end result is a PKCS#12 File, which combines the public certificate and private key, along with a password to protect the file when deployed. You can view the PEM file properties in an online certificate viewer, to understand issuer and DNS details:

Running UIs and APIs over SSL

When our API samples start up, the PKCS#12 File is read and used to listen on SSL port 443:

public async startListening(): Promise<void> {

    const port = this._configuration.api.port;
    if (this._configuration.api.sslCertificateFileName && this._configuration.api.sslCertificatePassword) {

        const pfxFile = await fs.readFile(this._configuration.api.sslCertificateFileName);
        const sslOptions = {
            pfx: pfxFile,
            passphrase: this._configuration.api.sslCertificatePassword,
        };

        const httpsServer = https.createServer(sslOptions, this._expressApp);
        httpsServer.listen(port, () => {
            console.log(`API is listening on HTTPS port ${port}`);
        });

    } else {

        this._expressApp.listen(port, () => {
            console.log(`API is listening on HTTP port ${port}`);
        });
    }
}

The host process then serves content securely for both of these base URLs:

  • API JSON data from https://api.mycompany.com
  • Web Static Content from https://web.mycompany.com

My preference is also to include full DNS API and Web domain names in the Subject Alternative Names of the certificate, since in some cases this is required by clients:

The Google Chrome browser has some useful tools under Main Menu / More Tools / Developer Tools / Security. We can use them to view SSL strength details, and it shows that our scripts produce TLS 1.3 certificates:

Where Are We?

We have improved our development setup, so that our future UIs and APIs will run over Real World SSL URLs. We are also able to view all OAuth message details on a Developer PC.

Next Steps