2

I've been trying to figure out how to use x509 client auth with Network.AMQP. It seems like I need to create an AMQP.ConnectionOpts with (amongst others) the coTLSSettings parameter as follows:

import qualified Network.AMQP as AMQP
import Network.Connection
let opts = AMQP.ConnectionOpts {
    ..
  , coTLSSettings = Just $ AMQP.TLSCustom $ ...
}

At this point (the ellipsis), having read some of the Network.Connection documentation (and being rather out of my depth) it's starting to look very complicated. And I'm left wondering whether I'm going down the right path here.

So, my question(s): how do I implement x509 client auth easily? If the answer to that is "you can't," does anyone know where I can find an example of x509 client auth using the Network.Connection module?

mkingston
  • 2,678
  • 16
  • 26
  • So you want to authenticate the client, so the server can trust the client? – typetetris Jul 12 '17 at 11:09
  • Sorry about the delayed reply. That is correct. I'll test your answer sometime this afternoon and let you know how it goes. Thanks! – mkingston Jul 12 '17 at 15:05
  • Let me know if you have problems getting this to work. We could start a chat for that, too. – typetetris Jul 13 '17 at 15:53
  • Thanks a lot for the offer. I had a bit of time yesterday afternoon, but not enough. Then was delayed today. So it's tomorrow morning's priority. My plan is to attempt a little more to incorporate your answer into my work, but if that fails I'll run your answer standalone and post back here with results. Thanks again for your help. – mkingston Jul 13 '17 at 22:35

1 Answers1

2

We need to do two (for my test environment three) things.

  1. read the client credentials
  2. set a cipher suite as defaultParamsClient sets an empty cipher suite (I don't know why.).
  3. (for my test environment) read the CA root certificate, so we can validate the server certificate presented to us. If you are not in a test environment, then this certificate should be installed in the system certificate store and should be found be default. In this case you could drop the CertificateStore handling from the programm.

The function mkMyTLSSettings in the following program replaces the mentioned parts from the result of defaultParamsClient. In the function used as onCertificateRequest you could consume the argument and hand out different credentials depending on the argument values. The needed values themself are read in main to get rid of IO.

For the following program, I modified bits I found in this answer.

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Data.Default.Class
import Network.AMQP
import Network.Socket.Internal (PortNumber)
import Network.TLS
import Network.TLS.Extra.Cipher (ciphersuite_default)
import Data.X509.CertificateStore (CertificateStore (..), readCertificateStore)
import Data.Maybe
import qualified Data.ByteString as BS
import qualified Network.Connection as C
import qualified Data.ByteString.Lazy.Char8 as BL

mkMyTLSSettings :: CertificateStore -> Credential -> C.TLSSettings
mkMyTLSSettings castore creds =
  let defaultParams = defaultParamsClient "127.0.0.1" BS.empty
      newClientShared = (clientShared defaultParams) { sharedCAStore = castore }
      newClientSupported = (clientSupported defaultParams) { supportedCiphers = ciphersuite_default }
      newClientHooks = (clientHooks defaultParams) { onCertificateRequest = \_ -> return (Just creds) }
  in C.TLSSettings $ defaultParams { clientShared = newClientShared
                                   , clientSupported = newClientSupported
                                   , clientHooks = newClientHooks
                                   }

myTLSSettings :: CertificateStore -> Credential -> TLSSettings
myTLSSettings castore creds = TLSCustom $ mkMyTLSSettings castore creds

myTLSConnectionOpts :: TLSSettings -> ConnectionOpts
myTLSConnectionOpts opts = ConnectionOpts
  [("127.0.0.1", 5671 :: PortNumber)]
  "/"
  [plain "guest" "guest"]
  (Just 131072)
  Nothing
  (Just 1)
  (Just opts)

testConnectionOpts :: ConnectionOpts -> IO ()
testConnectionOpts opts = do 
   conn <- openConnection'' opts
   chan <- openChannel conn
   declareQueue chan newQueue {queueName = "hello"}
   putStrLn "Trying to register callback"
   consumeMsgs chan "hello" Ack myCallback
   publishMsg chan "" "hello" newMsg {msgBody = (BL.pack "hello world"), msgDeliveryMode = Just Persistent}
   getLine
   closeConnection conn
   putStrLn "connection closed"

main :: IO ()
main = do
  testConnectionOpts defaultConnectionOpts
  putStrLn "trying with tls"
  castore <- maybe (error "couldn't read CA root Certificate") id <$> (readCertificateStore "/pathto/rootCA.pem")
  creds <- either error id <$> credentialLoadX509 "/pathto/client.pem" "/pathto/client.key"
  let opts = myTLSSettings castore creds
  testConnectionOpts (myTLSConnectionOpts opts)

myCallback :: (Message, Envelope) -> IO ()
myCallback (msg, env) = do
  putStrLn $ "received message: " ++ (BL.unpack $ msgBody msg)
  ackEnv env

As a gist.

The first communication in this program I did to make sure that rabbitmq was setup properly and I really only encountered TLS errors. If you delete line 20 and 23 you can test wether you configured your rabbitmq correctly. The connection attempt in this case should fail, as we don't present a client certificate.

I created a toy CA for testing and issued a certificate for use with the rabbitmq server and one for the client. So I had a file rootCA.pem which stored the CA root certificate and files like rabbitmq.key and rabbitmq.pem which where used to setup TLS with rabbitmq. Also client.pem and client.key for the client. I configured rabbitmq to only serve clients which present a trustworthy certificate, by setting fail_if_no_peer_cert to true and setting the {verify, verify_peer} options.

On my first tries I got the kinds of errors with LeafNotV3 which means I created my rabbitmq.pem wrong on my first try. It was a X509.v1 certificate, which are not accepted by Network.TLS by default. I needed to make sure to create a X509.v3 certificate, which is done, by enabling certain extensions while issuing the certificate rabbitmq.pem, see here. I needed to add the option -req to that command line cited there to make it work.

typetetris
  • 4,586
  • 16
  • 31
  • Hey, I haven't got my own solution working totally yet; but I think your answer got me within reach. Thanks a lot. Hope you don't mind if I come crawling back with more questions later. (Though I hope not to). Thanks again, have a great weekend. – mkingston Jul 14 '17 at 15:47