Obtaining an encrypted and authenticated communication channel in Haskell has, sadly, proven to be more complex than necessary. Over the past few years I've observed people using cryptographic primitives to roll their own solution and some people have made custom higher level libraries that often bring in additional C-library dependencies, restrictions such as thread safety, or even both. Another solution might be to glue together two of the many client and server TLS libraries, but I haven't seen any one library that supports both client and server; there's also a lack of information about interoperability, performance, and ease of use when combining two libraries.
For these reasons I have created the commsec and commsec-keyexchange packages that allow for authenticated, integrity and confidentiallity protected, communications.
The CommSec library sends and receives information in datagrams much like how IPSec ESP works. Each plaintext sending is transmitted as \([ LEN | IV | CT | TAG ]\) where LEN is a 4 byte length encoding of the message size (not including itself), IV is the 8 byte counter, CT is the AES GCM encryption of the plaintext under said counter, and TAG is the GCM authentication tag.
The key exchange is based on the RSA-authenticated station to station protocol. This is just an authenticated DH scheme: each side generates \(x (\bmod p)\) and \(y(\bmod p)\), where \(p\) is a public large prime. They compute and share \(a^x\) and \(a^y\), where \(a\) is a generator. We then have shared secret \(a^{xy}\) by computing \((a^x)^y\) or \((a^y)^x\). The result is then verified using an RSA signature, transferring \(\text{AES-CTR}(RSA(a^x, a^y))\) where the AES key is derived from the newly shared secret and each party uses their own private key for signing.
You first need to generate RSA keys and share the public key between your communicating systems:
$ ssh-keygen
...enter a path...
After that you can using commsec-keyexchange to perform an authenticated key exchange and send/recv data.
import Crypto.PubKey.OpenSsh
import qualified Data.ByteString as B
import Network.CommSec.KeyExchange
main = do
-- Step 1: (not shown) Get file paths, host, and port.
-- Step 2: Read in the keys
let dec d f = ether error id . d `fmap` B.readFile f
OpenSshPrivateKeyRsa priv <- dec decodePrivate myPrivateKeyFile
OpenSshPublicKeyRsa them _ <- dec decodePublic theirPublicKeyFile
The first thing we do is obtain the RSA keys - our private key and their public key.
-- Step 3: Listen for and accept a connection
-- (alternatively: connect to a listener).
ctx <- if doAccept
then accept host port them priv
else connect host port them priv
After that we can perform a key exchange, which allows establishes a socket and shared key.
-- Step 4: Use the communication contexts along with
-- the send and recv primitives to communicate.
send ctx "Hello!"
recv ctx >>= print
I benchmarked commsec to compare with the package I am replacing, secure-sockets:
Package (sending+recv) | |
CommSec | 16 |
CommSec | 2048 |
secure-sockets package | 16 |
secure-sockets package | 2048 |
These results aren't surprising. The design of commsec means it probably has lower overhead costs, but the GCM routines aren't as well optimized. This means that commsec will be faster on the small packets while secure-sockets is faster on larger messages (until I switch to a faster AES routine or someone optimizes the current one).
These libraries come with very strong warning: there is no guarentee of correctness. These libraries were not developed with any rigor or peer review!
As part of the build-up to this package, additional work was done on the crypto-pubkey-openssh package to ensure we could use ssh RSA keys generated from your typical ssh-keygen command, as well as work on a cipher-aes derivative to improve the throughput for small messages.