8.13 Authenticating with Kerberos

8.13.1 Problem

You need to authenticate using Kerberos.

8.13.2 Solution

If the client and the server are operating within the same Kerberos realm (or in separate realms, but cross-realm authentication is possible), you can use the user's credentials to authenticate from the client with the server. Both the client and the server must support this authentication method.

The code presented in this recipe assumes you are using either the Heimdal or the MIT Kerberos implementation. It further assumes you are using Version 5, which we consider reasonable because Version 4 has been obsolete for so many years. We do not cover the Windows interface to Kerberos in this book because of the significant difference in the API compared to Heimdal and MIT implementations, as well as the complexity of the SSPI API that is required on Windows. We do, however, present an equivalent recipe for Windows on the book's web site.

8.13.3 Discussion

First, we define a structure primarily for convenience. After a successful authentication, several pieces of information are passed back from the Kerberos API. We store each of these pieces of information in a single structure rather than adding several additional arguments to our authentication functions.

#include <krb5.h>
   
typedef struct {
  krb5_context      ctx;
  krb5_auth_context auth_ctx;
  krb5_ticket       *ticket;
} spc_krb5bundle_t;

On the client side, only the ctx and auth_ctx fields will be initialized. On the server side, all three fields will be initialized. Before passing an spc_krb5bundle_t object to either spc_krb5_client( ) or spc_krb5_server( ), you must ensure that auth_ctx and ticket are initialized to NULL. If the ctx field is not NULL, it should be a valid krb5_context object, which will be used instead of creating a new one.

Both the client and the server must be able to handle using Kerberos authentication. The code required for each side of the connection is very similar. On the client side, spc_krb5_client( ) will attempt to authenticate with the server. The code assumes that the user has already obtained a ticket-granting ticket from the appropriate Key Distribution Center (KDC), and that a credentials cache exists.

The function spc_krb5_client( ) has the following signature:

krb5_error_code spc_krb5_client(int sockfd, spc_krb5bundle_t *bundle,
                                char *service, char *host, char *version);

This function has the following arguments:

sockfd

Socket descriptor over which the authentication should be performed. The connection to the server should already be established, and the socket should be in blocking mode.

bundle

spc_krb5bundle_t object that will be loaded with information if the authentication with the server is successful. Before calling spc_krb5_client( ), you should be sure to zero the contents of this structure. If the structure contains a pointer to a Kerberos context object, spc_krb5_client( ) will use it instead of creating a new one.

service

Name component of the server's principal. It is combined with the server's hostname or instance to build the principal for the server. The server's principal will be of the form service/host@REALM. The realm is assumed to be the user's default realm.

host

Hostname of the server. It is used as the instance component of the server's principal.

version

Version string that is sent to the server. This string is generally used to indicate a version of the protocol that the client and server will speak to each other. It does not have anything to do with the Kerberos protocol or the version of Kerberos in use. The string may be anything you want, but both the client and server must agree on the same string for authentication to succeed.

If authentication is successful, the return value from spc_krb5_client( ) will be 0, and the relevant fields in the spc_krb5bundle_t object will be filled in. The client may then proceed to use other Kerberos API functions to exchange encrypted and authenticated information with the server. Of particular interest is that a key suitable for use with a symmetric cipher is now available. (See Recipe 9.6 for an example of how to use the key effectively.)

If any kind of error occurs while attempting to authenticate with the server, the return value from the following spc_krb5_client( ) function will be the error code returned by the Kerberos API function that failed. Complete lists of error codes are available in the Heimdal and MIT Kerberos header files.

krb5_error_code spc_krb5_client(int sockfd, spc_krb5bundle_t *bundle,
                                char *service, char *host, char *version) {
  int             free_context = 0;
  krb5_principal  server = 0;
  krb5_error_code rc;
   
  if (!bundle->ctx) {
    if ((rc = krb5_init_context(&(bundle->ctx))) != 0) goto error;
    free_context = 1;
  }
  if ((rc = krb5_sname_to_principal(bundle->ctx, host, service,
                                    KRB5_NT_SRV_HST, &server)) != 0) goto error;
   
  rc = krb5_sendauth(bundle->ctx, &(bundle->auth_ctx), &sockfd, version,
                     0, server, AP_OPTS_MUTUAL_REQUIRED, 0, 0, 0, 0, 0, 0);
  if (!rc) {
    krb5_free_principal(bundle->ctx, server);
    return 0;
  }
   
error:
  if (server) krb5_free_principal(bundle->ctx, server);
  if (bundle->ctx && free_context) {
    krb5_free_context(bundle->ctx);
    bundle->ctx = 0;
  }
  return rc;
}

