Thursday, December 23, 2010

Creating a Kerberized client/server app in Ruby with GSSAPI

It's been awhile since I posted anything so I thought I'd write a little bit about the GSSAPI Ruby gem that I have been working on recently. GSSAPI and Kerberos are probably the biggest things I've longed for in Ruby for some time now. I work in an Enterprise environment for the State of North Dakota and we use Active Directory authentication for most everything. Since AD has Kerberos built-in, it opens a lot of options for authentication and encryption. I wanted to bring this functionality into the Ruby world so I wrote a gem that utilizes FFI and the underlying GSSAPI C libraries to do exactly that. Since the MIT Kerberos source comes with a sample client and server application I thought I would create a simple duplication of that in Ruby.

The Client

GSSAPI/Kerberos relies on a Service Principal Name(SPN) on the server. We specify it in the client code so that is why I mention it here. If you were creating an actual server for production you would want to create a separate SPN for it, but since this is a demo I will be using the 'host' SPN which should exist by the mere fact that the server is part of the Kerberos Realm. To run the client you will also need to have credentials of your own via 'kinit'.

Require the appropriate gems.
require 'gssapi'
require 'base64'
require 'socket'

Specify the server and the service name. Together these become the SPN.
host = 'example.org'
service = 'host'

Open up the connection to the server
sock = TCPSocket.new(host, 8082)

Initialize the GSSAPI security context and Base64 encode it so it is ready for transport. The Base64 encoding is not really necessary in this case, but since GSSAPI is often used in HTTP communication and is Base64 encoded there I am using it in this example. The GSSAPI::Simple wrapper is doing a lot in the background for you. It's importing the SPN for use in GSSAPI string types, managing memory and all of that good stuff. If you need to do something a bit out of the ordinary with GSSAPI and need to call the gss_* methods directly, check out the Simple wrapper for examples.
cli = GSSAPI::Simple.new(host, service)
tok = cli.init_context
stok = Base64.strict_encode64(tok)

Send the initial token, get back a response token and complete the security context. Once this step is done the set-up is complete and we are ready to start passing messages.
sock.write("#{stok}\n") # send initial token
stok = sock.gets.chomp # get back continuation token
ctx = cli.init_context(Base64.strict_decode64(stok.chomp)) # complete security context
puts "Connection #{(ctx ? 'succeeded' : 'failed')}"

Now just prompt for text from STDIN, encrypt the message and send it to the server. Do this until "exit" is typed on the command line.
begin
print "> "
msg = STDIN.gets.chomp
emsg = cli.wrap_message(msg)
sock.write("#{Base64.strict_encode64(emsg)}\n")
end while msg != 'exit'

sock.close

The Server


Set up is the same.
require 'gssapi'
require 'base64'
require 'socket'

The host and service should be the same as the client as well.
host = 'example.org'
service = 'host'

You won't always want to run as root so the system keytab may not be available. You can specify an alternate keytab as long as it contains the correct SPN.
keytab = "#{ENV['HOME']}/.gssapi/krb5.keytab" # this is optional, but probably required if not running as root

Start listening....
tcpsrv = TCPServer.new(host, 8082)

Initialize the security context for the server and acquire the credentials from the keytab.
srv = GSSAPI::Simple.new(host, service, keytab)
srv.acquire_credentials

Start the main server loop. This will listen for incoming requests, accept the incoming token, and send back a token so the client can complete the security context. After that it just listens for messages, decrypts them and prints them out. It will exit when it receives the "exit" message.
loop do
Thread.start(tcpsrv.accept) do |s|
print(s, "Accepted Connection\n")
stok = s.gets.chomp
print(s, "Received string#{stok}\n")
otok = srv.accept_context(Base64.strict_decode64(stok.chomp))
s.write("#{Base64.strict_encode64(otok)}\n")

begin
emsg = s.gets.chomp
msg = srv.unwrap_message(Base64.strict_decode64(emsg.chomp))
puts "Received: #{msg}"
end while msg != 'exit'

print(s, "Closing Socket\n")
s.close
end
end

That's really it and it's much shorter and easier to understand than the C counter-part. If you have issues running it make sure you have your credentials on the client (kinit) and the server has access to the appropriate keytab. These are the two big follies that will mess things up. Other than that have fun with the gssapi gem. If you use it and want to share, add your app to the gssapi gem wiki:
https://github.com/zenchild/gssapi/wiki/Users-of-GSSAPI

Cheers and Happy Solstice!