ghedipunk / PHP-Websockets

A Websockets server written in PHP.
BSD 3-Clause "New" or "Revised" License
917 stars 374 forks source link

New feature discussion: TLS #41

Open ghedipunk opened 9 years ago

ghedipunk commented 9 years ago

I have decided to implement TLS (wss://) in the server using the built-in PHP OpenSSL functions.

This will require much testing and much review. I know I'm good, but I also know that hubris is the cause of many security holes. Simply put, I simply refuse to trust myself when it comes to encryption, even if I know I'm doing everything right. (I've had good teachers when it comes to encryption. I don't trust them, either.)

So, this be the discussion thread for implementing TLS.

ghedipunk commented 9 years ago

One thing I'm personally big on is encrypting your private key with a passphrase. I think the WS server should be able to load an encrypted private key when it's staring up and prompt the operator for a passphrase.

This would require human intervention in case the server ever needs to be restarted, such as after a power loss to the hardware... Security comes at the cost of convenience. If anyone is not comfortable getting up at 2am to SSH into their machine and enter a password, then they shouldn't encrypt their private key... then again, they also run a greater risk of their private key being compromised in case their server is compromised.

ghedipunk commented 9 years ago

Anyone intending on working on TLS once I get the initial branch set up: Check the OWASP TLS cheat sheet often.

https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet

ghedipunk commented 9 years ago

Both Chrome 43 and Firefox 38 do handshakes for TLS 1.0.

Vulnerabilities of TLS 1.0 are CBC Chaining and Padding Oracle attacks.

TODO: Find out if we can mitigate these attacks in WS.

Anyways, looks like we're implementing TLS 1.0 first, then can work up to 1.1 and 1.2. I wonder if we can get browsers to update their TLS defaults for wss: connections to reflect their https: defaults.

tylorjefcoat commented 8 years ago

anything new on this? trying to implement https/wss using your library, which btw works great for what i'm using it for!!!

is there anything i can do to implement wws, i'm still new to the websocket but now have a fully working solution on our intranet using your library and exploring future https implementation and big stop on using chrome because the websocket is not using wss while https.

Thanks!!

ghedipunk commented 8 years ago

Currently blocked on other changes.

The end goal of those changes are to make TLS work properly and securely, and make the server easier to use and extend (and a bit faster and more reliable).

Progress is slow, but not completely stopped.

tylorjefcoat commented 8 years ago

Anything you can point me toward to help?

Using this on a custom trouble ticket system I built to provide real-time updates and other features, haven't had a single issue so far

Thanks!

PeterElzinga commented 8 years ago

Hi tylorjefcoat,

I have just got my WSS working. You can use this as an example, however do not copy this directly into Ghedypunks class as I have changed naming conventions etc to suit my own needs.

ssl websocket example.txt

It is work in progress - not tested to great extends.

mdrmdrmdr commented 8 years ago

Hi, any news on the "wss" thing? I use your great code for the client-server communication of a home automation (mostly blinds) system. And I really would be happy to be able to use secure communication. Thanks!

ghedipunk commented 8 years ago

Been busy with young kids and a new job.

I will make TLS support a priority over the next few weekends.

Xaraknid commented 8 years ago

I guess the easiest way is to switch from socket library to stream library and use the built-in tls protocol.

mdrmdrmdr commented 8 years ago

@ghedipunk: Thanks for your efforts. I appreciate that :-)

@Xaraknid: With PHP, streams seems to be easy. How would the interface in Javascript on the client side look like? Or better, do you know of an simple example? E.g. a chat tool?

ghedipunk commented 8 years ago

@Xaraknid That's what I was just thinking, as well.

