OpenSSL s_client vs PHP stream_socket_client
We, as developers, craft bespoke apps that connect to remote servers, over encrypted connections. Those connections are authenticated using API keys, username and passwords or public certificate and private keys. Sometimes, those connections work out of the box, sometimes we’ll spend 6 hours figuring out what’s wrong.
The easiest way to establish a plain text connection from the terminal to a server, is to use telnet
telnet google.com 80
then issue a HTTP command like GET /
and you’ll get Google’s homepage, which will be a redirect to a HTTPS version of the same page.
To establish a SSL connection, you can use the openssl
Swiss knife.
openssl s_client -connect google.com:443
and then issue the same HTTP command GET /
and you’ll get the HTTPS version of the Google homepage.
OpenSSL can be used to open a connection that requires certificate authentication too, just supply those as CLI options. This way you can test that the Apple Push Notification connectivity, by using your developer certificate and private key. Just make sure that they are converted to the PEM file format first.
openssl s_client -connect gw.example.com:1234 -cert example.com.cert -key example.com.key -CAfile example.com.cacert -showcerts -state
After you’ve figured out which developer certificate and which private key are still active and match your remote server, you can establish the same connection from PHP.
$opts = array(
'ssl' => array(
'local_cert' => 'example.com.cert',
'local_pk' => 'example.com.key',
'cafile' => 'example.cacert',
'verify_peer' => true,
)
);
$timeout = 160;
$host = "ssl://gw.example.com:1234";
echo "Connecting\n";
$context = stream_context_create($opts);
$socket = stream_socket_client (
$host, $errno, $errstr, $timeout,
STREAM_CLIENT_CONNECT, $context);
if (!$socket) {
echo "Failure $errno errstr $errstr.\n";
} else {
echo "Success\n";
}
In most of the cases, after running this script, the output will be Success
.
However, if you’re unlucky, you’ll get the following output
Connecting
PHP Warning: stream_socket_client(): SSL operation failed with code 1. OpenSSL Error messages:
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed in /path/to/example.php on line 20
PHP Warning: stream_socket_client(): Failed to enable crypto in /path/to/example.php on line 20
PHP Warning: stream_socket_client(): unable to connect to ssl://gw.example.com:1234 (Unknown error) in /path/to/example.php on line 20
errno 0 errstr .
After you double check several times, you reach the conclusion that you’re using the same certificate, key and CA certificate as above. You’ll start wondering if there is a difference when the connection is made using openssl s_client
and PHP’s stream_context_create
and stream_socket_client
(or curl
).
One of the ways to get a glimpse into what each program is doing, is to use DTrace if you’re lucky enough to use one of the OSes where’s available (like MacOS X, FreeBSD or SmartOS) or to use its poor friend strace.
strace php example.php 2> example.php.log
strace openssl s_client -connect gw.example.com:1234 -cert example.com.cert -key example.com.key -CAfile example.com.cacert -showcerts -state 2> example.openssl.log
After a careful check of those file’s output, you’ll notice that OpenSSL and PHP are using two different CA stores. Open SSL’s /usr/lib/ssl/certs/
, versus PHP’s /usr/share/ca-certificates/mozilla/
. You can spot this by checking what happens before getting the stream_socket_client(): SSL operation failed with code 1.
error message:
PHP’s trace:
stat("/usr/share/ca-certificates/mozilla//6b99d060.0", 0x7ffded049c80) = -1 ENOENT (No such file or directory)
OpenSSL’s trace:
stat("/usr/lib/ssl/certs/6b99d060.0", {st_mode=S_IFREG|0644, st_size=1643, ...}) = 0
open("/usr/lib/ssl/certs/6b99d060.0", O_RDONLY) = 4
After figuring this little difference, it’s pretty easy to fix the PHP code. Change the stream_context_create
to include the new CA path:
$opts = array(
'ssl' => array(
'local_cert' => 'example.com.cert',
'local_pk' => 'example.com.key',
'cafile' => 'example.cacert',
'capath' => '/usr/lib/ssl/certs/',
'verify_peer' => true,
)
);
Tip o’ the hat to Ben Hearsum’s Python and SSL Certificate Verification article for suggesting to use strace to figure out why do we get different results, while using the same certificates.
References: