GSS-API Programming Guide

Chapter 2 A Walk–Through of the Sample GSS-API Programs

This chapter describes two sample GSS-API application programs. This chapter provides the following information:

Introduction to the Sample Programs

The source code for two C-language applications that make use of the GSS-API is provided in Appendix A, Sample C–Based GSS-API Programs. One sample application is for a client and the other for a server. This chapter guides you through those applications, step-by-step; it is intended to be read while referring to the cource code. It does not attempt to explain every facet of the applications in detail. Rather, it focuses on the aspects that relate to using the GSS-API.


Caution – Caution –

Because the GSS_API does not automatically clean up after itself, applications and functions using the GSS-API must do so themselves. This means that functions that use GSS-API buffers or GSS-API namespaces, for example, should call GSS-API functions such as gss_release_buffer() and gss_release_name() when they are finished.

To save space and avoid repetition, we have generally not included such cleanup in the following code walk-through. Be aware that it must be performed. However; refer to Appendix A, Sample C–Based GSS-API Programs to see the sample programs in full if you are unsure how or when to use the cleanup functions.


Client-Side GSS-API: gss-client

The sample client-side program, gss-client, creates a security context with a server, establishes security parameters, and sends a string (the “message”) to the server. It uses a simple TCP-based sockets connection to make its connection.

gss-client takes this form on the command line:



gss-client [-port port] [-d] [-mech mech] host service [-f] msg

Specifically, gss-client does the following:

  1. Parses the command line.

  2. Creates an OID (object ID) for a mechanism, if specified.

  3. Creates a connection to the server.

  4. Establishes a context.

  5. Wraps the message.

  6. Sends the message.

  7. Verifies that the message has been “signed” correctly by the server.

Following is a step-by-step description of how gss-client works. Because it is a sample program designed to show off functionality, the parts of the program that do not closely relate to the steps above are skipped. Some features, such as importing and exporting contexts, or getting a wrap size, are discussed elsewhere in this manual.

Overview: main() (Client)

As with all C programs, the outer shell of the program is contained in the entry-point function, main(). main() performs four functions:

  1. It parses command-line arguments, assigning them to variables:

    • If specified, port is the port number for making the connection to the remote machine specified by host.

    • If the -d flag is set, security credentials should be delegated to the server. Specifically, the deleg_flag variable is set to the GSS-API value GSS_C_DELEG_FLAG; otherwise deleg_flag is set to zero.

    • mech is the (optional) name of the security mechanism, such as Kerberos v5 or X.509, to use. If no mechanism is specified, the GSS-API will use a default mechanism.

    • The name of the network service requested by the client (such as telnet, ftp, or login service) is assigned to service_name.

    • Finally, msg is the string to send to the server as protected data. If the -f option is specified, then msg is the name of a file from which to read the string.

    An example command line might look like this:


    % gss-client -port 8080 -d -mech kerberos_v5 erebos.eng nfs "ls"
    

    This command line specifies neither mechanism nor port, and does not use delegation:


    % gss-client erebos.eng nfs "ls"
    

  2. It calls parse_oid() to create a GSS-API OID (object identifier) from the name of a security mechanism (if such a name has been provided on the command line):


    if (mechanism)
             parse_oid(mechanism, &g_mechOid);

    where mechanism is the string to translate and g_mechOid is a pointer to a gss_OID object for the mechanism. See Appendix C, Specifying an OID for more about specifying a non-default mechanism.

  3. It calls call_server(), which does the actual work of creating a context and sending data.


    if (call_server(hostname, port, g_mechOid, service_name,
                       deleg_flag, msg, use_file) < 0)
              exit(1);

  4. It releases the storage space for the OID if it has not been released yet.


    if (g_mechOID != GSS_C_NULL_OID)
         (void) gss_release_oid(&min_stat, &g_mechoid);

    Note that gss_release_oid(), while supported by the Sun implementation of the GSS-API, is not supported by all GSS-API implementations and is considered nonstandard. Since applications should if possible use the default mechanism provided by the GSS-API instead of allocating one (with gss_str_to_oid()), the gss_release_oid() command generally should not be used.

