IO::Socket::Async::SSL
This module provides a secure sockets implementation with an API very much
like that of the Raku built-in IO::Socket::Async
class. For the client
case, provided the standard certificate and host verification are sufficient,
it is drop-in replacement. The server case only needs two extra arguments to
listen
, specifying the server key and certificate.
As with IO::Socket::Async
, it is safe to have concurrent connections and to
share them across threads.
Synopsis
Client:
use IO::Socket::Async::SSL;
my $conn = await IO::Socket::Async::SSL.connect('www.raku.org', 443);
$conn.print: "GET / HTTP/1.0\r\nHost: www.raku.org\r\n\r\n";
react {
whenever $conn {
.print
}
}
$conn.close;
Server (assumes certificate and key files server-crt.pem
and server-key.pem
):
use IO::Socket::Async::SSL;
react {
my %ssl-config =
certificate-file => 'server-crt.pem',
private-key-file => 'server-key.pem';
whenever IO::Socket::Async::SSL.listen('localhost', 4433, |%ssl-config) -> $conn {
my $req = '';
whenever $conn {
$req ~= $_;
if $req.contains("\r\n\r\n") {
say $req.lines[0];
await $conn.print(
"HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n" ~
"<strong>Hello from a Raku HTTP server</strong>\n");
$conn.close;
}
}
}
}
Client
The connect
method on IO::Socket::Async::SSL
is used to establish a SSL
connection to a server. It requies two positional arguments, which specify the
host
and port
to connect to. It returns a Promise
, which will be kept
with an IO::Socket::Async::SSL
instance when the connection is established
and the SSL handshake completed.
my $conn = await IO::Socket::Async::SSL.connect($host, $port);
By default, the SSL certificate will be verified, using the default set of
accepted Certificate Authorities. The Promise
return by conenct
will be
broken if verification fails.
Sometimes it is convenient to create a CA and use it to sign certificates for
internal use, for example to secure communications between a set of services
on an internal network. In this case, the ca-file
named argument can be
passed to specify the certificate authority certificate file:
my $ca-file = '/config/ca-crt.pem';
my $conn = await IO::Socket::Async::SSL.connect('user-service', 443, :$ca-file);
Alternatively, a ca-path
argument can be specified, indicating a directory
where one or more certificates may be found.
It is possible to disable certificate verification by passing the insecure
named argument a true value. As the name suggests, this is not a secure
configuration, since there is no way for the client to be sure that it is
communicating with the intended server. Therefore, it is vulnerable to
man-in-the-middle attacks.
For control over the ciphers that may be used, pass the ciphers
argument to
connect
. It should be a string in OpenSSL cipher list format.
If wishing to view encrypted traffic with a tool such as Wireshark for debugging
purposes, pass a filename to ssl-key-log-file
. Session keys will be written to
this log file, and Wireshark can then be configured to introspect the encrypted
traffic (Preferences -> Protocols -> TLS -> (Pre-)-Master-Secret log filename). Note
that this key exposure compromises the security of the session!
Server
The listen
method returns a Supply
that, when tapped, will start an SSL
server. The server can be shut down by closing the tap. Whenever a connection
is made to the server, the Supply
will emit an IO::Socket::Async::SSL
instance. The listen
method requires two positional arguments, specifying
the host
and port
to listen on. Two named arguments are also required,
providing the certificate-file
and private-key-file
.
my %ssl-config =
certificate-file => 'server-crt.pem',
private-key-file => 'server-key.pem';
my $connections = IO::Socket::Async::SSL.listen('localhost', 4433, |%ssl-config);
react {
my $listener = do whenever $connections -> $conn {
say "Got a connection!";
$conn.close;
}
whenever signal(SIGINT) {
say "Shutting down...";
$listener.close;
exit;
}
}
For control over the ciphers that may be used, pass the ciphers
argument to
connect
. It should be a string in OpenSSL cipher list format. The following boolean options are also accepted:
prefer-server-ciphers
- indicates that the order of the ciphers list as configured on the server should be preferred over that of the one presented by the clientno-compression
- disables compressionno-session-resumption-on-renegotiation
Common client and server functionality
Both the connect
and listen
methods take the following optional named
arguments:
enc
, which specifies the encoding to use when the socket is used in character mode. Defaults toutf-8
.scheduler
, which specifies the scheduler to use for processing events from the underlyingIO::Socket::Async
instance. The default is$*SCHEDULER
. There is rarely a need to change this.
The Supply
, print
, write
, and close
methods have the same semantics as
in IO::Socket::Async.
Upgrading connections
Some protocols use opportunistic TLS,
where the decision to use transport layer security is first negotiated using
a non-encrypted protocol - provided negotiation is successful - a TLS handshake
is then performed. This functionality is provided by the upgrade-server
and
upgrade-client
methods. Note that the socket to upgrade must be an instance
of IO::Socket::Async
. Further, it is important to stop reading from the
socket before initiating the upgrade, which will typically entail working with
the Tap
directly, something not normally needed in react
/whenever
blocks.
Here is an example of using upgrade-server
.
react whenever IO::Socket::Async.listen('localhost', TEST_PORT) -> $plain-conn {
my $plain-tap = do whenever $plain-conn.Supply -> $start {
if $start eq "Psst, let's talk securely!\n" {
# Must stop reading...
$plain-tap.close;
# ...so the module can take over the socket.
my $enc-conn-handshake = IO::Socket::Async::SSL.upgrade-server(
$plain-conn,
private-key-file => 't/certs-and-keys/server.key',
certificate-file => 't/certs-and-keys/server-bundle.crt'
);
whenever $enc-conn-handshake -> $enc-conn {
uc-service($enc-conn);
}
$plain-conn.print("OK, let's talk securely!\n");
}
else {
$plain-conn.print("OK, let's talk insecurely\n");
uc-service($plain-conn);
}
}
sub uc-service($conn) {
whenever $conn -> $crypt-text {
whenever $conn.print($crypt-text.uc) {
$conn.close;
}
}
}
}
Here's an example using upgrade-client
; again, take note of the careful handling
of the Tap
.
my $plain-conn = await IO::Socket::Async.connect('localhost', TEST_PORT);
await $plain-conn.print("Psst, let's talk securely!\n");
react {
my $plain-tap = do whenever $plain-conn -> $msg {
$plain-tap.close;
my $enc-conn-handshake = IO::Socket::Async::SSL.upgrade-client(
$plain-conn,
host => 'localhost',
ca-file => 't/certs-and-keys/ca.crt');
whenever $enc-conn-handshake -> $enc-conn {
await $enc-conn.print("hello!\n");
whenever $enc-conn.head -> $got {
print $got; # HELLO!
done;
}
}
}
}
Bugs, feature requests, and contributions
Please use GitHub Issues
to file bug reports and feature requests. If you wish to contribute to this
module, please open a GitHub Pull Request, or email a Git patch (produced using
format-patch
) to jnthn@jnthn.net. Please also use
this email address to report security vulnerabilities.