SSL and PHP streams - Part 1: You are Doing It Wrong™
Come in and take a look inside the venditan hq!
We're recruiting php people, interested?Show Me More
The upcoming PHP 5.6 release brings with it a number of improvements to encrypted PHP streams, both internally and externally. In these articles I will try to cover the most important changes, and how they affect your code.
This article will focus on how to get the best security level in code that needs to be run on PHP versions below 5.6, and highlighting some of the gaps in the currently available functionality. Version 5.4.13 is the earliest version that supports all the options described below - and if you are running something earlier than this, then you really should consider upgrading to at least the latest version of the 5.4 series 1.
PHP provides some very simple APIs to use encrypted connections for client communication. It lets you make an encrypted HTTP request to retrieve some data with just one line of code:
<?php $data = file_get_contents('https://example.com/file.ext');
Great! We just retrieved some data from a remote server in completely secure manner, right? Wrong.
Problem 1: Peer verification
One of the most important parts of using encrypted connections is verifying that the remote peer you are communicating with is really who they say they are, and who you are expecting them to be. Without this crucial step, man-in-the-middle (MITM) attacks are trivial, and even though the data arrives on your machine encrypted it could have been stolen or altered by a 3rd party along the way.
If you've ever tried to use the cURL extension to retrieve an HTTPS resource, chances are you've seen this message:
SSL certificate problem, verify that the CA cert is OK. Details: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
That's because cURL attempts to verify and validate the certificate presented by the server and fails to do so, usually because it cannot be traced back to a trusted root certificate authority (CA). Often this is because the root certificate authority list is not installed or not correctly configured. A little research will then lead you to configure the `curl.cainfo` setting with a valid CA file, and your code will now work as expected.
But what about streams? If you make the same request using streams it works without complaining, so that's clearly a better option - except it isn't, because before PHP 5.6, PHP streams do not attempt to verify the peer certificate by default! This doesn't mean the streams are fundamentally insecure, it just means you need to use stream context options to make them secure.
Let's update our code to verify the peer certificate against a list of trusted root certificate authorities:
<?php $contextOptions = [ 'ssl' => [ 'verify_peer' => true, 'cafile' => '/path/to/cafile.pem', 'CN_match' => 'example.com', ] ]; $context = stream_context_create($contextOptions); $data = file_get_contents('https://example.com/file.ext', false, $context);
This allows us to verify the server's certificate against a trusted CA chain. Setting `verify_peer` to `true` instructs PHP to perform the verification process, and the `cafile` option supplies the trusted CA data to verify against. This can also be specified using the `capath` option, which allows you to store the trusted certificates in separate files in the specified directory. More details of how this needs to be formatted are available at openssl.org.
We must also specify the expected peer name on the presented certificate with the `CN_match` option, it will not be inferred from the URL and if it is not specified, the name on the certificate will not be validated. It must match the Common Name field of the certificate, the Subject Alternative Names field will not be considered - this is a pretty major limitation on the modern internet.
There is currently no mechanism for verifying a certificate fingerprint.
Problem 2: Cipher lists
There are many different ways to encrypt data - many different algorithms with many different sub-variants. PHP simply lets OpenSSL deal with this problem, by specifying `DEFAULT` as the list of acceptable algorithms (cipher list). Unfortunately, this list allows almost anything, including a number of very weak ciphers that can be easily broken by a determined attacker, allowing them to steal data.
The `ciphers` context option allows us to specify a list of acceptable ciphers to use with the connection - if one of these cannot be negotiated by both server and client, the connection will fail before any potentially sensitive data is transmitted. Let's update our code so specify that only `HIGH` encryption cipher suites may be used 2:
<?php $contextOptions = [ 'ssl' => [ 'verify_peer' => true, 'cafile' => '/path/to/cafile.pem', 'CN_match' => 'example.com', 'ciphers' => 'HIGH', ], ]; $context = stream_context_create($contextOptions); $data = file_get_contents('https://example.com/file.ext', false, $context);
Problem 3: Protocol support
PHP streams support SSL versions 2 and 3, and TLS version 1.0 3. Moreover, it's not possible to directly specify which protocol will be used with an `https://` or `ftps://` connection. SSLv2 is very broken, and SSLv3 is also less than desirable. On the modern internet, there are very few servers that do not support at least TLS version 1.0.
While it's not currently possible to directly specify which protocol to use, it is possible to forbid them using the cipher list, so lets update our code to require TLS:
<?php $contextOptions = [ 'ssl' => [ 'verify_peer' => true, 'cafile' => '/path/to/cafile.pem', 'CN_match' => 'example.com', 'ciphers' => 'HIGH:!SSLv2:!SSLv3', ], ]; $context = stream_context_create($contextOptions); $data = file_get_contents('https://example.com/file.ext', false, $context);
When creating raw socket streams it is possible to directly specify the protocol to use, using either the approiate URI scheme (`sslv2://`, `sslv3://` or `tls://`) when creating the socket, or by using the appropriate constant when enabling encryption on an existing TCP socket via `stream_socket_enable_crypto()`.
Even so, our code does not support the more modern TLS version 1.1 and 1.2 protocols, which contain a number of security improvements over TLS version 1.0. Luckily, in practice, these security improvements are largely theoretical - the known attacks against TLSv1.0 can and do work, but they are currently impractical to execute in real life applications without a level of system compromise that would render the attack pointless - the attacker can already steal your data much more easily by this point.
Problem 4: TLS compression attack vulnerability
The CRIME attack vector against TLS can be easily mitigated by disabling protocol-level compression. Unfortunately, in PHP it's enabled by default 4. Luckily, since PHP 5.4.13, it can be easily disabled using a simple boolean context option `disable_compression`. Let's update our code to use it:
<?php $contextOptions = [ 'ssl' => [ 'verify_peer' => true, 'cafile' => '/path/to/cafile.pem', 'CN_match' => 'example.com', 'ciphers' => 'HIGH:!SSLv2:!SSLv3', 'disable_compression' => true, ], ]; $context = stream_context_create($contextOptions); $data = file_get_contents('https://example.com/file.ext', false, $context);
Using PHP for managing server streams is less common, but it is possible and has been for a long time. Problems 2, 3 and 4 described above also apply to server streams (problem 1 doesn't come into play unless you require a client certificate) but there are other issues with server streams that are currently not resolvable in PHP.
Problem 5: Cipher order
Some attack vectors are only possible if certain ciphers are used. Currently PHP instructs OpenSSL to use the cipher priorities specified by the client, potentially leaving the server open to attack.
Problem 6: TLS renegotiation attacks
As problems go, this is a big one. SSL version 3 and all versions of TLS allow renegotiation of the connection settings after it has been created - changing protocol and cipher lists. This process is vulnerable to a limited MitM attack (an attacker can inject data but cannot see the response) and it also opens a denial-of-service (DoS) flaw on the server.
Negotiating a secure connection considerably more expensive for the server than it is for the client in terms of CPU cycles, meaning that anyone with a laptop can bring your super-powerful server down by simply repeatedly renegotiating the connection. Moreover, this potential DoS attack is very difficult to detect on the edge firewall or the server firewall, because it only requires a single TCP connection, and does not create excessive traffic on that link.
At present, PHP provides no way to disable renegotiation, or limit the rate at which renegotiation requests will be honoured.
Using secure connections in PHP streams is not as simple as it appears on the surface. Our original "one line" of code is now a complex set of options. It's harder to read, and it's impossible to write it correctly without understanding precisely what the various options do.
In order to obtain an acceptable level of security, the user is required to understand some of the technical elements of the cryptography they are attempting to use. This can be a daunting, and as result many applications are deployed using insecure settings - some of them so bad as to almost completely negate the expense of use encryption in the first place.
In the next article, we'll look at how some of the changes in PHP 5.6 will make life a lot simpler for the average PHP developer to create truly secure communication routines.
It is important to note that the code samples from the article do not represent the one true "correct" way to make your arbitrary HTTPS request. You *must* understand the implications of these options before you use them.
1 At the time or writing, the current release in the 5.4 branch is 5.4.29. This release, along with the 5.5.13 sister release, contain a behavioural regression with unserialising strings which breaks (amongst other things) elements of PHPUnit and Doctrine. It's probably best to avoid these specific releases, 5.4.30 and 5.5.14 should "fix the fix".
2 This is not a recommendation, merely an example, although it's not a bad jumping-off point.
3 This is not completely true, but I'll cover the specifics of this in a later article.
4 This does not necessarily mean that a given connection will be compressed, as the server can also refuse to honour the client's request to compress the data.