Specifying a Non-Default Mechanism

As a general rule, any application using the GSS-API should not attempt to specify a particular mechanism, but instead use the default mechanism provided by the GSS-API implementation. The default mechanism is specified by setting the gss_OID representing the mechanism to the value GSS_C_NULL_OID.

Because setting a non-default mechanism is not recommended, this part of the program does not cover it here. Interested readers can see how the client application parses the user-input mechanism name by looking at the code in parse_oid() and by looking at Appendix C, Specifying an OID, which explains how to using non-default OIDs.

Calling the Server

After the mechanism has been put in the form of a gss_OID, you can do the actual work, so main() now calls the function call_server() with much the same arguments as on the command line.


call_server(hostname, port, g_mechOid, service_name,
     deleg_flag, msg, use_file);

(use_file is a flag indicating whether the message to be sent is contained in a file or not.)

Connecting to the Server

After declaring its variables, call_server() first makes a connection with the server:


if ((s = connect_to_server(host, port)) < 0)
     return -1;

where s is a file descriptor (an int, initially returned by a call to socket()).

connect_to_server() is a simple function that uses sockets to create a connection. Because it doesn't use the GSS-API, it's skipped here. You can see it at connect_to_server().

Establishing a Context

After the connection is established, call_server() uses the function client_establish_context() to, yes, establish the security context:


int client_establish_context(s, service_name, deleg_flag, oid, 
     &context, &ret_flags)

where

To initiate the context, the application uses the function gss_init_sec_context(). As this function, like most GSS-API functions, requires names to be in internal GSS-API format, the application must first translate the service name from a string to internal format. For that, it can use gss_import_name():


maj_stat = gss_import_name(&min_stat, &send_tok, 
     (gss_OID) GSS_C_NT_HOSTBASED_SERVICE, &target_name);

This function takes the name of the service (stored in an opaque GSS_API buffer, send_tok) and converts it to the GSS_API internal name target_name. (send_tok is used to save space, instead of declaring a new gss_buffer_desc.) The third argument is a gss_OID type that indicates the name format that send_tok has. In this case, it is GSS_C_NT_HOSTBASED_SERVICE, which means a service of the format service@host. (See Name Types for other possible values for this argument.)

Once the service has been rendered in GSS-API internal format, we can proceed with establishing the context. In order to maximize portability, context-establishment should always be performed as a loop (see Context Initiation (Client)).

First, the application initializes the context to be null:


*gss_context = GSS_C_NO_CONTEXT;

It does the same for the token that we'll receive from the server:


token_ptr = GSS_C_NO_BUFFER;

The application now enters the loop. The loop proceeds by checking two things: the status returned by gss_init_sec_context() and the size of the token to be sent to the server (also generated by gss_init_sec_context()). If the token's size is zero, then the server is not expecting another token from the client. The pseudocode for the loop that follows looks like this:

do
     gss_init_sec_context()
     if no context was created
         uh-oh.  Exit with error;
    if the status is neither "complete" nor "in process"
         uh-oh.  Release the service namespace and exit with error;
    if there's a token to send to the server (= if its size is nonzero)
          send it;
         if sending it fails,
               oops!  release the token and the service 
                    namespaces and exit with error;
         release the namespace for the token we've just sent;
     if we're not done setting up the context
          receive a token from the server;
while the context is not complete

First, the call to gss_init_sec_context():


