Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to read TLS certificates websockets using PHP?

I am trying to connect to a secure websocket created by PHP, but for some reason it doesn't work. The certificate files are readable for PHP.

This is my code so far (PHP side; stripped down code for simplicity):

$context = stream_context_create();
stream_context_set_option($context, 'ssl', 'allow_self_signed', false);
stream_context_set_option($context, 'ssl', 'verify_peer', true);
stream_context_set_option($context,  'ssl', 'peer_name', 'example.com');
stream_context_set_option($context,  'ssl', 'CN_match', 'example.com');
stream_context_set_option($context,  'ssl', 'SNI_enabled', true);
stream_context_set_option($context, 'ssl', 'local_cert',  '/path/to/ssl/cert/example.com');
stream_context_set_option($context, 'ssl', 'local_pk', '/path/to/ssl/private/example.com');

$serverSocket = stream_socket_server(
        'tls://example.com:8090',
        $errNo,
        $errStr,
        \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
        $context
);
$client = stream_socket_accept($serverSocket);

// send initial websocket connection stuff
$request = socket_read($client, 5000);
preg_match('#Sec-WebSocket-Key: (.*)\r\n#', $request, $matches);
$key = base64_encode(pack(
    'H*',
    sha1($matches[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
));
$headers = "HTTP/1.1 101 Switching Protocols\r\n";
$headers .= "Upgrade: websocket\r\n";
$headers .= "Connection: Upgrade\r\n";
$headers .= 'Sec-WebSocket-Version: 13' . "\r\n";
$headers .= 'Sec-WebSocket-Accept: ' . $key . "\r\n\r\n";
socket_write($client, $headers, \mb_strlen($headers));

// do something here...

socket_close($client);
socket_close($serverSocket);

The client side:

var con = new WebSocket('wss://' + host + ':' + port);
var $chat = $('#chat');

con.onmessage = function(e) {
    $chat.append('<p>' + e.data + '</p>');
};

con.onopen = function(e) {
    con.send('Hello Me!');
};

con.onclose = function (e) {
    console.log('connection closed.', arguments);
}

I do not have any *.pem file. Just the two files, that are used in apache web server. It would be possible to convert that files into a pem file, if needed. But I think it should also work in php with these both files, shouldn't it?

For a better testing, we are using an isolated subdomain with an let's encrypt certificate. Because we got full access to this server. However, from the certifacte generator I got only those two mentioned files. For the web server it works perfectly. But how to do the same for a websocket in php?

Now, with this code, after sending some messages to the client the server script tells me, that it was not able to get the peer from the certificates. I don't know what this message means and how to fix that. I also alreay tried to swap local_cert and local_pk with each other, but it hasn't helped anyway.

Edit: After researching a while, it turns out, that php fails with every combination with another error.

My generated certificate files look like this:

File /opt/psa/var/certificates/cert-0x8zHR:

-----BEGIN CERTIFICATE REQUEST-----
some letters
-----END CERTIFICATE REQUEST-----

-----BEGIN PRIVATE KEY-----
some letters
-----END PRIVATE KEY-----

-----BEGIN CERTIFICATE-----
some letters
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
some letters
-----END CERTIFICATE-----

File /opt/psa/var/certificates/cert-Du9H8N:

-----BEGIN CERTIFICATE-----
some letters
-----END CERTIFICATE-----

All lines from the certificate strings from both of them end at character position 65.

As you can read in php documentation, php expects to get a file in PEM format: http://php.net/manual/en/context.ssl.php#context.ssl.local-cert

I found this tutorial to convert my two files into one PEM file, but my certificates look different to mentioned on the site and also I don't know what to exactly include into the PEM file, that php needs: https://www.digicert.com/ssl-support/pem-ssl-creation.htm

Edit 2: As mentioned below, here are the exact erros I get:

with

stream_context_set_option($cont, 'ssl',  'local_pk', '/opt/psa/var/certificates/cert-0x8zHR');
stream_context_set_option($cont, 'ssl', 'local_cert', '/opt/psa/var/certificates/cert-Du9H8N');

Warning: stream_socket_accept(): Unable to set private key file `/opt/psa/var/certificates/cert-0x8zHR' in ... on line ...

Warning: stream_socket_accept(): Failed to enable crypto in ... on line ...

Warning: stream_socket_accept(): accept failed: Success in ... on line ...

Warning: socket_read() expects parameter 1 to be resource, boolean given in ... on line ...

Notice: Undefined offset: 1 in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_close() expects parameter 1 to be resource, boolean given in ... on line ...

And with

stream_context_set_option($cont, 'ssl',  'local_cert', '/opt/psa/var/certificates/cert-0x8zHR');
stream_context_set_option($cont, 'ssl', 'local_pk', '/opt/psa/var/certificates/cert-Du9H8N');

Warning: stream_socket_accept(): Unable to set private key file `/opt/psa/var/certificates/cert-Du9H8N' in ... on line ...

Warning: stream_socket_accept(): Failed to enable crypto in ... on line ...

Warning: stream_socket_accept(): accept failed: Success in ... on line ...

Warning: socket_read() expects parameter 1 to be resource, boolean given in ... on line ...

Notice: Undefined offset: 1 in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_close() expects parameter 1 to be resource, boolean given in ... on line ...

And with (just a concatenation of cert-0x8zHR with cert-Du9H8N)

$file = dirname(__FILE__, 3) . \DIRECTORY_SEPARATOR . 'fullchain.pem';
stream_context_set_option($cont, 'ssl', 'local_cert', $file);

Warning: stream_socket_accept(): Could not get peer certificate in ... ...

Warning: stream_socket_accept(): Failed to enable crypto in ... ...

Warning: stream_socket_accept(): accept failed: Success in ... ...

Warning: socket_read() expects parameter 1 to be resource, boolean given in ... on line ...

Notice: Undefined offset: 1 in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... on line ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... ...

Warning: socket_write() expects parameter 1 to be resource, boolean given in ... ...

Warning: socket_close() expects parameter 1 to be resource, boolean given in ... on line ...

Edit 3: Yes, indeed, there is an openssl error. After the third socket_stream_accept warning I now get this error by just using the code from the php doc example:

error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch

I searched for this error in the internet. They say, if this error appears, I have chosen the wrong certificate file.

Also, if I play this command:

openssl x509 -noout -in cert-0x8zHR -modulus

I got two different strings for the modulus. But I don't know why, neither how to fix this. These both files are used in apache web server vhost config and it works fine:

SSLEngine on
SSLVerifyClient none
SSLCertificateFile /opt/psa/var/certificates/cert-0x8zHR
SSLCACertificateFile /opt/psa/var/certificates/cert-Du9H8N

PS: If you know any tool for local conversion to a pem file, please let me know. Online conversion is impossible.

like image 952
alpham8 Avatar asked May 11 '18 08:05

alpham8


Video Answer


1 Answers

You said:

My generated certificate files look like this:

File /opt/psa/var/certificates/cert-0x8zHR:

-----BEGIN CERTIFICATE REQUEST----- some letters -----END CERTIFICATE REQUEST-----

-----BEGIN PRIVATE KEY----- some letters -----END PRIVATE KEY-----

-----BEGIN CERTIFICATE----- some letters -----END CERTIFICATE-----

-----BEGIN CERTIFICATE----- some letters -----END CERTIFICATE-----

This is wrong on so many levels.

First the "CERTIFICATE REQUEST" part is completely useless as soon as you get the certificate. You can just ignore this part.

Now copy the "PRIVATE KEY" part, with headers in one file. This is your private key, you can use everywhere you have options "local_pk" or software asking for a key.

Then you have two certificates. And another one in another file. You will need to sort all of this. If you had given the real certificates content (which are public content) people could have help you better. Here with just "some letters" we can only guess.

In the above file the two certificates may be the CA and intermediate. You should copy them both to another file, and this will correspond to the "ca_cert" option.

I guess that the second file is your try certificate (only a guess again, without your details). Use it everywhere you have "local_cert" or asking for a certificate. This certificate should match the private key file (you can not verify that by hand, you need tooling).

Once you created all the correct files, the first one is not to be used anymore. I would recommend using better semantics for the file name because cert-X and cert-Y will be quite difficult. Use instead the website name so that you have files like www.example.com.cert, www.example.com-ca.cert and www.example.com.key, so that it is immediately clear what is what. Or with a directory called www.example.com/, and then cert.pem, ca.pem and key.pem inside the directory. The extensions by themselves are not used by the software and irrespective to the content, it is only up to you to define things that make sense.

So first sort all of this, so that your question is clearer. Right now it seems you are trying stuff blindly until coming to something that works, which is not the ideal situation in the realm of security and TLS stuff.

If possible I would also encourage you to try using higher level abstraction library for TLS handling, as this is clearly far too low, exposing so many options that you are getting lost.

like image 103
Patrick Mevzek Avatar answered Sep 19 '22 00:09

Patrick Mevzek