Jump to content

Sockets and TLS


Recommended Posts

Trying to add tls sockets, but getting an error.  I noticed that the client doesn't utilize a certificate.  Will it need to?  If not specified under the reactphp script, can it be a php.ini setting?  Thanks
 
 

Server

$server = new \React\Socket\TcpServer($this->host['url'].':'.$this->host['port'], $loop);
if($this->host['tls']) {
    $server = new \React\Socket\SecureServer($server, $loop, ['local_cert' => '../sslforfree/certificate.crt']);
}
$server->on('connection', function (\React\Socket\ConnectionInterface $conn) {
});
Option 1 client
$connector = new \React\Socket\TimeoutConnector(new \React\Socket\Connector($loop), $this->settings['httpWaitTime'], $loop);
if($this->settings['socket']['tls']) {
    $connector = new \React\Socket\SecureConnector($connector, $loop, []);
}
$connector->connect($this->settings['socket']['url'].':'.$this->settings['socket']['port'])
->then(function (\React\Socket\ConnectionInterface $connection)  use (&$response, $loop, $msg) {
    // ...
})->otherwise(function(\RuntimeException $error) use(&$response, $loop){
    // ...
});
Option 2 client
$connector=new \React\Socket\Connector($loop);
$connector->connect(($this->settings['socket']['tls']?'tls://':'').$this->settings['socket']['url'].':'.$this->settings['socket']['port'])
->then(function (\React\Socket\ConnectionInterface $connection)  use (&$response, $loop, $msg) {
    // ...
})->otherwise(function(\RuntimeException $error) use(&$response, $loop){
    // ...
});
Error generated by client script
May 16 10:41:38 devserver server: No connection to server: (No connection to server: (Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): SSL operation failed with code 1. OpenSSL Error messages: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure)): (32003)
May 16 10:41:38 devserver server: ERROR in /var/www/datalogger/src/classes/Helpers.php (199): No connection to server: (No connection to server: (Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): SSL operation failed with code 1. OpenSSL Error messages: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure)): (32003)

 

Link to comment
Share on other sites

Your client needs to have a copy of the server's certificate which it can use to verify the server connection. You can specify the certificate with the cafile option.

 

<?php
$connector = new \React\Socket\TimeoutConnector(new \React\Socket\Connector($loop), $this->settings['httpWaitTime'], $loop);
if($this->settings['socket']['tls']) {
    $connector = new \React\Socket\SecureConnector($connector, $loop, [
        'cafile' => 'server.crt'
    ]);
}

$connector->connect($this->settings['socket']['url'].':'.$this->settings['socket']['port'])
->then(function (\React\Socket\ConnectionInterface $connection)  use (&$response, $loop, $msg) {
    // ...
})->otherwise(function(\RuntimeException $error) use(&$response, $loop){
    // ...
});
Link to comment
Share on other sites

Thank you kicken,

 

I made the changes, but am still getting Unable to complete SSL/TLS handshake.

 

I've verified that I am not making a bonehead error such as wrong paths to the certificates on both the client and server.

 

The error originates in React\Socket\src\StreamEncryption.php in the following method.

    public function toggleCrypto($socket, Deferred $deferred, $toggle)
    {
        set_error_handler(array($this, 'handleError'));
        $result = stream_socket_enable_crypto($socket, $toggle, $this->method);
        //stream_socket_enable_crypto(15, true, 57) returns false
        $result = stream_socket_enable_crypto($socket, $toggle, $this->method);
        //....
    }

Before posting my original post, I thought that maybe the client would need some knowledge of the certificate, and searched for it in the documentation, but did not find anything.  After getting your post, I searched for the text cafile in all of the reactphp scripts, but did not find it.  I also searched for anything with the text cert in in, and found only the property local_cert.  Odd how they give no description about giving the client the certificate, however, this is uncharted waters for me and everything is odd.

 

