Click here to Skip to main content
Click here to Skip to main content

Halibut: a secure communication stack for .NET/Mono

By , 28 Feb 2013
 

Introduction

Halibut is an open source, secure communication stack for .NET and Mono. It uses a JSON-RPC style protocol over SSL, and is a lightweight alternative to WCF.  

The source code and samples in this article are available on GitHub

Background  

Halibut came about as an attempt to build a secure alternative to WCF for use in Octopus Deploy, an automated deployment product for .NET developers.

As an automated deployment tool, Octopus needs to be able to push packages and configuration information to machines that might be on the local network or in the cloud. To do this securely, users of Octopus establish a two-way trust relationship:

  • The Octopus server, which co-ordinates deployments, needs to know it is sending packages and configuration information to a machine that it trusts, not an imposter. 
  • The Tentacle agent, which receives and installs the packages, needs to know that it is receiving packages from an Octopus server that it trusts, and not an imposter.  

To do this, Octopus uses public/private key cryptography. On installation, the Octopus and Tentacle servers both generate an X.509 certificate. The administrator who sets up each machine then pastes the thumbprint of the public key of each machine to the other, establishing the trust. 

In Octopus Deploy, we use those X.509 certificates with WCF's wsHttpBinding stack. When the connection is established, each side verifies the thumbprint of the certificate presented by the other party. If the public key isn't what we expect, we reject the connection. 

(You can read more about how this works on our reference page.) 

In a future release, we'd love to add Mono support to Octopus, so that we can deploy packages to Linux and other Unix-like operating systems. But there's a problem: WCF's wsHttpBinding isn't supported on Mono!

Faced with this problem, we needed to come up with an alternative communication stack.  

Goals of Halibut  

When building Halibut, we had a few goals in mind: 

  • Open source: We felt that making the code for our communication stack open to the world would help to increase the security of such a critical piece of our architecture.  
  • Secure: We still wanted public/private key cryptography to be the cornerstone of our secure communication stack.   
  • Simple: We wanted to write the least code possible, and to make it super easy to get started with.  
  • Friendly: Error messages can be bad enough, but security types are especially good at writing convoluted error messages that users can never understand. We wanted to make sure that any errors coming out of our stack would be easy to understand.  
  • Stand on the shoulders of giants: We wanted to leverage code written by people who know far more about implementing secure communication stacks than ourselves. Our goal would just be to join a few pieces together into an easy to use package.  
  • Works on Mono and .NET: We needed to choose technologies that would work on both platforms. 

 After lots of experimentation, we ended up building Halibut on top of: 

  • TcpListener/TcpClient, to bind to the TCP sockets. We use raw TCP rather than HTTP to avoid relying on HTTP.sys under Windows, since it requires additional permissions to work with.    
  • SslStream. This class is baked into .NET and Mono, and takes care of encrypting the connections between the two services using SSL. It handles presenting the certificates and negotiating the encryption levels to use. We use TLS 1.0.  
  • Json.NET: This has become the standard for JSON on .NET and Mono, so it was the logical choice when it came to serializing the objects to send on the wire. Using JSON also means that we can more easily handle backwards compatibility and versioning than if we'd chosen a proprietary binary format.  
Once a secure connection is established, Halibut uses a protocol based on JSON-RPC. A request is sent, containing the method name to call and arguments. The server processes the request and returns a response object. If an error occurs, Halibut catches it and returns an error object, and the error is rethrown on the client.

Show us the code! 

To get started with Halibut, first define an interface for your service:

public interface ICalculatorService
{
    long Add(long a, long b);
    long Subtract(long a, long b);
}  

There's no need to decorate it with anything, Halibut assumes all methods on the interface are available for RPC. On your server, implement the interface: 

public class CalculatorService : ICalculatorService
{
    public long Add(long a, long b)
    {
        return a + b;
    }

    public long Subtract(long a, long b)
    {
        return a - b;
    }
} 

Next, load a certificate which your server will use to identify itself. The certificate needs to have a public key as well as a private key. To make it easy to create certificates, the sample code comes with an executable (Halibut.CertificateGenerator.exe) which you can call like this:

Halibut.CertificateGenerator.exe CN=YourApplication YourApplication.pfx  

This creates a file containing the public and private key of a certificate identifying YourApplication in the example above.  

Load the certificate in your server:

var certificate = new X509Certificate2("HalibutServer.pfx"); 

Finally, start your service. Here we are listening on port 8433: 

var endPoint = new IPEndPoint(IPAddress.Any, 8433);

var server = new HalibutServer(endPoint, certificate);
server.Services.Register<ICalculatorService, CalculatorService>();
server.Start();  

Now we need to connect our client. Again, use Halibut.CertificateGenerator.exe to generate a certificate, and load it on your client: 

var certificate = new X509Certificate2("HalibutClient.pfx"); 

Now connect to the server:

var serverThumbprint = "EF3A7A69AFE0D13130370B44A228F5CD15C069BC";
var client = new HalibutClient(certificate);
var calculator = client.Create<ICalculatorService>(new Uri("rpc://" + hostName + ":8433/"), serverThumbprint);
var result = calculator.Add(12, 18); 

Note that in the example above, our client specifies the thumbprint of the public key of the server. This way, our client will abort the connection if the server identifies itself as a server we weren't expecting.   

Our server is currently accepting connections from any valid client however. We can change this by adding a validation callback to the server before calling Start():

