How to read TLS certificates websockets using PHP?
Asked Answered
C

3

15

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.

Culmination answered 11/5, 2018 at 8:45 Comment(3)
Please could you post the exact error you are getting?Arianearianie
@Arianearianie I just added all error messages with my different combinations I tried :-)Culmination
"some letters" is not really great to help you... the certificates are public data anyway, why not giving it? Also for the local_pk attribute you are passing it a file named cert-something, so if it is indeed a certificate, giving it as key will never work. Make sure to provide to local_pk the associated private key of your certificate.Bartizan
B
4

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.

Bartizan answered 25/5, 2018 at 21:33 Comment(6)
none of your solutions worked. As I said above this is an let`s encrypt certificate for this domain generated by its tools using plesk web interface. So, the apache config lines were also added automatically. However, the question should more be why it works in apache with this two or three lines and not in php?Culmination
What solutions? It is not clear what you tested and what errors you have. I was just telling how to make things more proper because the content of the file does not match anything making sense. Note that I know nothing about Plesk. Your first file is certainly NOT a certificate since it is a concatenation of a CSR, a private key, and 2 certificates. It is seldom a good idea to have the private key and the certificate in the same file, for security reasons.Bartizan
As to why it works more or less in one case, see Apache documentation: httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile and note this part: "Finally the end-entity certificate's private key can also be added to the certificate file instead of using a separate SSLCertificateKeyFile directive. This practice is highly discouraged. If it is used, the certificate files using such an embedded key must be configured after the certificates using a separate key file.". You should instead use SSLCertificateKeyFile but only after your fixed your first file content.Bartizan
I tried your solution to divide the certificate into the needed parts. So, I put private key in a separate file and I just used the both -- BEGIN CERTIFICATE --- blocks. This does not work. What do you mean with "the certificate files using such an embedded key must be configured after the certificates using a separate key file"?Culmination
"This does not work" is hard to troubleshoot. As for the rest it is a direct quote from Apache documentation. In all cases, mixing private keys and certificates in the same file is not recommended even in Apache configuration, so I see no benefits (and only drawbacks) to do it. But that may be constrained by Plesk. Whatever happens on your Apache configuration that you may or may not control, you could create new files separately with appropriate content and use them from your PHP application. That should make things clearer. Also see my previous comment: this TLS library seem too low levelBartizan
First of all, I cannot post the certificates here, since this is private data and can be used for attacks. However, these certificates are generated using let' s encrypt. So, if you have a server, you should get the same result, shouldn' t you? I have already used cboden/ratchet, but this is not made for my use case. Because you are there in an event loop and you cannot send messages directly to the client using this lib, you an additional lib / wrapper for doing this. Also at this libs you need to pass an array with the ssl details (key/ value pair). So, no difference on high level APIs.Culmination
A
0

It looks like OpenSSL doesn't like your key.

You could try calling http://php.net/manual/en/function.openssl-error-string.php to see if there is more info available about why?

Or try different combinations of key files, as per these possibly related questions which feature the same error:

GL

Arianearianie answered 25/5, 2018 at 13:23 Comment(1)
ok, thanks, I just added the openssl error messages and how the certificate files are used in apache.Culmination
B
0

Try this:

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

If that doesn't work, try to comment out second line (ca_flie) and set verify_peer to false

What are error messages in both cases?

Update: BTW, your certificates are already in .pem (ASCII-base64-encoded) format.

Bearing answered 25/5, 2018 at 20:54 Comment(8)
verify_peer to false is a very bad advice... defeating all security features coming from TLS.Bartizan
Not all security features, just peer verification. And it helps to pinpoint problem to CA certificate.Bearing
No because if it works people will keep it like that and never put it back, hence you loose all security (authenticity is more important than confidentiality, contrary to intuition). Have a look at cs.utexas.edu/~shmat/shmat_ccs12.pdfBartizan
There is a SSLVerifyClient none directive in Apache config of this setup, so it could be a bad or expired CA certificate.Bearing
Not relevant. His application is opening a TLS socket on 8090.Bartizan
with your solution it works until it tries to send a message to the client: Warning: fwrite(): SSL: Broken pipeCulmination
Which line? socket_write($client, $headers, \mb_strlen($headers)); ?Bearing
@Bearing Well, I changed it to fwrite, since socket_write does not work in this context. So, the line you guessed.Culmination

© 2022 - 2024 — McMap. All rights reserved.