Just for fun, I changed cafile to local_cert and now get Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): Unable to set private key file `/var/www/datalogger/sslforfree/certificate.crt'.  Not a step in the right direction...

 

Just to double confirm, both the server and client both include the same certificate, right?  Shouldn't the server get the certificate from the client when requesting service?  Where does the private key come in?

 

Note that my certificate is call certificate.crt provided by slforfree, yet the example reactphp scripts use extention pem.  Could this be related?

 

Thanks again for all your help.

 

EDIT.  What versions of reactphp are you using?  My composer.json file includes the following:

 

        "react/socket": "^0.8.0",
        "react/stream": "^0.7.0",
        "react/event-loop": "^0.4.2"
Link to comment
Share on other sites

One side needs the certificate + key (typically the server) and the other side needs just the certificate for verification purposes (typically the client).

 

The cafile option is part of PHP, not react. local_cert is as well.

 

SecureServer and SecureConnector call stream_context_set_option with whatever parameters you pass in the constructor so you won't find them within the react source anywhere, you need to look at the PHP documentation for the SSL wrapper.

 

If you add an error handler to your listening side you can get a more detailed message.

$server->on('error', function($error) use ($loop){
        echo "Some error occured!\n";
        var_dump($error);
        $loop->stop();
});
When I tried using similar code to what you posted, this is the error that was returned:

UnexpectedValueException: Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): Unable to set private key file `/home/kicken/secureTest/certificate.pem' in /home/kicken/secureTest/vendor/react/socket/src/StreamEncryption.php on line 117
There are two ways to resolve that error. Either combine your private key and certificate into a single file (just concatenate them together) or specify the location of the key file using the locak_pk option.

 

You may need to specify the proper name on the certificate for verification to complete successfully. You can do that using the peer_name option. I had to do that since I just used a random certificate I had laying around rather than create a new one explicitly for this purpose.

 

Server:

<?php

require 'vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();
$server = new \React\Socket\TcpServer('127.0.0.1:12123', $loop);
$server = new \React\Socket\SecureServer($server, $loop, [
        'local_cert' => 'combined.pem'
]);
$server->on('connection', function (\React\Socket\ConnectionInterface $conn) use($loop) {
        echo "Received new connection!\n";
        var_dump($conn);
        $loop->stop();
});
$server->on('error', function($error) use ($loop){
        echo "Some error occured!\n";
        var_dump($error);
        $loop->stop();
});
$loop->run();
Client:

<?php
require 'vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();
$connector = new \React\Socket\TimeoutConnector(new \React\Socket\Connector($loop), 10, $loop);
$connector = new \React\Socket\SecureConnector($connector, $loop, [
        'cafile' => 'certificate.pem'
        , 'peer_name' => 'Kickenscripts.us'
]);
$connector->connect('127.0.0.1:12123')
->then(function (\React\Socket\ConnectionInterface $connection)  use (&$response, $loop, $msg) {
        echo "Connected successfully!";
        var_dump($connection);
        $loop->stop();
})->otherwise(function(\RuntimeException $error) use(&$response, $loop){
        echo "Failed to connect!";
        var_dump($error);
        $loop->stop();
});
$loop->run();

Note that my certificate is call certificate.crt provided by slforfree, yet the example reactphp scripts use extention pem. Could this be related?

No, the extension doesn't make a difference. Some people use .crt to indicate a certificate, some use .pem because it's a PEM-encoded file format. Use whichever you prefer.

Link to comment
Share on other sites

Thank you kicken,
 
Ah, of course cafile and local_cert is part of stream_context_set_option, and not reactphp.  I should have known.
 
Just to make sure, server and the client being on the same machine won't be an issue, right?
 
Regarding the peer_name (i.e. Kickenscripts.us), is this the host name, or something included in the certificate?  Currently I am setting verify_peer and verify_peer_name to false which is probably a bad idea.  EDIT.  When creating a csr, openssl asks for the FQDN, but allows for now entry for default.  Should it always be included?  Guess not relevant here, but if used for subdomain.example.com, should it be example.com or subdomain.example.com?
 
I see now how for the server I can either use local_pk and local_cert or local_cert, and can even set the directory path (for both?) using capath.  I seem to get better results with using the two files, but neither is working:
 
Server when using cafile and local_pk
Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): SSL_R_NO_SHARED_CIPHER: no suitable shared cipher could be used.  This could be because the server is missing an SSL certificate (local_cert context option)
Client
Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): SSL operation failed with code 1. OpenSSL Error messages: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure
 
Server when using combined.pem (which was created using cat private.key certificate.crt > combined.pem and not openssl)
Unable to complete SSL/TLS handshake: stream_socket_enable_crypto(): Unable to set local cert chain file `/var/www/datalogger/sslforfree/combined.pem'; Check that your cafile/capath settings include details of your certificate and its issuer
Client
Unable to complete SSL/TLS handshake: 
 
I am thinking it might have something to do with my certificate which was created using https://www.sslforfree.com/.  Note the following.  I am thinking I either need to CSR or maybe even don't use this at first and start off using self signed keys.
Private Keys are generated in your browser and never transmitted.
For modern browsers we generate a private key in your browser using the Web Cryptography API and the private key is never transmitted. The private key also gets deleted off your browser after the certificate is generated. If your browser does not support the Web Cryptography API then the keys will be generated on the server using the latest version of OpenSSL and outputted over SSL and never stored. For the best security you are recommended to use a supported browser for client generation. You can also provide your own CSR when using manual verification in which case the private key is handled completely on your end.

 

 

Thanks again for all your help!

Link to comment
Share on other sites

Eureka!

 

I still would like confirmation whether I am doing the peer_name part correct.  Is this the hostname for the server and the commonName for the certificate?

 

Thanks for all your help!

 

 

How I generated my keys.

$certificateData = [
    "countryName" => "US",
    "stateOrProvinceName" => "WA",
    "localityName" => "Seattle",
    "organizationName" => "TestingOrganizationName.com",
    "organizationalUnitName" => "TestingOrganizationalUnitName",
    "commonName" => "devserver.michaels.lan",
    "emailAddress" => "Testing@emailAddress.com"
];
$pem_passphrase = 'testing';


// Generate certificate
$privateKey = openssl_pkey_new();
$certificate = openssl_csr_new($certificateData, $privateKey);
$certificate = openssl_csr_sign($certificate, null, $privateKey, 365);


// Generate PEM file
$pem = [];
openssl_x509_export($certificate, $pem[0]);
file_put_contents('./sslforfree/certificate.pem', $pem[0]);
openssl_pkey_export($privateKey, $pem[1], $pem_passphrase); //Remove $pem_passphrase for no
$pem = implode($pem);


file_put_contents('./sslforfree/private.pem', $pem);

Server

$server = new \React\Socket\TcpServer($this->host['url'].':'.$this->host['port'], $loop);
if($this->host['tls']) {
    $server = new \React\Socket\SecureServer($server, $loop, [
        'local_cert'    => realpath( __DIR__.'/../../sslforfree/private.pem'),
        'passphrase' => 'testing'
    ]);
}
$server->on('connection', function (\React\Socket\ConnectionInterface $conn) {/* ... */});
$server->on('error', function($error) use ($loop){
    syslog(LOG_INFO,'onServerError: '.$error->getMessage());
    //$loop->stop();
});

Client

if($this->settings['socket']['tls']) {    $connector = new \React\Socket\SecureConnector($connector, $loop, [
        'cafile' => realpath( __DIR__.'/../../sslforfree/certificate.pem'),
        'peer_name' => 'devserver.michaels.lan',
        'allow_self_signed'=>true,
    ]);
}
//$this->settings['socket']['url'] is set to 127.0.0.1
$connector->connect($this->settings['socket']['url'].':'.$this->settings['socket']['port'])
->then(function (\React\Socket\ConnectionInterface $connection)  use (&$response, $loop, $msg) {/*...*/})
->otherwise(function(\RuntimeException $error) use(&$response, $loop){/*...*/});

Link to comment
Share on other sites

Just to make sure, server and the client being on the same machine won't be an issue, right?

Same machine is fine, won't make a difference. My examples/tests were done by just running the server.php and client.php files in separate terminals and connecting to localhost.

 

 

Regarding the peer_name (i.e. Kickenscripts.us), is this the host name, or something included in the certificate?  Currently I am setting verify_peer and verify_peer_name to false which is probably a bad idea.  EDIT.  When creating a csr, openssl asks for the FQDN, but allows for now entry for default.  Should it always be included?  Guess not relevant here, but if used for subdomain.example.com, should it be example.com or subdomain.example.com?

It's the common name of the certificate. For web certificates it should be the same as your domain name.

 

You can use the command openssl x509 -in certificate.pem -noout -subject to see what your certificate is set to. What you need your peer_name set to is the CN= part. If you're using a URL that includes the domain name then you can skip setting the peer name, it'll automatically use the name in the URL.

 

 

I am thinking it might have something to do with my certificate which was created using https://www.sslforfree.com/.  Note the following.  I am thinking I either need to CSR or maybe even don't use this at first and start off using self signed keys.

It seems if you're using a certificate chain setup then things get slightly more complicated. I was initially using a self-signed certificate to test with. It took me a little while of playing around but what I eventually was able to get working is a setup like this:

 

For server.php

  • Create combined.pem by concatenating your certificate and any intermediate CA certificates:

    cat yours.pem intermediate1.pem intermediate2.pem ... > combined.pem
    
  • Save your private key in key.pem
  • Configure local_cert => combined.pem and local_pk => key.pem
For client.php
  • Obtain your CA's root certificate and save it in root.pem
  • Configure cafile => root.pem
If you're only need for the certificates is to secure the channel between your own two programs then you can just use your own self-signed certificates, there's no need to go through a CA. You only need to use a CA when you need a certificate that will be trusted by peoples browsers/other software by default.
Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.