SSL/TLS support added (original) (raw)
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 thelogs/daytime/1.log
file in the Factor directory. We can then invokerotate-logs
to move1.log
to2.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 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 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 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 ourUSING:
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 replacinginternet-server
withsecure-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 15to15 to 15to3000), 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 ( -- )
[
"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 indaytime-server
. This makeswith-client
establish an SSL connection:
: daytime-client ( hostname -- timestamp )
9671 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:
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 ( -- )
[
"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 ascii [ readln ] with-client
rfc822>timestamp ;
: daytime-client-no-verify ( hostname -- timestamp )
#! Use this if your server has a self-signed certificate
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.