GSS-API Programming Guide

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().