I'm really torn on whether to do it in the new development branch first, or in the legacy branch, though. I don't have nearly enough done in the new development branch that I could take it to completion quickly. (At least, not quickly while I've got 1 year old twins in my house.) On the other hand, the new branch is meant to make unit testing easy, which is a feature that I think is almost as important as TLS.

Alright, decision made: I am officially putting unit testing on the ledger as technical debt. Any new development done without getting a good baseline for unit testing will add to that technical debt. And, I am officially adding more technical debt by deciding to implement TLS on the legacy branch first.

@mdrmdrmdr The Javascript and all other client code would stay exactly the same, we'd just be able to use "wss://" properly now. (If the client code doesn't support TLS, then it would have to be updated, but all major browsers use TLS 1.0 when opening a connection with the WSS protocol.)

Xaraknid commented 8 years ago

@mdrmdrmdr as said by @ghedipunk.

@ghedipunk socket and libev library work on the same level and still can be easily done but with some search will require the openssl library to near easy than stream built-in.

socket and stream library have the same restriction as both use select for polling and have a hard system limit of 1024 concurrent connections.

ghedipunk commented 8 years ago

Current way that the server is implemented, we can't have both a secure and insecure connection going at the same time without changing the constructor's signature. (I consider that a soft breaking change: Easy solution is to add more parameters to the constructor, but we can't change the order of arguments without breaking backwards compatibility.)

I'm of the opinion that if a process is handling secure data, it shouldn't handle data insecurely... That is, if you're using TLS, then all traffic must be over TLS. However, this opinion isn't shared with people who are used to the Apache HTTPd way of doing things, where the same process will happily serve up HTTP and HTTPS content at will.

So, either use WS or WSS.

Not sure how the major browsers handle self-signed certs over WSS... Will need to test.

Will also need to set up the cert on my public web server.

See also: https://www.eff.org/press/releases/new-free-certificate-authority-dramatically-increase-encrypted-internet-traffic

ghedipunk commented 8 years ago

Note: Latest push to the legacy branch (d1cfae2bbe1c0f922c2ad18ee57951e77aaf99a0) is broken.

Will not make an attempt to unbreak it until I'm ready to see if the changes work or not.

Xaraknid commented 8 years ago

You are right you can't run both at the same time. But I think you can support both all will depend how you start the server.

ws $test = new phpws("0.0.0.0","8080",'stream');

wss $test = new phpws("tls://0.0.0.0","8080",'stream');

for stream the job is done internally only thing required is to config it $ctx=stream_context_create(array('ssl'=>array( "local_cert"=>"mycertandkey.pem", "passphrase"=>"badpassphrase" )));

Not tested it yet and as said socket and libev all job need to be done by server. By knowing the layer those job need to be done you only need to have a flag turn on when using TLS connection and call private method when the flag is on (encrypt / decrypt).

Self-signed certificate will at least pop-up a warning if not working at all. Let's encrypt is a good solution as long you did not forget to renew it once per 3 month as they are valid for 90 days.

EDIT : even with socket library it's relatively easy you'll need to use openssl_private_decrypt on READ buffer and openssl_private_encrypt on WRITE buffer before sending.

ghedipunk commented 8 years ago

Well on the way to switching to a stream_socket_* based implementation, rather than the socket_ wrappers around the C BSD Sockets library.

Potential Compatibility Note: Moving the Master socket resource out of the sockets array. (You only need to worry about this if you implement your own run() method, or otherwise expect the Master socket resource anywhere but $this->master.)

New implementation will use stream_socket_accept() with a zero timeout before the one second blocking on stream_socket_select(), rather than having the master socket (now the master socket stream) be included in &$read and again tested against $this->master.

Xaraknid commented 8 years ago

There is minimal difference between stream and socket. I can assure you as I can run both eventloop.

Master socket MUST be in socket array know as $read. socket_select or stream_socket_select. Otherwise the server will never be notify that a client attempt to connect.

I highly against a timeout of 0 that will result in heavy wasted CPU usage. It's better to wait patiently untill there is something on the wire than checking constantly with nothing on the wire.

ghedipunk commented 8 years ago

Legacy branch passes very basic smoke test after switching to stream_socket_* implementation. (Smoke test on PHP 5.5, OSX)

Have not yet tested the TLS part, don't have any certs handy on this machine.

ghedipunk commented 8 years ago

Great point about the timeout of zero. Matches my observation of the high-CPU busy loop, although stream_socket_accept did work when I tried accepting. Once stream_socket_select had a socket in &$read to check, it respected the 1 second timeout there.

Xaraknid commented 8 years ago

The only good reason I see to use timeout is if you want to implement disconnection process after too long inactivity like sending a ping opcode and if no pong then kill the client. Even there I prefer having timeout blank.

An advice do not use @ they are tempting but you should restraint yourself of using it. First they are slow, secondly and the most important point is that you only hide bug under the carpet. It's better to found and kill the bug on the long run.

ghedipunk commented 8 years ago

Moved the master resource back into the &$read parameter of select(), along with accept(). (Switched things up to break out of the foreach, rather than have an if/elseif/else.)

Passes smoke test.

Besides worrying about TLS, I also want to test unusual client disconnects, since I couldn't find the appropriate functions to check TCP error codes under the stream_ functions in the manual. (Note to self: Also verify normal client disconnects.)

ghedipunk commented 8 years ago

Note on testing timeline: The host that manages the server to which ghedipunk.net is pointed has recently changed details about their SSH access on their managed VPS accounts... Basically, they said that it's hard to support people who know what they're doing, so they're taking away our tools, and those who want the tools back will have to pay 10 times what they're paying now for a dedicated server. No SSH to the terminal. No sudo. Just a few point-and-click options on a recently dumbed down admin panel. (alright, I'll stop ranting. Yes, I used to recommend them. Now I don't.)

So, since I'm too lazy to actually switch hosts, and I really don't want to bother spinning up a new host on Amazon's cloud, I'll be setting things up on the linux box at my house and in order to get a certificate through Let's Encrypt, I'll be making a few domain changes, which will take some time to propagate...

In other words, I might not get to testing the actual TLS portion tonight. Will absolutely test the client disconnect scenarios, though.

ghedipunk commented 8 years ago

Progress so far:

Basic smoke test passed on Windows 10, PHP 5.6.

Found an old(ish) cert (still valid for a couple more months) and edited my hosts file to make localhost match.

So far, failing at "stream_socket_accept(): Failed to enable crypto" which is good progress. Probably need to create an openssl.cnf file.

ghedipunk commented 8 years ago
Warning: stream_socket_accept(): SSL operation failed with code 1. OpenSSL Error messages:
error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number

Definite progress. It's handshaking. Not really in the mood to get OpenSSL working on Windows, though.

Time to move it over to Linux.

ghedipunk commented 8 years ago

Correction: Great success!

wss-success

(Note: ghedipunk.net is pointing to localhost in this case, due to edits in my hosts file)

Trick was to edit client.html on disk, rather than use Chrome's DOM editor to change the URI scheme from WS to WSS after it was loaded.

Will polish things up then push things so far, then get on to testing the differences between Berkeley sockets and Stream sockets.

ghedipunk commented 8 years ago

3c2d6ca402c827d4ddcc750a98159f6d74b35b5c passes the smoke test.

Some cleanup needed... I'm thinking of having an example TLS echo server, just like the current testwebsock.php, and probably update client.html with the option to connect with either WS or WSS.

Will also need to update the readme.

Then I need to really dig into the differences between Berkeley sockets and stream sockets, especially detecting when a client disconnects, whether normally or abnormally... then test large packets, Nagle's Algorithm... Then I'll add the Autobahn|testsuite to the standard testing procedures... THEN merge into master.

Then back to work on the next branch. (Actually, work on the support site first, but that's a whole other mess.)

mdrmdrmdr commented 8 years ago

Sounds great :-) I'd use - once it's stable - the PHP part on a Raspberry Pi 2 and the Client on Windows 7 and iOS 9.x. Thanks for your work!

Xaraknid commented 8 years ago

http://phpsecurity.readthedocs.org/en/latest/Transport-Layer-Security-%28HTTPS-SSL-and-TLS%29.html A very good information about secure connection. It's important to fine tune configure options around ssl/tls connection to prevent many sort of exploit and in the end having false impression of "security".

I know you want to keep backward compatibility and I think a good way to do that for adding TLS feature without having user the need to go in internal file ( websockets.php ) to config it. In the path right now every patch to the core will required the user to modify it to set up the feature.

I suggest moving this step outside of websockets.php to testwebsock.php. Simply change the construct from

__construct($addr, $port, $bufferLength = 2048)

to

__construct($addr, $port, $type= 'ws',$bufferLength = 2048,$ssl_options='')
or similar order

That will be backward compatible, need of only one method to setup connection. With that you can check enforce all options neccessary for secure connection to be present if some missing exit("malformed ssl_options missing X ")

ghedipunk commented 8 years ago

The route that I've already started going down is removing the setupSecureConnection() method, and will have the example script (testtlssock.php) override setupConnection() with a secure implementation.

So far,

class echoServer extends WebSocketServer {

    /**
     * Overrides the base setupConnection to implement TLS.
     *
     * @return void
     */
    protected function setupConnection() {
        $errno = $errstr = null;

        $options = array(
            'ssl' => array(
                'peer_name' => 'YOUR DOMAIN NAME HERE',
                'verify_peer' => false,
                'local_cert' => 'cert.pem',
                'local_pk' => 'pk.key',
            ),
        );
        $context = stream_context_create($options);

        $this->master = stream_socket_server(
            'tls://' . $this->listenAddress . ':' . $this->listenPort,
            $errno,
            $errstr,
            STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
            $context
        );
    }
... continue on with the echo server as in testwebsock.php

I need to research some secure defaults for the context options still, before I put the example up publicly. My philosophy is that security requires failing closed, so I'm also considering putting in how to encrypt and decrypt the certificate, securing the context options inside of a configuration file (which is really where these details need to live, so that nobody is editing source code to change, say, the filename for the cert), and even encrypting the config file and prompting for a password to decrypt that option file when the WS server starts up.

(Also, yes, I'm fully aware that there is a context option named 'ciphers' that defaults to 'DEFAULT'... and that this default might be able to be downgraded all the way down to no encryption at all. Lots of steps to go through in the checklist to make sure things are secure. I'll also be re-reading through the OWASP docs to see if I've missed anything they've missed, as well as peppering this example with plenty of comments warning people to maintain a minimum level of security, and that authentication, integrity, and non-repeating is as much an essential part of security as encryption is.)

Xaraknid commented 8 years ago

By prompting for password didn't prevent the use of nohup and let's only option screen ?

It's all good to tighten security as long all the chain is on same level. The level of security is equal of the weakest link to keep thing in perspective.

ghedipunk commented 8 years ago

Yes, prompting for a password will prevent the use of nohup, which will require the process itself to become a daemon.

Process to become a daemon:

ghedipunk commented 8 years ago

Default TLS context options that I intend to use in the example code, posted here for any discussion:

$options = array(
    'ssl' => array(
        'peer_name' => 'YOUR DOMAIN NAME HERE',
        'verify_peer' => false,
        'local_cert' => 'cert.pem',
        'local_pk' => 'pk.key',
        'disable_compression' => true,
        'SNI_enabled' => true,
        'ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK',
    ),
);
ghedipunk commented 8 years ago

Implementation detail note: (In other words, don't count on this behavior anywhere, even here, even if you know that it's happening.)

Stream_socket doesn't give many handy ways to know if a connection is closed, like BSD sockets do. Adding in WS pings for periodically testing the connection, in cases where a WS close frame is not possible (i.e., unplugged network cable).

Xaraknid commented 8 years ago

At first I was wondering what you are talking about connection closed. You mean when you reading on the socket. You are right as socket are low-end library you have more control than stream.

Socket can tell you why it fail. But both have the same behavior to know when you lost connection.

socket_recv return false when a error happen. fread return false when a error happen.

In both case most likely because you lost connection and you can safely close your side of the connection.

Unless you want to behave differently depending on "why" error happen. You do not need to go deeper than that.

Edit : normal disconnection socket_recv return 0 fread return not tested yet

As state above as the result is the same ( normal or abnormal disconnection ) you close the connection. A simple == will catch 0 and FALSE.

mdrmdrmdr commented 8 years ago

Hi, any news here?

mdrmdrmdr commented 8 years ago

Push!

Hope my last question was not ignored since it was issued on April 1st ;-)

mdrmdrmdr commented 8 years ago

Ok, no response. Since I did not want to use ratchet, I played around with PHP-Websockets to achive secure communication. What I did:

So far my websocket communication runs stable with https/wss on only one port (443). No need for an extra port to be opended at the router for the websocket communication due to the usage of the mod_proxy module. So at least for me, a TLS version of PHP-Websockets is not required anymore.

:-))

