{"id":34,"date":"2020-02-17T17:57:33","date_gmt":"2020-02-17T17:57:33","guid":{"rendered":"https:\/\/ted.mielczarek.org\/blog\/?p=34"},"modified":"2020-02-17T17:57:33","modified_gmt":"2020-02-17T17:57:33","slug":"tls-certificates-on-macos-10-15-catalina-and-ios-13","status":"publish","type":"post","link":"https:\/\/ted.mielczarek.org\/blog\/2020\/02\/17\/tls-certificates-on-macos-10-15-catalina-and-ios-13\/","title":{"rendered":"TLS Certificates on macOS 10.15 Catalina and iOS 13"},"content":{"rendered":"\n<p>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 <abbr title=\"Transport Layer Security\">TLS<\/abbr> certificates so you can connect to your local instances via TLS just like you would in production. We use hostnames under the <code>.test<\/code> <abbr title=\"top-level domain\">TLD<\/abbr> which is <a href=\"https:\/\/en.wikipedia.org\/wiki\/.test\">explicitly reserved<\/a> by the <abbr title=\"Internet Engineering Task Force\">IETF<\/abbr> 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&#8217;s native <code>Security.framework<\/code> for certificate verification it failed. Sure enough accessing the services in Safari failed the same way. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"692\" height=\"335\" src=\"https:\/\/i0.wp.com\/ted.mielczarek.org\/blog\/wp-content\/uploads\/2020\/02\/Screen-Shot-2020-02-17-at-12.18.42-PM.png?resize=692%2C335&#038;ssl=1\" alt=\"[broken lock] This Connection Is Not Private\nThis website may be impersonating &quot;www.service.test&quot; to steal your personal or financial information. You should go back to the previous page.\n[button labeled Go Back]\n\nSafari 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.\n\nTo learn more you can view the certificate. If you understand the risks involved you can visit this website.\" class=\"wp-image-35\" srcset=\"https:\/\/i0.wp.com\/ted.mielczarek.org\/blog\/wp-content\/uploads\/2020\/02\/Screen-Shot-2020-02-17-at-12.18.42-PM.png?w=692&amp;ssl=1 692w, https:\/\/i0.wp.com\/ted.mielczarek.org\/blog\/wp-content\/uploads\/2020\/02\/Screen-Shot-2020-02-17-at-12.18.42-PM.png?resize=300%2C145&amp;ssl=1 300w\" sizes=\"auto, (max-width: 692px) 100vw, 692px\" \/><\/figure>\n\n\n\n<p>Some brief research quickly turned up <a href=\"https:\/\/support.apple.com\/en-us\/HT210176\">this Apple support page<\/a> 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 <code>CommonName<\/code> field is no longer trusted). It also has two <em>additional<\/em> requirements for certificates issued after July 1, 2019: the <code>ExtendedKeyUsage<\/code> extension must be present and contain the <code>id-kp-serverAuth<\/code> OID, and also the validity period of the certificate must be 825 days or fewer. The script we were using to create certificates called <code>openssl<\/code> and looked like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">openssl req \\\n  -new \\\n  -newkey rsa:2048 \\\n  -sha512 \\\n  -days 3650 \\\n  -nodes \\\n  -x509 \\\n  -keyout service.ssl.key \\\n  -out service.ssl.crt \\\n  -config service.test.ssl.cnf<\/code><\/pre>\n\n\n\n<p>The .cnf file is a template that looked like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">  [req]\n  distinguished_name = req_distinguished_name\n  x509_extensions = v3_req\n  prompt = no\n  [req_distinguished_name]\n  CN = *.service.test\n  [v3_req]\n  keyUsage = keyEncipherment, dataEncipherment, cRLSign, keyCertSign\n  extendedKeyUsage = serverAuth\n  subjectAltName = @alt_names\n  subjectKeyIdentifier = hash\n  authorityKeyIdentifier = keyid:always,issuer:always\n  basicConstraints = CA:true\n  [alt_names]\n  DNS.1 = *.service.test\n  DNS.2 = service.test<\/code><\/pre>\n\n\n\n<p>Right away I knew that that <code>-days 3650<\/code> 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&#8217;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&#8217;s a pretty infuriating process.)<\/p>\n\n\n\n<p>Our environment setup scripts did attempt to ensure that the certificates were trusted&#8212;they ran the command <code>security verify-cert -L -c service.ssl.crt<\/code> to ask <code>Security.framework<\/code> whether the certificate was trusted. Indeed, running this command manually showed that it printed <code>\u2026certificate verification successful.<\/code> One of my colleagues suggested that we should perhaps switch to using <a href=\"https:\/\/github.com\/FiloSottile\/mkcert\/\">mkcert<\/a> 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. <code>mkcert<\/code> 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!<\/p>\n\n\n\n<p>I was beginning to think that I would never get things working until I stumbled upon <a href=\"https:\/\/github.com\/FiloSottile\/mkcert\/issues\/206\">this closed issue<\/a> 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 <code>.test<\/code> TLD. I hadn&#8217;t considered this at all but the certificate was being generated with subject alt names for <code>*.service.test<\/code> and <code>service.test<\/code> but I was accessing it at <code>www.service.test<\/code>. I tried generating the certificate again but this time added <code>www.service.test<\/code> 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Addendum<\/h2>\n\n\n\n<p>After the fact I realized one key mistake&#8212;we were not in fact using <code>security verify-cert<\/code> properly! Looking at the <code>security<\/code> 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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">$ security verify-cert -L https:\/\/www.service.test:4443\nCert Verify Result: Host name mismatch<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[7,5,6],"class_list":["post-34","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-macos","tag-ssl","tag-tls"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/posts\/34","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/comments?post=34"}],"version-history":[{"count":1,"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/posts\/34\/revisions"}],"predecessor-version":[{"id":36,"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/posts\/34\/revisions\/36"}],"wp:attachment":[{"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/media?parent=34"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/categories?post=34"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ted.mielczarek.org\/blog\/wp-json\/wp\/v2\/tags?post=34"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}