Add root CA certificates to trust stores on MacOS

NOTE: Exported from this Notion page

This article is about adding your own root CA certificate to your local root trust stores. And by doing that all the certificates (intermediate or leaf) signed by that is automatically trusted because of the “chain of trust”. I.e., when you have created one root certificate with mkcert you only have to add it once to the trust stores. When you need to create new certificates you can do so successfully as long as those are signed by your root certificate. The alternative would be to add every certificate you create to every trust store.

What I want to achieve looks something like this:

Certificates and trust stores

Diagram rendered by the amazing plantuml ☝️

There’s a lot of different CA (Certificate Authority) root trust stores, not all applications uses the system’s, e.g., macOS Trust Store. So if you want some applications to trust your certificate you need to add it to those particular store. E.g., if you’re using Google Chrome, you’re good as of now, because it uses the system’s store. But they are planning on creating their own in the future. Read about their “Root Program” here.

Out of scope of this article

I present multiple ways of installing certificates:

On all the commands below I use the $CERT variable which point to my root CA certificate I want to trust:

CERT="/path/to/my/cert/my-root.crt"

Install certificates using step

Smallstep supports adding root certs to multiple trust stores. This is a lot easier than doing it manually. As of now it supports the following stores:

brew install step
step certificate install --all $CERT

Even though I got an error when installing cert to Firefox… But if it works it will save you a lot of time 🙂

Install certificates manually

macOS Trust Store

MacOS store is managed through “Keychain Access”. Install a cert with a command like this:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain $CERT

Nodejs

You add extra certs with an environment variable included in the node process:

export NODE_EXTRA_CA_CERTS=$CERT

See https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file

Curl

Curl uses openssl as its trust store. And by default (I think) openssl is using the system’s store. But if you only want to update openssl’s store follow along.

You do that by copying the cert to this directory: /usr/local/etc/[email protected]/certs/. I take for granted openssl is installed with version 1.1 with homebrew on it’s default location.

cp $CERT /usr/local/etc/[email protected]/certs/

And then run this to update openssl:

/usr/local/opt/[email protected]/bin/c_rehash

And that’s it. That was easy ☺️.

The above is explained by homebrew: brew info openssl.

Optional approach to trust certificate per request (without adding it to store):

curl --cacert $CERT https://localhost

Firefox

Firefox is using Mozillas NSS root store. Firefox recommends installing custom certificates this way: https://support.mozilla.org/en-US/kb/setting-certificate-authorities-firefox#w_using-policies-to-import-ca-certificates-recommended. I haven’t tried that as I wanted a fully automated way and went for using certutil instead.

Install with certutil

certutil is a cli-tool and it’s dependent on nss. Here is a resource with all the commands and arguments to certutil: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/tools/NSS_Tools_certutil.

  1. Install dependencies

    brew install nss
    
  2. Locate certutil (optional if it’s not added to your PATH)

    When I installedd nss with homebrew it was symlinked into /usr/local/bin and therefore in my PATH already. So I could skip this step. If this command gives output you are fine as well:

    $ which certutil
    /usr/local/bin/certutil
    

    If not let’s find certutil. Locate nss with:

    brew --prefix nss
    

    That should output the location of nss which contains certutil. Append bin/certutil to the above output and you have /usr/local/opt/nss/bin/certutil on a macOS.

  3. Locate your Firefox profile

    You also need to find you Firefox profile path. It usually is here: ~/Library/Application Support/Firefox/Profiles/xxxxxxxx.default with the .default suffix on it. But maybe you use another profile. My profile was called xxxxxxxx.dev-edition-default-1579772843598/ as I use Firefox Developer Edition.

  4. Add certificate

    Ok. Now we can add the certificate when we have the path to our certificate ($CERT), the $PROFILE and can execute certutil. To identify the certificate it’s also required to set a “nickname” on it with -t. I set it with $NAME:

    PROFILE="~/Library/Application\ Support/Firefox/Profiles/df25fipq.dev-edition-default-1579772843598/"
    NAME=MyUniqueTestCAName1
    certutil -A -d $PROFILE -t "C,," -n $NAME -i $CERT
    

    You can now list the certificates in the profile to see if it got added. You should recognize it by the name you specified with -n parameter.

  5. List certificates

    certutil -L -d $PROFILE
    
  6. Extra: Remove certificate

    It’s useful to be able to remove certificates. Do it by targetting the name of it:

    certutil -D -d $PROFILE -n $NAME
    

iOS

Apple seems to recommend using Apple Configurator 2 from the app store. See https://support.apple.com/en-us/HT204477 and https://developer.apple.com/library/archive/qa/qa1948/_index.html. But I’m not sure if it’s still applicable for iOS13

Java

I’ll refer to IBM’s knowledge center: https://www.ibm.com/support/knowledgecenter/en/SSEP7J_10.2.2/com.ibm.swg.ba.cognos.adm_ba_pattern.1.2.0.doc/t_biblu_add_cacert.html or Oracles: https://docs.oracle.com/cd/E19906-01/820-4916/geygn/index.html

Docker environment

Container to Container communication

I’ve came across scenarios when I make HTTPS requests between containers, e.g., when I want to mock an external service and can’t change the protocol to HTTP. So if I do a HTTPS requests from my Nodejs-container app to https://stripe.com/api-call which is my mock-service I need to present a valid certificate for Nodejs from my mock service.

A good thing to know about is that you can redirect network domains in docker-compose with network aliasas. This is great for mocking external services, when you, e.g., want to handle all external traffic in a an internal service.

This example make all calls to [api.stripe.com](http://api.stripe.com) getting routed to my-app for example:

my-app:
  image: my-app
  ports:
    - 80:80
    - 443:443
  networks:
    default:
      aliases:
        - api.stripe.com

Resource: https://hackernoon.com/alpine-docker-image-with-secured-communication-ssl-tls-go-restful-api-128eb6b54f1f

Resources