I won't mince words: There is an epidemic of PHP software that disables certificate validation when using cURL, and this is (in most cases) harmful.
Let's talk about what the dangers are, how to do it correctly, and a tool we built to make doing the right thing easier.
A Brief Recap on Certificate Validation with cURL
Most security professionals already know this, but just in case... if you see either of the following two snippets of code in a PHP file that uses the cURL extension, here be dragons:
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
What Happens If You Disable CURLOPT_SSL_VERIFYHOST?
Imagine you're a webserver connecting to an Authorize.net gateway to send credit card details in order to process a payment, so you can fulfill an order for the customer.
The server responds with a valid TLS certificate signed by a trusted Certificate Authority. But you were trying to connect to authorize.example.com
and the certificate is for authorize-example-com.obvious-phishing-site.cx
.
Do you still connect? If you disabled CURLOPT_SSL_VERIFYHOST
, the answer is sadly "Yes".
Always set
CURLOPT_SSL_VERIFYHOST
to2
(which is the default value). No excuses.
What Happens If You Disable CURLOPT_SSL_VERIFYPEER?
If you disable this check, you're opting out of the Certificate Authority infrastructure, which means you've elected to blindly accept self-signed certificates.
This exposes you to extremely trivial man-in-the-middle attacks. All the intercepting proxy needs to do is offer a self-signed certificate and PHP will just trust it, but only if you turn this off.
In today's ecosystem, the only real reason to use this is if you're using CURLOPT_PINNEDPUBLICKEY and for some reason can't use LetsEncrypt.
Unless you prefer certificate pinning, always set
CURLOPT_SSL_VERIFYPEER
toTRUE
.
Why Would Anyone Choose to Do Differently?
The funny thing about Best Practices is that they're really obvious when spelled out in plain and simple language.
In order to better prevent this at an ecosystem level, rather than simply shame projects that commit these mistakes, it's better to stop and think about what their motivations could be and see if there's a way to solve the orthogonal challenges that the developers are facing that lead to insecure code.
Keeping in mind that CURLOPT_SSL_VERIFYPEER
requires a bundle of Certificate Authorities' certificates (called a CA-Cert bundle), a very reasonable hypothesis for why these mistakes happen emerges:
- On some systems, there is either no CA-Cert bundle available, because either:
- It's not world-readable.
- It's in a non-standard location that the PHP script can't possibly know about.
- The web application runs inside a chroot jail, and the CA-Cert bundle isn't exposed to it.
- (Other similar complications.)
- The developers of the software do not have a strong incentive to spend a lot of time dealing with troubleshooting other people's server configurations.
- If you just disable it, the code "just works". If you enable it, you get a non-lucrative support headache every now and then.
There are other possible answers ("I copied it from StackOverflow 5 years ago and I'm too lazy to fix it" comes to mind), but this is plausible and gives us a way forward. There are several different approaches we can take. Let's get the obviously-flawed ones out of the way first.
Why not just bundle a static cacert.pem file?
This is a tempting solution, but the list of trusted Certificate Authority certificates changes every few months.
Unless you are actively maintaining your cacert.pem file, and your users are applying your updates in a timely manner, the odds of systems continuing to trust a revoked CA certificate is extremely worrisome.
Why not just download the cacert.pem bundle on-the-fly from haxx.se?
Now we have many more obvious problems:
- How do we validate the first hop without a pre-existing CA-Cert bundle?
- We have just made haxx.se a single point of failure for everyone who uses the software.
- We risk overloading the haxx.se server, which is not cool.
- The cacert.pem file does not have an accompanying digital signature.
Of course, we built a tool that solves these problems.
Introducing Certainty
Certainty is a new open source software library that guarantees that your users will have a valid and up-to-date cacert.pem file, even if their webserver is misconfigured and they don't update their own software regularly.
Certainty ships with the latest CACert bundles, their SHA256 checksums (to cross-correlate with the checksums provided by the cURL developer), and an Ed25519 signature provided by Paragon Initiative Enterprises. Certainty includes a mechanism for downloading the latest CACert from the Internet and caching these files locally.
Installing Certainty
Simply add paragonie/certainty
to your composer.json requirements and run composer update
.
Using Certainty
You can use Certainty directly in your software like so:
<?php
use ParagonIE\Certainty\RemoteFetch;
/* ~8<~8<~8<~8<~8<~8<~8<~8<~ - S N I P - ~8<~8<~8<~8<~8<~8<~8<~8<~ */
$ch = curl_init();
/* ~8<~8<~8<~8<~8<~8<~8<~8<~ - S N I P - ~8<~8<~8<~8<~8<~8<~8<~8<~ */
$latestBundle = (new RemoteFetch())->getLatestBundle();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO, $latestBundle->getFilePath());
/* ~8<~8<~8<~8<~8<~8<~8<~8<~ - S N I P - ~8<~8<~8<~8<~8<~8<~8<~8<~ */
$response = curl_exec($ch);
This will cache the latest results for a reasonable time (by default, 24 hours). After this time period has passed, the metadata file is redownloaded (along with any fresher CA-Cert bundles).
The getLatestBundle()
method will return the most recent available cacert file that has a valid sha256sum and Ed25519 signature.
Alternative Use-Case
The simplest usage is to run the following command:
vendor/bin/certainty-cert-symlink /path/to/destination/for/cacert.pem
What this does is create a symbolic link at /path/to/destination/for/cacert.pem
pointing to the most recent verified CA-Cert bundle. Your code is simpler, but you don't get the benefit of automatic updates:
<?php
$ch = curl_init();
/* ~8<~8<~8<~8<~8<~8<~8<~8<~ - S N I P - ~8<~8<~8<~8<~8<~8<~8<~8<~ */
# THIS IS NOT RECOMMENDED OVER THE PREVIOUS EXAMPLE!
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/destination/for/cacert.pem');
/* ~8<~8<~8<~8<~8<~8<~8<~8<~ - S N I P - ~8<~8<~8<~8<~8<~8<~8<~8<~ */
$response = curl_exec($ch);
Advanced: Custom CA Support
Some Enterprise users may desire the capability of automating their own in-house Certificate Authorities updates in addition to the official trusted bundle list.
Certainty already includes support for managing custom Certificate Authorities. As of this writing, these features are not yet documented in full.
The solution involves using the LocalCACertBuilder class to append your certificates to a derivative CA-Cert chain, providing your users with a custom Validator, and then publishing your own ca-certs.json
metadata file.
Why Should Our Software Project Use Certainty?
- You can guarantee that the CA-Cert file is available on every system that uses your software, thereby allowing you to safely do the right thing and validate TLS certificates.
- When used as directed, the CA-Cert bundles will reliably be updated on all your users' systems without any intervention from you or them.
- CA-Cert bundles are Ed25519-signed by our team, and we take security seriously.
- We include simultaneous support for Custom CAs and the latest CA-Cert bundle.
Certainty is available on Github under the extremely permissive ISC License.