Thursday, May 22, 2008

SSL/TLS support added

Some time last year, Elie Chaftari implemented an OpenSSL binding in Factor (and by the way, Elie, good luck with Medioset!). I took this binding and extended it into a high-level SSL library which integrates with Factor's streams and sockets. The effort involved was non-trivial, because OpenSSL's nonblocking I/O behavior is somewhat confusing and not very well documented. So far it only works on Unix. Integration with the Windows overlapped I/O system is still pending.
I will start by presenting a simple client/server application that uses standard sockets, then convert it to use SSL.
We will implement an application which sends the current time of day, formatted as an RFC822 string, to the user.
First of all, how do we print such a string? We get the current time with now, which pushes a timestamp object on the stack, then convert it to an RFC822 string with timestamp>rfc822:
( scratchpad ) USING: calendar calendar.format ;
( scratchpad ) now timestamp>rfc822 print
Thu, 22 May 2008 01:25:18 -0500

We want to send this to every client that connects, so we use the with-server combinator from the io.server vocabulary. It takes four parameters:
  • A sequence of addresses to listen on. We create these addresses by passing a port number to the internet-server word. It gives us back a sequence of addresses:
    ( scratchpad ) 9670 internet-server .
    {
    T{ inet6 f "0:0:0:0:0:0:0:0" 9670 }
    T{ inet4 f "0.0.0.0" 9670 }
    }
  • A service name for logging purposes. We pass "daytime", which means that connections will be logged to the logs/daytime/1.log file in the Factor directory. We can then invoke rotate-logs to move 1.log to 2.log, and so on. Log analysis tools are available too. I've discussed this in a prior blog post.
  • Finally, a quotation which is run for each client connection, in a new thread. Our quotation just prints the current time:
    [ now timestamp>rfc822 print ]
  • An I/O encoding specifier for client connections. We only care about ASCII so we pass ascii.

Here is a daytime-server word, with the complete with-server form:
USING: calendar.format calendar io.server io.encodings.ascii ;

: daytime-server ( -- )
9670 internet-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;

We can start our server as follows:
USE: threads

[ daytime-server ] in-thread

Now, let's test it with telnet:
slava$ telnet localhost 9670
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Thu, 22 May 2008 01:32:17 -0500
Connection closed by foreign host.

Works fine. The log file logs/daytime/1.log will have now logged something like the following:
[2008-05-22T01:32:17-05:00] NOTICE accepted-connection: { T{ inet6 f "0:0:0:0:0:0:0:1" 50916 } }

Now, let's write a client for this service. We'll write the client using the with-client combinator from io.sockets. It takes three parameters:
  • An address specifier. This is the server to connect to. We're going to connect to port 9670 on localhost, so we pass "localhost" 9670 <inet>.
  • An encoding specifier. Again, we're only interested in 7-bit ASCII, so we pass ascii.
  • The final parameter is a quotation. We wish to a line of input from the server and leave it on the stack, so we pass [ readln ].

Here is the complete with-client form:
USING: io io.sockets io.encodings.ascii ;

"localhost" 9670 <inet> ascii [ readln ] with-client

We're not quite done though; we should really parse the timestamp from its RFC822 string representation back into a timestamp object. We can do this with the rfc822>timestamp word. Here is the complete daytime-client word, refactored to take a host name from the stack:
USING: calendar.format io io.sockets io.encodings.ascii ;

: daytime-client ( hostname -- timestamp )
9670 <inet> ascii [ readln ] with-client
rfc822>timestamp ;

Let's test our program, using the describe word to get a verbose description of the timestamp returned:
( scratchpad ) "localhost" daytime-client describe
timestamp instance
"delegate" f
"year" 2008
"month" 5
"day" 22
"hour" 1
"minute" 37
"second" 47
"gmt-offset" T{ duration f 0 0 0 -5 0 0 }

So we're done! Here is the complete program:
USING: calendar.format calendar
io io.server io.sockets
io.encodings.ascii ;
IN: daytime

: daytime-server ( -- )
9670 internet-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;

: start-daytime-server ( -- )
[ daytime-server ] in-thread ;

: daytime-client ( hostname -- timestamp )
9670 <inet> ascii [ readln ] with-client
rfc822>timestamp ;

You can put this in a file named work/daytime/daytime.factor in the Factor directory, then enter the following in the listener:
( scratchpad ) USE: daytime
Loading P" resource:work/daytime/daytime.factor"
( scratchpad ) start-daytime-server
( scratchpad ) "localhost" daytime-client describe

So we have a simple client/server application with support for multiple connections and logging.