server.Options.ClientCertificateValidator = ValidateClientCertificate;

...

static CertificateValidationResult ValidateClientCertificate(X509Certificate2 clientcertificate)
{
    return clientcertificate.Thumbprint == "2074529C99D93D5955FEECA859AEAC6092741205"
        ? CertificateValidationResult.Valid
        : CertificateValidationResult.Rejected;
}  

Clients 

The HalibutClient class is a factory that creates instances of proxies that connect to a Halibut server. When you call:

var calculator = halibutClient.Create<ICalculatorService>(...); 

HalibutClient returns a proxy (implemented using RealProxy). When a method is called on the proxy, Halibut connects to the server, serializes the JSON requests, and sends them. It deserializes the result from the server, and returns the result as the result of the method call. If the server returns an error, or the connection fails, the proxy will throw.  

Halibut doesn't use sessions and doesn't keep connections open, so each call is completely stateless. 

Extension points  

The HalibutServer class provides an Options property which can be used to customize and extend Halibut: 

  • Options.ServiceFactory: If you want to control how services are constructed (e.g., to call an IOC container), implement IServiceFactory. Construct the object, returning it in a disposable object that will be disposed when the service call is complete.  
  • Options.Serializer: returns the Json.NET serializer that Halibut uses. Use it to customize serialization settings. 
  • Options.ServiceInvoker: control how methods on the service are invoked.    

Logging

Halibut automatically logs messages using trace sources, and they are compatible with the WCF service trace viewer. To enable logging, add the following to your configuration file:

<?xml version="1.0" encoding="utf-8"?>

<configuration>
  <system.diagnostics>
    <sources>
      <source name="Halibut.Client" switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
          <add name="console" />
        </listeners>
      </source>
      <source name="Halibut.Server" switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
          <add name="console" />
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="Halibut.Client" value="Verbose" />
      <add name="Halibut.Server" value="Verbose" />
    </switches>
    <sharedListeners>
      <add name="xml" type="System.Diagnostics.XmlWriterTraceListener" initializeData="starling.e2e" />
      <add name="console" type="System.Diagnostics.ConsoleTraceListener" />
    </sharedListeners>
    <trace autoflush="true" />
  </system.diagnostics>
</configuration> 

Halibut sends trace activity ID's over the wire too, so if you use the XML logger above, you can open both the client and server log messages in the same Service Trace Viewer instance to see the log messages joined together, giving you beautiful end-to-end tracing.   

Summary 

Halibut was built as an alternative to using WCF's wsHttpBinding, primarily because wsHttpBinding isn't supported on Mono, and we needed something to use. It is open source, simple, and secure. I hope you find a use for it in your projects too, or at least, you enjoy reading the code. Thanks! 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Paul Stovell
Octopus Deploy
Australia Australia
Member
My name is Paul Stovell. I live in Brisbane and develop an automated release management product, Octopus Deploy. Prior to working on Octopus I worked for an investment bank in London, and for Readify. I also work on a few open source projects. I am a Microsoft MVP for Client Application Development.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionpgpmemberLeblanc Meneses14 Mar '13 - 11:36 
I would have solved this problem with pgp - bouncycastle is supported on mono already.
 

What is the benefit of using halibut vs bouncycastle pgp libraries?
 

From octopus server you would need to encrypt using ( tentacle public key, octopus server private key)
 
tcp
 
From tentacle.exe you would need to decrypt the message (tentacle private key, octopus server public key)
GeneralMy vote of 5memberMinion no. 51 Mar '13 - 0:28 
A good combination of open source and ssecurity.
GeneralMy vote of 5memberVitorHugoGarcia28 Feb '13 - 22:41 
nice one
QuestionThis looks really interestingmemberGarth J Lancaster28 Feb '13 - 20:51 
... but I have a question
 
You wrote "Note that in the example above, our client specifies the thumbprint of the public key of the server. This way, our client will abort the connection if the server identifies itself as a server we weren't expecting."
 
really ? to me, that seems @rse about face - the server would abort the connection if the client wasnt a client we were expecting seems more 'how it usually works' - in a client, you usually point it at a server so why would the client then say 'nah. Im not going to connect to a server thats just accepted the connection'
 
I can understand if its a 'uddi' type thing, where several servers may be advertising services .. just not as you suggest it
 
but Ive bookmarked it for further study, nice work
 
'g'
AnswerRe: This looks really interestingmemberPaul Stovell28 Feb '13 - 21:36 
Thanks, good question.
 
When you open a web browser and visit a your bank's website, the bank (server) presents a certificate. Your browser (the client) actually validates that certificate. It checks that the certificate is valid for the domain, and that it was issued by a trusted party. Your browser doesn't just accept whatever certificate it's given. If you entered your banks URL, and the certificate was issued by the Russian mafia, then why would the browser continue the connection? This is to prevent man in the middle attacks.
 
For the same reason, our client validates the certificate of the server it is connecting to, just as the server validates the client. Only instead of checking certificate chains, we just check the thumbprint (because it's more secure to verify the certificate itself than to rely on third parties). That's what makes this a two-way trust relationship.
 
Hope that helps to clarify things,
 
Paul
GeneralRe: This looks really interestingmemberGarth J Lancaster28 Feb '13 - 22:47 
thanks - good explanation Smile | :)
 
and, I do agree with only checking the thumbprint
 
nice
 
'g'

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 1 Mar 2013
Article Copyright 2013 by Paul Stovell
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid