TLS Certificates on macOS 10.15 Catalina and iOS 13

My first week at my current job I ran into a puzzling issue. We have a script that sets up a local development environment and as part of standing up local instances of all of our services it creates self-signed TLS certificates so you can connect to your local instances via TLS just like you would in production. We use hostnames under the .test TLD which is explicitly reserved by the IETF for testing use. These certificates all worked fine when accessing the services via Chrome but when I attempted to use some code that used Apple’s native Security.framework for certificate verification it failed. Sure enough accessing the services in Safari failed the same way.

[broken lock] This Connection Is Not Private
This website may be impersonating "www.service.test" to steal your personal or financial information. You should go back to the previous page.
[button labeled Go Back]

Safari warns you when a website has a certificate that is not valid. This may happen if the website is misconfigured or an attacker has compromised your connection.

To learn more you can view the certificate. If you understand the risks involved you can visit this website.

Some brief research quickly turned up this Apple support page describing some new requirements for TLS server certificates in macOS 10.15 and iOS 13. It calls out a minimum RSA key size (2048 bits), hash algorithm requirements (SHA-1 is deprecated), and making the Subject Alternative Name extension a requirement (the CommonName field is no longer trusted). It also has two additional requirements for certificates issued after July 1, 2019: the ExtendedKeyUsage extension must be present and contain the id-kp-serverAuth OID, and also the validity period of the certificate must be 825 days or fewer. The script we were using to create certificates called openssl and looked like:

openssl req \
  -new \
  -newkey rsa:2048 \
  -sha512 \
  -days 3650 \
  -nodes \
  -x509 \
  -keyout service.ssl.key \
  -out service.ssl.crt \
  -config service.test.ssl.cnf

The .cnf file is a template that looked like:

  [req]
  distinguished_name = req_distinguished_name
  x509_extensions = v3_req
  prompt = no
  [req_distinguished_name]
  CN = *.service.test
  [v3_req]
  keyUsage = keyEncipherment, dataEncipherment, cRLSign, keyCertSign
  extendedKeyUsage = serverAuth
  subjectAltName = @alt_names
  subjectKeyIdentifier = hash
  authorityKeyIdentifier = keyid:always,issuer:always
  basicConstraints = CA:true
  [alt_names]
  DNS.1 = *.service.test
  DNS.2 = service.test

Right away I knew that that -days 3650 option was a problem. Since I had run this script after July 1, 2019 macOS was not going to accept a 10 year validity period for these certificates! Aside from that it looked like we had our ducks in a row which was promising. I tried lowering that value and regenerating the certificates but Safari still wouldn’t accept them! (Incidentally if you know how to get Safari to provide more details on why it does not trust a TLS certificate please let me know! It’s a pretty infuriating process.)

Our environment setup scripts did attempt to ensure that the certificates were trusted—they ran the command security verify-cert -L -c service.ssl.crt to ask Security.framework whether the certificate was trusted. Indeed, running this command manually showed that it printed …certificate verification successful. One of my colleagues suggested that we should perhaps switch to using mkcert which aimed to encapsulate all this complexity. I installed it and gave it a go, generating a server certificate with the same set of DNS names. mkcert creates a separate CA certificate and then uses that to sign the server certificates it generates (which is generally more sensible than the all-in-one approach we had been using). When I tried accessing my local service in Safari it still gave me the same error!

I was beginning to think that I would never get things working until I stumbled upon this closed issue on the mkcert GitHub repository. It turns out that in addition to the other requirements macOS will also refuse to honor a wildcard hostname match for certs with a .test TLD. I hadn’t considered this at all but the certificate was being generated with subject alt names for *.service.test and service.test but I was accessing it at www.service.test. I tried generating the certificate again but this time added www.service.test as a subject alt name alongside the other two. Sure enough, now it worked! The change to mkcert turns out to be unnecessary, explicitly adding the subdomains we use to the certificates is enough to make them work. I am working on changing things to use mkcert anyway to hopefully wrap some of the complexity here in the future.

Addendum

After the fact I realized one key mistake—we were not in fact using security verify-cert properly! Looking at the security man page showed me that instead of a path to a certificate file you can provide it an https URL and it will fetch the certificate by starting a TLS connection and attempt to validate it. Running the command that way against a wildcard certificate does indeed fail validation:

$ security verify-cert -L https://www.service.test:4443
Cert Verify Result: Host name mismatch