The code for the server side of the connection is similar to the client side, although it is somewhat simplified because most of the information in the exchange comes from the client. The function spc_krb5_server( ), listed later in this section, performs the server-side part of the authentication. It ultimately calls krb5_recvauth( ), which waits for the client to initiate an authenticate request.

The function spc_krb5_server( ) has the following signature:

krb5_error_code spc_krb5_server(int sockfd, spc_krb5bundle_t *bundle,
                                char *service, char *version);

This function has the following arguments:

sockfd

Socket descriptor over which the authentication should be performed. The connection to the client should already be established, and the socket should be in blocking mode.

bundle

spc_krb5bundle_t object that will be loaded with information if the authentication with the server is successful. Before calling spc_krb5_server( ), you should be sure to zero the contents of this structure. If the structure contains a pointer to a Kerberos context object, spc_krb5_server( ) will use it instead of creating a new one.

service

Name component of the server's principal. It is combined with the server's hostname or instance to build the principal for the server. The server's principal will be of the form service/hostname@REALM.

On the client side, an additional argument is required to specify the hostname of the server, but on the server side, the hostname of the machine on which the program is running will be used.

version

Version string that is generally used to indicate a version of the protocol that the client and server will speak to each other. It does not have anything to do with the Kerberos protocol or the version of Kerberos in use. The string may be anything you want, but both the client and server must agree on the same string for authentication to succeed.

If authentication is successful, the return value from spc_krb5_server( ) will be 0, and the relevant fields in the spc_krb5bundle_t object will be filled in. If any kind of error occurs while attempting to authenticate with the server, the return value from spc_krb5_server( ) will be the error code returned by the Kerberos API function that failed.

krb5_error_code spc_krb5_server(int sockfd, spc_krb5bundle_t *bundle,
                                char *service, char *version) {
  int             free_context = 0;
  krb5_principal  server = 0;
  krb5_error_code rc;
   
  if (!bundle->ctx) {
    if ((rc = krb5_init_context(&(bundle->ctx))) != 0) goto error;
    free_context = 1;
  }
  if ((rc = krb5_sname_to_principal(bundle->ctx, 0, service,
                                    KRB5_NT_SRV_HST, &server)) != 0) goto error;
   
  rc = krb5_recvauth(bundle->ctx, &(bundle->auth_ctx), &sockfd, version,
                     server, 0, 0, &(bundle->ticket));
  if (!rc) {
    krb5_free_principal(bundle->ctx, server);
    return 0;
  }
   
error:
  if (server) krb5_free_principal(bundle->ctx, server);
  if (bundle->ctx && free_context) {
    krb5_free_context(bundle->ctx);
    bundle->ctx = 0;
  }
  return rc;
}

When a successful authentication is completed, an spc_krb5bundle_t object is filled with information resulting from the authentication. This information should eventually be cleaned up, of course. You may safely keep the information around as long as you need it, or you may clean it up at any time. If, once the authentication is complete, you don't need to retain any of the resulting information for further communication, you may even clean it up immediately.

Call the function spc_krb5_cleanup( )when you no longer need any of the information contained in an spc_krb5bundle_t object. It will free all of the allocated resources in the proper order.

void spc_krb5_cleanup(spc_krb5bundle_t *bundle) {
  if (bundle->ticket) {
    krb5_free_ticket(bundle->ctx, bundle->ticket);
    bundle->ticket = 0;
  }
  if (bundle->auth_ctx) {
    krb5_auth_con_free(bundle->ctx, bundle->auth_ctx);
    bundle->auth_ctx = 0;
  }
  if (bundle->ctx) {
    krb5_free_context(bundle->ctx);
    bundle->ctx = 0;
  }
}

8.13.4 See Also

Recipe 9.6