Skip to content

Commit

Permalink
Add Opportunistic TLS implementation
Browse files Browse the repository at this point in the history
This commit introduces the functionality required to build opportunistic TLS clients and servers with
ReactPHP. It does so by introducing a prefix to `tls://`, namely `opportunistic`, to create
`opportunistic+tls://example.com:5432` for example as the full URL. This will create an
`OpportunisticTlsConnectionInterface` (instead of a `ConnectionInterface`) that extends the
`ConnectionInterface` and exposes the `enableEncryption` method to enable TLS encryption at the
desired moment. Inside this PR is an example of a server and client negotiating when to enable TLS
and enable it when ready.

Depends on: reactphp/async#65
Opportunistic Security described in RFC7435: https://www.rfc-editor.org/rfc/rfc7435
External PR using the proposed changes in this commit: voryx/PgAsync#52
  • Loading branch information
WyriHaximus committed Dec 7, 2022
1 parent 936546b commit 0275788
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 9 deletions.
111 changes: 107 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ handle multiple concurrent connections without blocking.
* [ConnectionInterface](#connectioninterface)
* [getRemoteAddress()](#getremoteaddress)
* [getLocalAddress()](#getlocaladdress)
* [OpportunisticTlsConnectionInterface](#opportunistictlsconnectioninterface)
* [enableEncryption()](#enableencryption)
* [Server usage](#server-usage)
* [ServerInterface](#serverinterface)
* [connection event](#connection-event)
Expand Down Expand Up @@ -193,6 +195,62 @@ If your system has multiple interfaces (e.g. a WAN and a LAN interface),
you can use this method to find out which interface was actually
used for this connection.

### OpportunisticTlsConnectionInterface

The `OpportunisticTlsConnectionInterface` extends the [`ConnectionInterface`](#connectioninterface) and adds the ability of
enabling the TLS encryption on the connection when desired.

#### enableEncryption

When negotiated with the server when to start encrypting traffic using TLS you enable it by calling
`enableEncryption()` which returns a promise that resolve with a `OpportunisticTlsConnectionInterface` connection but now all
traffic back and forth will be encrypted.

In the following example we ask the server if they want to encrypt the connection, and when it responds with `yes` we
enable the encryption:

```php
$connector = new React\Socket\Connector();
$connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
$connection->write('let\'s encrypt?');

return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'yes') {
return $connection->enableEncryption();
}

return $stream;
});
})->then(function (React\Socket\ConnectionInterface $connection) {
$connection->write('Hello!');
});
```

The `enableEncryption` function resolves with itself. As such you can't see the data encrypted when you hook into the
events before enabling, as shown below:

```php
$connector = new React\Socket\Connector();
$connector->connect('opportunistic+tls://example.com:5432/')->then(function (React\Socket\OpportunisticTlsConnectionInterface $startTlsConnection) {
$connection->on('data', function ($data) {
echo 'Raw: ', $data, PHP_EOL;
});

return $connection->enableEncryption();
})->then(function (React\Socket\ConnectionInterface $connection) {
$connection->on('data', function ($data) {
echo 'TLS: ', $data, PHP_EOL;
});
});
```

When the other side send `Hello World!` over the encrypted connection, the output will be the following:

```
Raw: Hello World!
TLS: Hello World!
```

## Server usage

### ServerInterface
Expand Down Expand Up @@ -253,10 +311,10 @@ If the address can not be determined or is unknown at this time (such as
after the socket has been closed), it MAY return a `NULL` value instead.

Otherwise, it will return the full address (URI) as a string value, such
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`
`unix://example.sock` or `unix:///path/to/example.sock`.
Note that individual URI components are application specific and depend
on the underlying transport protocol.
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
`unix://example.sock`, `unix:///path/to/example.sock`, or
`opportunistic+tls://127.0.0.1:443`. Note that individual URI components
are application specific and depend on the underlying transport protocol.

If this is a TCP/IP based server and you only want the local port, you may
use something like this:
Expand Down Expand Up @@ -478,6 +536,22 @@ $socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array(
));
```

To start a server with opportunistic TLS support use `opportunistic+tls://` as the scheme instead of `tls://`:

```php
$socket = new React\Socket\SocketServer('opportunistic+tls://127.0.0.1:8000', array(
'tls' => array(
'local_cert' => 'server.pem',
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
)
));
$server->on('connection', static function (OpportunisticTlsConnectionInterface $connection) use ($server) {
return $connection->enableEncryption();
});
```

See also the [examples](examples).

> Note that available [TLS context options](https://www.php.net/manual/en/context.ssl.php),
their defaults and effects of changing these may vary depending on your system
and/or PHP version.
Expand Down Expand Up @@ -697,6 +771,21 @@ here in order to use the [default loop](https://github.com/reactphp/event-loop#l
This value SHOULD NOT be given unless you're sure you want to explicitly use a
given event loop instance.

Opportunistic TLS is supported by the secure server by passing true in as 4th constructor
parameter. This, when a client connects, emits a
[`OpportunisticTlsConnectionInterface`](#opportunistictlsconnectioninterface) instead
of the default [`ConnectionInterface`](#connectioninterface) and won't be TLS encrypted
from the start so you can enable the TLS encryption on the connection after negotiating
with the client.

```php
$server = new React\Socket\TcpServer(8000);
$server = new React\Socket\SecureServer($server, null, array(
'local_cert' => 'server.pem',
'passphrase' => 'secret'
), true);
```

> Advanced usage: Despite allowing any `ServerInterface` as first parameter,
you SHOULD pass a `TcpServer` instance as first parameter, unless you
know what you're doing.
Expand Down Expand Up @@ -1389,6 +1478,20 @@ $secureConnector = new React\Socket\SecureConnector($dnsConnector, null, array(
));
```

Opportunistic TLS is supported by the secure connector by using `opportunistic-tls://` as scheme instead of `tls://`. This, when
connected, returns a [`OpportunisticTlsConnectionInterface`](#opportunistictlsconnectioninterface) instead of the default
[`ConnectionInterface`](#connectioninterface) and won't be TLS encrypted from the start so you can enable the TLS
encryption on the connection after negotiating with the server.

```php
$secureConnector = new React\Socket\SecureConnector($dnsConnector);
$secureConnector->connect('opportunistic-tls://example.com:5432')->then(function (OpportunisticTlsConnectionInterface $connection) {
return $connection->enableEncryption();
})->then(function (OpportunisticTlsConnectionInterface $connection) {
$connection->write('Encrypted hi!');
});
```

> Advanced usage: Internally, the `SecureConnector` relies on setting up the
required *context options* on the underlying stream resource.
It should therefor be used with a `TcpConnector` somewhere in the connector
Expand Down
68 changes: 68 additions & 0 deletions examples/31-opportunistic-tls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

// Opportunistic TLS example showing a basic negotiation before enabling the encryption. It starts out as an
// unencrypted TCP connection. After both parties agreed to encrypt the connection they both enable the encryption.
// After which any communication over the line is encrypted.
//
// This example is design to show both sides in one go, as such the server stops listening for new connection after
// the first, this makes sure the loop shuts down after the example connection has closed.
//
// $ php examples/31-opportunistic-tls.php

use React\EventLoop\Loop;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\OpportunisticTlsConnectionInterface;
use React\Socket\SocketServer;

require __DIR__ . '/../vendor/autoload.php';

$server = new SocketServer('opportunistic+tls://127.0.0.1:0', array(
'tls' => array(
'local_cert' => __DIR__ . '/localhost.pem',
)
));
$server->on('connection', static function (OpportunisticTlsConnectionInterface $connection) use ($server) {
$server->close();

$connection->on('data', function ($data) {
echo 'From Client: ', $data, PHP_EOL;
});
React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'Let\'s encrypt?') {
$connection->write('yes');
return $connection->enableEncryption();
}

return $connection;
})->then(static function (ConnectionInterface $connection) {
$connection->write('Encryption enabled!');
})->done();
});

$client = new Connector(array(
'tls' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
),
));
$client->connect($server->getAddress())->then(static function (OpportunisticTlsConnectionInterface $connection) {
$connection->on('data', function ($data) {
echo 'From Server: ', $data, PHP_EOL;
});
$connection->write('Let\'s encrypt?');

return React\Promise\Stream\first($connection)->then(function ($data) use ($connection) {
if ($data === 'yes') {
return $connection->enableEncryption();
}

return $connection;
});
})->then(function (ConnectionInterface $connection) {
$connection->write('Encryption enabled!');
Loop::addTimer(1, static function () use ($connection) {
$connection->end('Cool! Bye!');
});
})->done();
4 changes: 4 additions & 0 deletions src/Connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public function __construct($context = array(), $loop = null)
'dns' => true,
'timeout' => true,
'happy_eyeballs' => true,
'opportunistic+tls' => true,
);

if ($context['timeout'] === true) {
Expand Down Expand Up @@ -150,6 +151,9 @@ public function __construct($context = array(), $loop = null)
}

$this->connectors['tls'] = $context['tls'];
if ($context['opportunistic+tls'] !== false) {
$this->connectors['opportunistic+tls'] = $this->connectors['tls'];
}
}

if ($context['unix'] !== false) {
Expand Down
109 changes: 109 additions & 0 deletions src/OpportunisticTlsConnection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace React\Socket;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;
use React\Stream\DuplexResourceStream;
use React\Stream\Util;
use React\Stream\WritableResourceStream;
use React\Stream\WritableStreamInterface;

/**
* The actual connection implementation for StartTlsConnectionInterface
*
* This class should only be used internally, see StartTlsConnectionInterface instead.
*
* @see OpportunisticTlsConnectionInterface
* @internal
*/
class OpportunisticTlsConnection extends EventEmitter implements OpportunisticTlsConnectionInterface
{
/** @var Connection */
private $connection;

/** @var StreamEncryption */
private $streamEncryption;

/** @var string */
private $uri;

public function __construct(Connection $connection, StreamEncryption $streamEncryption, $uri)
{
$this->connection = $connection;
$this->streamEncryption = $streamEncryption;
$this->uri = $uri;

Util::forwardEvents($connection, $this, array('data', 'end', 'error', 'close'));
}

public function getRemoteAddress()
{
return $this->connection->getRemoteAddress();
}

public function getLocalAddress()
{
return $this->connection->getLocalAddress();
}

public function isReadable()
{
return $this->connection->isReadable();
}

public function pause()
{
$this->connection->pause();
}

public function resume()
{
$this->connection->resume();
}

public function pipe(WritableStreamInterface $dest, array $options = array())
{
return $this->connection->pipe($dest, $options);
}

public function close()
{
$this->connection->close();
}

public function enableEncryption()
{
$that = $this;
$connection = $this->connection;
$uri = $this->uri;

return $this->streamEncryption->enable($connection)->then(function () use ($that) {
return $that;
}, function ($error) use ($connection, $uri) {
// establishing encryption failed => close invalid connection and return error
$connection->close();

throw new \RuntimeException(
'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(),
$error->getCode()
);
});
}

public function isWritable()
{
return $this->connection->isWritable();
}

public function write($data)
{
return $this->connection->write($data);
}

public function end($data = null)
{
$this->connection->end($data);
}
}
18 changes: 18 additions & 0 deletions src/OpportunisticTlsConnectionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace React\Socket;

use React\Promise\PromiseInterface;

/**
* @see DuplexStreamInterface
* @see ServerInterface
* @see ConnectionInterface
*/
interface OpportunisticTlsConnectionInterface extends ConnectionInterface
{
/**
* @return PromiseInterface<OpportunisticTlsConnectionInterface>
*/
public function enableEncryption();
}
Loading

0 comments on commit 0275788

Please sign in to comment.