RoxKilly commented 8 years ago

@mdrmdrmdr I will try to implement your instructions. Thanks so much for sharing the basic steps! If you can, please answer these questions

  1. Where do you put the ProxyPass directive? .htaccess?
  2. What do you mean by "and corresponding ProxyPassReverse"? Please copy and paste your ProxyPass and ProxyPassReverse rules
  3. Please show the modified run function. It's difficult for me to know exactly what you did from your description
RoxKilly commented 8 years ago

@PeterElzinga Would you mind sharing the rest of your websocket class? the snippet you posted relies on modifications you've made elsewhere so it's difficult to implement it

mdrmdrmdr commented 8 years ago

@RoxKilly: I think the proxy code could also go to .htaccess, but I placed it into the file named in the instructions for wstunnel, which is "/etc/apache2/mods-enabled/proxy_wstunnel.load". My "run" function is attached. Pls note that there are some globel constants used. E.g. CR=\r and NL=\n. Also "msg" is my logging function. U need to replace this.

Note: remove ".txt" from the filenames.

Good luck! ;-)

proxy_wstunnel.load.txt run.php.txt

RoxKilly commented 8 years ago

@mdrmdrmdr Thanks a ton for posting your function run() and your directives.

It took me hours to realize that my ProxyPass rules were not working because they require not just mod_proxy, but also mod_proxy_wstunnel

