External SASL with mutual authentication through certificates

Jun 19, 2015 at 2:27 AM
I have two applications - one acting as a server and one acting as a client. I am working on setting up security so that the peers mutually authenticate with certificates. I am attempting to do this by setting the SASL profile to external and hooking into the SSL handshake to do some extra application specific certificate validation on both the client and server.

For the server I am using ContainerHost. The listeners are not publicly accessible. What is the intended way to set a listener SASL profile to external and tie into the SslSettings.RemoteCertificateValidationCallback callback? Should I be creating a ConnectionListener directly? Or is there some other way that I am completely missing?

On the client side I am using ConnectionFactory and can access the ConnectionFactory.SSL and ConnectionFactory.SASL properties.

I am trying to write a rough prototype right now but currently get an exception when trying to create a connection from the connection factory on the client side. Any examples or documentation to get me started are appreciated. Thanks!

On a side note...after investigating other AMQP 1.0 options this library is filling a huge void in the .NET AMQP community. Thanks and hope to see it continue to grow!


The exception at line 347 of ConnectionListener:
System.AggregateException was unhandled
  HResult=-2146233088
  Message=One or more errors occurred.
  Source=mscorlib
  StackTrace:
       at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
       at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
       at System.Threading.Tasks.Task`1.get_Result()
       at AMQP.NET_Lite.Security.Program.RunClient() in c:\dev\AMQP.NET_Lite\AMQP.NET_Lite.Security\AMQP.NET_Lite.Security\Program.cs:line 90
       at AMQP.NET_Lite.Security.Program.Main(String[] args) in c:\dev\AMQP.NET_Lite\AMQP.NET_Lite.Security\AMQP.NET_Lite.Security\Program.cs:line 22
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: System.IO.IOException
       HResult=-2146232800
       Message=Authentication failed because the remote party has closed the transport stream.
       Source=System
       StackTrace:
            at System.Net.Security.SslState.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
            at System.Net.Security.SslState.EndProcessAuthentication(IAsyncResult result)
            at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
            at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
         --- End of stack trace from previous location where exception was thrown ---
            at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
            at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
            at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
            at Amqp.TcpTransport.<ConnectAsync>d__5.MoveNext() in d:\Source\Repos\amqpnetlite\src\Net\TcpTransport.cs:line 103
         --- End of stack trace from previous location where exception was thrown ---
            at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
            at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
            at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
            at Amqp.ConnectionFactory.<CreateAsync>d__2.MoveNext() in d:\Source\Repos\amqpnetlite\src\Net\ConnectionFactory.cs:line 129
       InnerException: 
using Amqp;
using Amqp.Listener;
using Amqp.Sasl;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AMQP.NET_Lite.Security
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.Run(() => RunServer());
            Thread.Sleep(500);
            RunClient();

            Console.WriteLine("done");
            Console.Read();
        }

        const string Address = "amqps://localhost:5672";
        const string MsgProcessorName = "msg_processor";

        private static X509Certificate2 GetCertificate()
        {
            X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
            store.Open(OpenFlags.ReadOnly);

            try
            {
                //a self signed test certificate
                return store.Certificates.Find(X509FindType.FindBySerialNumber, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx", false)[0];
            }
            finally
            {
                store.Close();
            }
        }

        public static bool ValidateServerCertificate(
            object sender,
            X509Certificate certificate,
            X509Chain chain,
            SslPolicyErrors sslPolicyErrors)
        {
            //TODO - validation
            Console.WriteLine("server cert validation called");
            return true;
        }

        static void RunServer()
        {
            Uri addressUri = new Uri(Address);
            ContainerHost host = new ContainerHost(new Uri[] { addressUri }, GetCertificate(), addressUri.UserInfo);
            host.RegisterMessageProcessor(MsgProcessorName, new MessageProcessor());

            //TODO - how can I set the SASL profile to external?
            //TODO - how do I validate the client certificate?

            try
            {
                host.Open();
                Console.Read();
            }
            finally
            {
                host.Close();
                Console.WriteLine("Server closed");
            }
        }

        static void RunClient()
        {
            var connFactory = new ConnectionFactory();

            //SSL settings
            connFactory.SSL.ClientCertificates = new X509CertificateCollection() { GetCertificate() };
            connFactory.SSL.RemoteCertificateValidationCallback = ValidateServerCertificate;

            //SASL settings
            connFactory.SASL.Profile = SaslProfile.External;

            var connection = connFactory.CreateAsync(new Address(Address)).Result;
            var session = new Session(connection);
            var sender = new SenderLink(session, "message-client", MsgProcessorName);

            try
            {
                sender.Send(new Message(new byte[4] { 0, 1, 2, 3 }));
                Console.WriteLine("msg sent");
            }
            finally
            {
                session.Close();
                session.Close();
                connection.Close();
            }
        }
    }

    class MessageProcessor : IMessageProcessor
    {
        public int Credit
        {
            get { return 300; }
        }

        public void Process(MessageContext messageContext)
        {
            Console.WriteLine("msg received");
            messageContext.Complete();
        }
    }
}
Coordinator
Jun 20, 2015 at 1:23 AM
I have added support for this scenario. Listener can now be configured using the setting properties. This example shows more details.
https://amqpnetlite.codeplex.com/SourceControl/latest#Examples/PeerToPeer/PeerToPeer.Certificate/Program.cs
Jun 20, 2015 at 6:15 PM
Wow! Thanks for the quick response and action on this. I'll give it a try this weekend and let you know how it worked out.