Let's add SSL support! There are four things to change.

  • We need to add io.sockets.secure to our USING: list:
    USING: calendar.format calendar
    io io.server io.sockets io.sockets.secure
    io.encodings.ascii ;

  • We change daytime-server to accept SSL connections by replacing internet-server with secure-server; we also change the port number:
    : daytime-server ( -- )
    9671 secure-server
    "daytime"
    ascii
    [ now timestamp>rfc822 print ]
    with-server ;

    The secure-server word is much like internet-server in that it takes a port number and outputs a list of addresses, except this time the addresses are secure addresses, which means that a server socket listening on one will accept SSL connections:
    ( scratchpad ) 9671 secure-server .
    {
    T{ secure f T{ inet6 f "0:0:0:0:0:0:0:0" 9671 } }
    T{ secure f T{ inet4 f "0.0.0.0" 9671 } }
    }


  • Next, we change start-daytime-server to establish SSL configuration parameters. We need two things here, Diffie-Hellman key exchange parameters, and a certificate for the server.
    Diffie-Hellman key exchange parameters can be generated with a command-line tool:
    openssl dhparam -out dh1024.pem 1024

    There are several ways to go about obtaining a server certificate. You can either fork out some money to a CA such as VeriSign or GoDaddy (prices range from $15 to $3000), or you can generate a self-signed certificate, for example by following these directions. Once you have the two files, dh1024.pem and server.pem, place them inside work/daytime/, and modify start-daytime-server to wrap the daytime-server call with the with-secure-context combinator. This combinator takes a secure-config object from the stack, and we fill in the slots of this object with the pathnames of the two files we just generated:
    : start-daytime-server ( -- )
    [
    <secure-config>
    "resource:work/daytime/dh1024.pem" >>dh-file
    "resource:work/daytime/server.pem" >>key-file
    [ daytime-server ] with-secure-context
    ] in-thread ;

    Note that you must nest with-secure-context inside in-thread, not vice versa, because in-thread returns immediately, and with-secure-context destroys the context after its quotation returns.

  • Finally, we change daytime-client to establish SSL connections. It suffices to change <inet> to <inet> <secure>, and use the new port number we defined in daytime-server. This makes with-client establish an SSL connection:
    : daytime-client ( hostname -- timestamp )
    9671 <inet> <secure> ascii [ readln ] with-client
    rfc822>timestamp ;

    However, by default, the client will validate the server's certificate, and unless you're using a certificate signed by a root CA, this will fail.
    To disable verification, we can wrap client calls in an SSL configuration with verification disabled:
    <secure-config> f >>verify [ "localhost" daytime-client ] with-secure-context



Here is our complete daytime client/server vocabulary which uses SSL:
USING: calendar.format calendar
io io.server io.sockets io.sockets.secure
io.encodings.ascii ;
IN: daytime

: daytime-server ( -- )
9671 secure-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;

: start-daytime-server ( -- )
[
<secure-config>
"resource:work/daytime/dh1024.pem" >>dh-file
"resource:work/daytime/server.pem" >>key-file
[ daytime-server ] with-secure-context
] in-thread ;

: daytime-client ( hostname -- timestamp )
9671 <inet> <secure> ascii [ readln ] with-client
rfc822>timestamp ;

: daytime-client-no-verify ( hostname -- timestamp )
#! Use this if your server has a self-signed certificate
<secure-config> f >>verify
[ daytime-client ] with-secure-context ;

That's it.
There is one more topic to cover, and that is mixed secure/insecure servers. Often you want to listen for insecure connections on one port, and secure connections on another. We can achieve this easily enough by concatenating the results of internet-server and secure-server:
( scratchpad ) 9670 internet-server 9671 secure-server append .
{
T{ inet6 f "0:0:0:0:0:0:0:0" 9670 }
T{ inet4 f "0.0.0.0" 9670 }
T{ secure f T{ inet6 f "0:0:0:0:0:0:0:0" 9671 } }
T{ secure f T{ inet4 f "0.0.0.0" 9671 } }
}

The server will now listen on all four addresses, and the quotation can differentiate between insecure and secure clients by checking if the value of the remote-address variable is an instance of secure or not using the secure? word.
So what is missing? Several things:
  • As I've mentioned at the beginning, SSL sockets don't yet work on Windows. This will be addressed soon.
  • Session resumption is not supported, although this is relatively easy to implement; I just wanted to get everything else working first.
  • Client-side authentication isn't supported either. Again, this is pretty easy, and I'll address it soon.
  • While it is very easy to create client and server SSL sockets (just wrap your addresses with <secure>) there is currently no way to upgrade a socket that has already been opened to an SSL socket. This is needed for SMTP/TLS support. I haven't thought of a nice API for this, as soon as I do I will implement it.
  • Better control is needed over cypher suites and certificate validation.

2 comments:

Elie Chaftari said...

Slava, thanks a lot for your encouragement. I know that you'll keep enhancing the library but it's already one of the cleanest implementations I've seen.

radix42 said...

Oh sweet, now I just need to bug my webhost into upgrading from FreeBSD 6.3 to 7.x so I can run factor there.