do {
     maj_stat = gss_init_sec_context(&min_stat,
                           GSS_C_NO_CREDENTIAL,
                           gss_context,
                           target_name
                           oid
                           GSS_C_MUTUAL_FLAG | 
                              GSS_C_REPLAY_FLAG |
                              deleg_flag,
                           0,
                           NULL,
                           &send_tok,
                           ret_flags,
                           NULL);

where the arguments are as follows:

You might have noticed that the client does not need to acquire credentials before initiating a context. On the client side, credential management is handled transparently by the GSS-API. That is, the GSS-API “knows” how to get credentials created by this mechanism for this principal (usually at login time). That is why the application passes gss_init_sec_context() a default credential. On the server side, however, a server application must explicitly acquire credentials for a service before accepting a context. See Acquiring Credentials.

After checking that it has a context (but not necessarily a complete one) and that gss_init_sec_context() is returning valid status, the application sees if gss_init_sec_context() has given it a token to send to the server. If it hasn't, it's because the server has indicated that it doesn't need (another) one. If it has, then send it to the server. If sending it fails, release the namespaces for it and the service, and exit. Remember, you can check for the presence of a token by looking at its length:


if (send_tok_length != 0) {
               if (send_token(s, &send_tok) < 0) {
                    (void) gss_release_buffer(&min_stat, &send_tok);
                    (void) gss_release_name(&min_stat, &target_name);
                    return -1;
               }
          }

send_token() is not a GSS-API function; it is a basic write-to-file function written by the user. (You can see it at send_token().) Note that the GSS-API does not send or receive tokens itself. It is the responsibility of the calling applications to send and receive any tokens created by the GSS-API.

If the server doesn't have any (more) tokens to send, then gss_init_sec_context() returns GSS_S_COMPLETE. So if gss_init_sec_context()hasn't returned this value, the application knows there's another token out there to fetch. If the fetch fails it releases the service namespace and quit:


if (maj_stat == GSS_S_CONTINUE_NEEDED) {
               if (recv_token(s, &recv_tok) < 0) {
                    (void) gss_release_name(&min_stat, &target_name);
                    return -1;

Finally, the program resets its token pointers, and continues the loop until the context is completely established. Thus its do loop ends as follows:


} while (maj_stat == GSS_S_CONTINUE_NEEDED);

Sending the Data

Having established the security context, gss-client needs to wrap the data, send it, and then verify the “signature” that the server returns. Because gss-client is an example program, it does various other things as well, such as display information about the context, but we'll skip all of that in order to get the data sent out and verified. So first the program puts the message to be sent (such as “ls”) into a buffer:


     if (use_file) {
         read_file(msg, &in_buf);
     } else {
         /* Wrap the message */
         in_buf.value = msg;
         in_buf.length = strlen(msg) + 1;
     }

Before wrapping, the program checks to see if it can encrypt the data:


     if (ret_flag & GSS_C_CONF_FLAG) {
          state = 1;
     else
          state = 0;
     }

And then it wraps it up:


     maj_stat = gss_wrap(&min_stat, context, conf_req_flag, 
                         GSS_C_QOP_DEFAULT, &in_buf, &state, &out_buf);
     if (maj_stat != GSS_S_COMPLETE) {
          display_status("wrapping message", maj_stat, min_stat);
          (void) close(s);
          (void) gss_delete_sec_context(&min_stat, &context, 
                         GSS_C_NO_BUFFER);
          return -1;
     } else if (! state) {
          fprintf(stderr, "Warning!  Message not encrypted.\n");
     }

Thus the message stored in in_buf is to be sent to the server referenced by context, with confidentiality service and the default Quality of Protection (QOP) requested. (Quality of Protection indicates which algorithm to apply in transforming the data; it's a good idea for portability's sake to use the default whenever possible.) gss_wrap() wraps the message, puts the result into out_buf, and sets a flag (state) that indicates whether confidentiality was in fact applied in the wrapping.

The client sends the wrapped message to the server with its own send_token() function, which you've already seen in Establishing a Context:


send_token(s, &outbuf)

Verifying the Message

The program can now verify the validity of the message it sent. It knows that the server returns the MIC for the message it sent, so it retrieves it with its recv_token() function and then uses gss_verify_mic() to verify its “signature” (the MIC).


     maj_stat = gss_verify_mic(&min_stat, context, &in_buf,
                               &out_buf, &qop_state);
     if (maj_stat != GSS_S_COMPLETE) {
          display_status("verifying signature", maj_stat, min_stat);
          (void) close(s);
          (void) gss_delete_sec_context(&min_stat, &context, 
                                        GSS_C_NO_BUFFER);
          return -1;
     }

gss_verify_mic() compares the MIC received with the server's token (in out_buf) with one it produces from the original, unwrapped message, held in in_buf. If the two MICs match, the message is verified. The client releases the buffer for the received token, out_buf.

To finish, call_server() deletes the context and returns to main().

Server-Side GSS-API: gss-server

Naturally, the client needs a server to perform a security handshake. Where the client initiates a security context and sends data, the server must accept the context, verifying the identity of the client. In doing so, it might need to authenticate itself to the client, if requested to do so, and it may have to provide a “signature” for the data to the client. Plus, of course, it has to process the data!

gss-server takes this form on the command line (the line has been broken up to make it fit):



gss-server [-port port] [-verbose] [-inetd] [-once] [-logfile file] \
                 [-mech mechanism] service_name

gss-server does the following:

  1. Parses the command line.

  2. Translates the mechanism name given on the command-line, if any, to internal format.

  3. Acquires credentials for the caller.

  4. Checks to see if the user has specified using the inetd daemon for connecting or not.

  5. Establishes a connection.

  6. Gets the data.

  7. Signs the data and returns it.

  8. Releases namespaces and exits.

Following is a step-by-step description of how gss-server works. Because it is a sample program designed to show off functionality, the parts of the program that do not closely relate to the steps above are skipped here.

Overview: main() (Server)

gss-client begins with the main() function. main() performs the following tasks:

  1. It parses command-line arguments, assigning them to variables:

    • port, if specified, is the port number to listen on. If no port is specified, the program uses port 4444 as the default.

    • If -verbose is specified, the program runs in a quasi-debug mode.

    • The -inetd option indicates that the program should use the inetd daemon to listen to a port; inetd uses stdin and stdout to hand the connection to the client.

    • If -once is specified, then the program makes only a single-instance connection.

    • mechanism is the (optional) name of the security mechanism to use, such as Kerberos v5, to use. If no mechanism is specified, the GSS-API uses a default mechanism.

    • The name of the network service requested by the client (such as telnet, ftp, or login service) is specified by service_name.

    An example command line might look like this:


    % gss-server -port 8080 -once -mech kerberos_v5 erebos.eng nfs "hello"
    

  2. It converts the mechanism, if specified, to a GSS-API object identifier (OID). This is because GSS-API functions handle names in internal format.

  3. It acquires the credentials for the service (such as ftp), for the mechanism being used (for example, Kerberos v5).

  4. It calls the sign_server() function, which does most of the work (establishes the connection, retrieves and signs the message, and so on).

    If the user has specified using inetd, then the program closes the standard output and standard error and calls sign_server() on the standard input, which inetd uses to pass connections. Otherwise, it creates a socket, accepts the connection for that socket with the TCP function accept(), and calls sign_server() on the file descriptor returned by accept().

    If inetd is not used, the program creates connections and contexts until it's terminated. However, if the user has specified the -once option, the loop terminates after the first connection.

  5. It releases the credentials it has acquired.

  6. It releases the mechanism OID namespace.

  7. It closes the connection, if it's still open.

Creating an OID for the Mechanism

As with the gss-client program example, the sample server program allows the user to specify a mechanism. However, it is strongly recommended that all applications use the default mechanism provided by the GSS-API implementation. The default mechanism is obtained by setting the gss_OID that represents the mechanism to GSS_C_NULL_OID. Interested readers can refer to the code itself in createMechOid() and read about using non-default mechanisms in Appendix C, Specifying an OID.

Acquiring Credentials

As with the client application, neither the server application nor the GSS-API create credentials; they are created by the underlying mechanism(s). Unlike the client program, the server needs to explicitly acquire the credentials it needs. (Some client applications might want to acquire credentials explicitly, in which case they do so in the same manner as shown here. But generally the client has acquired credentials before that, at login time, and GSS-API acquires those automatically.)

The gss-server program has its own function, server_acquire_creds(), to get the credentials for the service being provided. It takes as its input the name of the service, and the security mechanism being used, then returns the credentials for the service.

server_acquire_creds() uses the GSS-API function gss_acquire_cred() to get the credentials for the service that the server provides. Before it can do this, however, it must do two things.

If a single credential can be shared by multiple mechanisms, gss_acquire_cred() returns credentials for all those mechanisms. Therefore, it takes as input not a single mechanism, but a set of mechanisms. (See Credentials.) However, in most cases, including this one, a single credential might not work for multiple mechanisms. Besides, in the server application, either a single mechanism is specified on the command line or the default mechanism is used. Therefore, the first thing to do is make sure that the set of mechanisms passed to gss_acquire_cred() contains a single mechanism, default or otherwise:


if (mechOid != GSS_C_NULL_OID) {
     desiredMechs = &mechOidSet;
     mechOidSet.count = 1;
     mechOidSet.elements = mechOid;
} else
     desiredMechs = GSS_C_NULL_OID_SET;

GSS_C_NULL_OID_SET indicates that the default mechanism should be used.

Because gss_acquire_cred() takes the service name in the form of a gss_name_t structure, the second thing to do is import the name of the service into that format. To do this, use gss_import_name(). Because this function, like all GSS-API functions, requires arguments to be GSS-API types, the service name has to be copied to a GSS-API buffer first:


     name_buf.value = service_name;
     name_buf.length = strlen(name_buf.value) + 1;
     maj_stat = gss_import_name(&min_stat, &name_buf,
                (gss_OID) GSS_C_NT_HOSTBASED_SERVICE, &server_name);
     if (maj_stat != GSS_S_COMPLETE) {
          display_status("importing name", maj_stat, min_stat);
          if (mechOid != GSS_C_NO_OID)
                gss_release_oid(&min_stat, &mechOid);
          return -1;
     }

Note again the use of the nonstandard function gss_release_oid(). See Overview: main() (Client).

The input is the service name, as a string in name_buf, and the output is the pointer to a gss_name_t structure, server_name. The third argument, GSS_C_NT_HOSTBASED_SERVICE, is the name type for the string in name_buf; in this case it indicates that the string should be interpreted as a service of the format service@host.

Now the server program can call gss_acquire_cred():


maj_stat = gss_acquire_cred(&min_stat, server_name, 0,
                                 desiredMechs, GSS_C_ACCEPT,
                                 server_creds, NULL, NULL);

Where:

Accepting a Context, Getting and Signing Data

Having acquired credentials for the service, the server program checks to see if the user has specified using inetd (see Overview: main() (Server)) and then calls sign_server(), which does the main work of the program. The first thing that sign_server() does is establish the context by calling server_establish_context().


Note –

inetd is not covered here. Basically, if inetd has been specified, the program calls sign_server() on the standard input. If not, it creates a socket, accepts a connection, and then calls sign_server() on that connection.


sign_server() does the following:

  1. Accepts the context.

  2. Unwraps the data.

  3. Signs the data.

  4. Returns the data.

Accepting a Context

Because establishing a context can involve a series of token exchanges between the client and the server, both context acceptance and context initialization should be performed in loops, to maintain program portability. Indeed, the loop for accepting a context is very similar to that for establishing one, although rather in reverse. (Compare with Establishing a Context.)

  1. The first thing the server does is look for a token that the client should have sent as part of the context initialization process. Remember, the GSS-API does not send or receive tokens itself, so programs must have their own routines for performing these tasks. The one the server uses for receiving the token is called recv_token() (it can be found at recv_token()):


         do {
              if (recv_token(s, &recv_tok) < 0)
                   return -1;

  2. Next, the program calls the GSS-API function gss_accept_sec_context():


         maj_stat = gss_accept_sec_context(&min_stat,
                                          context,
                                          server_creds,
                                          &recv_tok,
                                          GSS_C_NO_CHANNEL_BINDINGS,
                                          &client,
                                          &doid,
                                          &send_tok,
                                          ret_flags,
                                          NULL,     /* ignore time_rec */
                                          NULL);    /* ignore del_cred_handle */

    where

    • min_stat is the error status returned by the underlying mechanism.

    • context is the context being established.

    • server_creds is the credential for the service being provided (see Acquiring Credentials).

    • recv_tok is the token received from the client by recv_token().

    • GSS_C_NO_CHANNEL_BINDINGS is a flag indicating not to use channel bindings (see Channel Bindings).

    • client is the ASCII name of the client.

    • oid is the mechanism (in OID format).

    • send_tok is the token to send to the client.

    • ret_flags are various flags indicating whether the context supports a given option, such as message-sequence-detection.

    • NULL and NULL indicate that the program is not interested in the length of time the context will be valid, nor in whether the server can act as a client's proxy.

    The acceptance loop continues (barring an error) as long as gss_accept_sec_context() sets maj_stat to GSS_S_CONTINUE_NEEDED. If maj_stat is not equal to either that value nor to GSS_S_COMPLETE, there's a problem and the loop exits.

  3. gss_accept_sec_context() returns a positive value for the length of send_tok if there is a token to send back to the client. The next step is to see if there's a token to send, and, if so, to send it:


         if (send_tok.length != 0) {
              . . .
              if (send_token(s, &send_tok) < 0) {
                   fprintf(log, "failure sending token\n");
                   return -1;
              }
    
              (void) gss_release_buffer(&min_stat, &send_tok);
              }

Unwrapping the Message

After accepting the context, the server receives the message sent by the client. Because the GSS-API doesn't provide a function to do this, the program uses its own function, recv_token():


if (recv_token(s, &xmit_buf) < 0)
     return(-1);

Since the message might be encrypted, the program uses the GSS-API function gss_unwrap() to unwrap it:


maj_stat = gss_unwrap(&min_stat, context, &xmit_buf, &msg_buf,
                           &conf_state, (gss_qop_t *) NULL);
     if (maj_stat != GSS_S_COMPLETE) {
        display_status("unwrapping message", maj_stat, min_stat);
        return(-1);
     } else if (! conf_state) {
        fprintf(stderr, "Warning!  Message not encrypted.\n");
     }

     (void) gss_release_buffer(&min_stat, &xmit_buf);

gss_unwrap() takes the message that recv_token() has placed in xmit_buf, translates it, and puts the result in msg_buf. Two arguments to gss_unwrap() are noteworthy: conf_state is a flag to indicate whether confidentiality was applied for this message (that is, if the data is encrypted or not), and the final NULL indicates that the program isn't interested in the QOP used to protect the message.

Signing the Message, Sending It Back

All that is left, then, is for the server to “sign” the message — that is, to return the message's MIC (Message Integrity Code, a unique tag associated with message) to the client to prove that the message was sent and unwrapped successfully. To do that, the program uses the function gss_get_mic():


maj_stat = gss_get_mic(&min_stat, context, GSS_C_QOP_DEFAULT,
                            &msg_buf, &xmit_buf);

which looks at the message in msg_buf and produces the MIC from it, storing it in xmit_buf. The server then sends the MIC back to the client with send_token(), and the client verifies it with gss_verify_mic(). See Verifying the Message.

Finally, sign_server() performs some cleanup; it releases the GSS-API buffers msg_buf and xmit_buf with gss_release_buffer() and then destroys the context with gss_delete_sec_context().

Importing and Exporting a Context

As noted in Context Export and Import, the GSS-API allows you to export and import contexts. The usual reason for doing this is to share a context between different processes in a multiprocess program.

sign_server() contains a proof-of-concept function, test_import_export_context(), which illustrates how exporting and importing contexts works. This function doesn't pass a context between processes. It only displays the amount of time it takes to export and then import a context. Although rather an artificial function, it does indicate how to use the GSS-API importing and exporting functions, as well as give an idea of how to use timestamps with regard to manipulating contexts. test_import_export_context() can be found in test_import_export_context().

Cleanup

Back in the main() function, the application deletes the service credential with gss_delete_cred() and, if an OID for the mechanism has been specified, deletes that with gss_delete_oid() and exits.

Accessory Functions

The client and server programs use certain support functions, for example to display the value of returned flags. As they are either not specific to the GSS-API or else are not terribly important, they are not covered here. They may be found in Ancillary Functions. Two of them, however, send_token() and recv_token(), are significant enough that they are listed separately in send_token() and recv_token().