Skip to main content

PHP sockets: Fix "Unable to complete TLS handshake" with mkcert local development certificates

Published on

I use DDEV-Local for my local development stack. DDEV leverages mkcert for trusted local development certificates. The mkcert tool has been a missing component in my local development stack for a long time. And, the best part, it has worked without any problems. Until this week. My coworker said that a script I wrote was not working – it kept failing saying that the remote certificate could not be validated. However, cURL had no complaints, nor did any web browser. I chalked it up as "works on my machine 🤷‍♂️." Until today 😬.

I was working on the follow up to my blog about using ReactPHP to consume an HTTP API. I was taking the resulting data and creating entities on a Drupal site. I point the react/http client to https://drupex.ddev.site... and got an unexpected error.

Connection to drupex.ddev.site:443 failed during TLS handshake: Unable to complete TLS handshake: SSL operation failed with code 1. OpenSSL Error messages: error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed

Now, why did this work before on my other machine? I was using PHP's built-in webserver. I wasn't using a secure connection locally. My coworker was. Just like him, I had no issues connecting via cURL or wget or a browser.

To be safe, I ran the mkcert installer again

~ » mkcert -install                                                                                                                                                                                                 
Using the local CA at "/Users/mglaman/Library/Application Support/mkcert" ✨
The local CA is already installed in the system trust store! 👍
The local CA is already installed in the Firefox trust store! 👍

🙌 All right, it is already installed in the system trust store. Maybe I just needed to rerun it? Nope. Still nothing. I flexed my search-fu on Google and DuckDuckGo to no avail. So I asked on the Twitterverse and began digging.

I looked into the react/socket code and saw it was using OpenSSL functions. This must have meant that the problem was OpenSSL was not aware of the mkcert certificate authority installed on my machine. So I checked the OpenSSL configuration for my PHP install.

~ » php -i | grep openssl

openssl
Openssl default config => /usr/local/etc/[email protected]/openssl.cnf
openssl.cafile => /usr/local/etc/[email protected]/cert.pem => /usr/local/etc/[email protected]/cert.pem
openssl.capath => /usr/local/etc/[email protected]/certs => /usr/local/etc/[email protected]/certs

I'm on macOS with Homebrew installs of OpenSSL and PHP. That's why you see [email protected]. From the PHP documentation, the capath configuration operation is for:

If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory.

So, I need to symlink my mkcert CA into the capath for OpenSSL!

 mkdir /usr/local/etc/[email protected]/certs
ln -s "$(mkcert -CAROOT)/rootCA.pem"  /usr/local/etc/[email protected]/certs

The quotes are important, as for macOS they install to /Users/{user}/Library/Application Support/mkcert and the space must be escaped.

Filippo Valsorda, creator of mkcert, managed to find my tweet, and it sounds like he will be adding a fix to mkcert. I opened an issue for good measure, if you would like to follow along.