My ProxyPass and ProxyPassReverse rules are in VirtualHost config (within the httpd.conf file):

<IfModule mod_proxy.c>
    ProxyPass "/_ws_/" "ws://127.0.0.1:8080/"
    ProxyPassReverse "/_ws_/" "ws://127.0.0.1:8080/"
</IfModule>

I then start PHP-Websockets listening at that IP and port. So the WS server is not directly exposed to the internet.

I still need to figure out how to handle routes, but at least I have the setup working.

nsmith1024 commented 8 years ago

I tried your run code but it said CR and NL is undefined

mdrmdrmdr commented 8 years ago

Read the posts carefully then you'll find the answer...

guillenexo commented 7 years ago

@mdrmdrmdr thanks for your contributions.

A query, when you say: - start the client with "https", how would you do it? PHP CLI? or from a browser?

Thank you

RoxKilly commented 7 years ago

@guillenexo it means load the https webpage in the browser (the page that will attempt to connect to the websocket via wss://...)

mdrmdrmdr commented 7 years ago

Couldn't have said it better ;-)

guillenexo commented 7 years ago

thanks!

FixedLocally commented 7 years ago

My server is using HTTPS, so that I can use a ProxyPass to link up the web server and 5GE backed websocket server and encrypt data on the fly

MichelPoulain commented 7 years ago

Big thanks to @mdrmdrmdr whose 15 Aug 2016 comment worked perfectly for me. I was able to run TLS (wss://) Websockets through the native SSL Apache server and PHP without changing PHP code. I only changed the HTML file with protocol wss and port to 443. Apache's mod_proxy with ProxyPass and ProxyPassReverse in the virtualhost conf was a genius idea!

Thanks @ghedipunk for PHP-Websockets project. We're able to send broadcast messages to multiple users for a chat server for example. This is not the case with websocketd project which isolates each user.