GSS-API Programming Guide

Some Introductory Concepts

Before looking at the actual process of using the GSS-API, let's examine four important concepts. They are: principals, GSS-API data types, status codes, and tokens.

Principals

In network-security terminology, a principal is a user, a program, or a machine. Principals can be either clients or servers. Examples of principals are: a user (joe@machine) logging into another machine; a network service (nfs@machine); a machine that runs an application (swim2birds@eng.company.com).

In the GSS-API, principals are referred to by a special data type— see Names.

GSS-API Data Types

The following sections explain the more important and visible GSS-API data types; see GSS-API Data Types and Values for more information.


Caution – Caution –

It is the responsibility of the calling application to free all data space that has been allocated.


Integers

Because the size of an int can vary from platform to platform, the GSS-API provides the following integer data type:


OM_uint32

which is a 32–bit unsigned integer.


Strings and Similar Data

Since the GSS-API handles all data in internal formats, strings must be converted to a GSS-API format before being passed to GSS-API functions. The GSS-API handles strings with the gss_buffer_desc structure; gss_buffer_t is a pointer to such a structure.


typedef struct gss_buffer_desc_struct {
     size_t     length;
     void       *value;
}  gss_buffer_desc *gss_buffer_t;

Therefore, strings must be put into a gss_buffer_desc structure before being passed to functions that use them. Consider a generic GSS-API function that takes a message and processes it in some way (for example, applies protection to it before it is transmitted), as follows:


Example 1–1 Using Strings

char *message_string;
gss_buffer_desc input_msg_buffer;

input_msg_buffer.value = message_string;
input_msg_buffer.length = strlen(input_msg_buffer.value) + 1;

gss_generic_function(arg1, &input_msg_buffer, arg2...);

gss_release_buffer(input_msg_buffer);

Note that input_msg_buffer must be deallocated with gss_release_buffer() when you are finished with it.

The gss_buffer_desc object is not just for character strings; for example, tokens are manipulated as gss_buffer_desc objects. (See GSS-API Tokens.)

Names

A name refers to a principal — that is, a person, a machine, or an application, such as joe@company or nfs@machinename. In the GSS-API, names are stored as a gss_name_t object, which is opaque to the application. Names are converted from gss_buffer_t objects to the gss_name_t form by the gss_import_name() function. Every imported name has an associated name type, which indicates what kind of format the name is in. (See under OIDs for more about name types, and see Name Types for a list of valid name types).

gss_import_name() looks like this:

OM_uint32 gss_import_name (
       OM_uint32          *minor_status,
       const gss_buffer_t input_name_buffer,
       const gss_OID      input_name_type,
       gss_name_t         *output_name)

minor_status

Status code returned by the underlying mechanism. (See Status Codes.)

input_name_buffer

The gss_buffer_desc structure containing the name to be imported. The application must allocate this explicitly (see Strings and Similar Data as well as Example 1–2.) This argument must be deallocated with gss_release_buffer() when the application is finished with it.

input_name_type

A gss_OID that specifies the format that the input_name_buffer is in. (See Name Types; also, Name Types contains a table of valid name types.)

output_name

The gss_name_t structure to receive the name.

Slightly modifying the generic example shown in Example 1–1, here is how you can use gss_import_name(). First, the regular string is inserted into a gss_buffer_desc structure, and then gss_import_name() places it into a gss_name_t structure.


Example 1–2 Using gss_import_name()

char *name_string;
gss_buffer_desc input_name_buffer;
gss_name_t      output_name_buffer;

input_name_buffer.value = name_string;
input_name_buffer.length = strlen(input_name_buffer.value) + 1;

gss_import_name(&minor_status, input_name_buffer, 
                    GSS_C_NT_HOSTBASED_SERVICE, &output_name);

gss_release_buffer(input_name_buffer);

An imported name can be put back into a gss_buffer_t object for display in human-readable form with gss_display_name(); however, gss_display_name() does not guarantee that the resulting string will be the same as the original, because of the way the underlying mechanisms store names. The GSS-API includes several other functions for manipulating names; see GSS-API Functions.

A gss_name_t structure can contain several versions of a single name — one version produced for each mechanism supported by the GSS-API. That is, a gss_name_t structure for “joe@company” might contain one version of that name as rendered by Kerberos v5, another version as given by a different mechanism, and so on. The GSS-API provides a function, gss_canonicalize_name(), that takes as its input an internal name (that is, a gss_name_t structure) and a mechanism and yields a second internal name (also a gss_name_t) that contains only a single version of the name, specific to that mechanism.

Such a mechanism-specific name is called a Mechanism Name (MN). “Mechanism Name” is a slightly confusing label, since it refers not to the name of a mechanism, but to the name of a principal as produced by a given mechanism. This process is illustrated by Figure 1–3.

Figure 1–3 Internal Names and Mechanism Names

Diagram shows how mechanism names are derived.

Comparing Names

Why is such a function useful? Consider the case where a server has received a name from a client and wants to look up that name in an Access Control List. (An Access Control List, or ACL, is a list of principals with particular access permissions.) One way to do this would be as follows:

  1. Import the client name into GSS-API internal format with gss_import_name(), if it hasn't already been imported.

    In some cases, the server will receive a name in internal format, so this step will not be necessary — in particular, if the server is looking up the client's own name. (During context initiation, the client's own name is passed in internal format.)

  2. Import each name in the ACL with gss_import_name().

  3. Compare each imported ACL name with the imported client's name, using gss_compare_name().

This process is shown in Figure 1–4; in this case, we assume that Step 1 is needed.

Figure 1–4 Comparing Names (Slow)

Diagram shows how internal client names are compared using the gss_compare_name function.

That procedure is fine if you only need to compare the client's name with a few names. However, it is a very slow way to check a large list! Running gss_import_name() and gss_compare_name() for every name in the ACL might require a lot of CPU cycles. This is a better way:

  1. Import the client's name with gss_import_name() (if it hasn't already been imported).

    As with the previous method of comparing names, in some cases the server receives a name in internal format and so this step is not necessary.

  2. Use gss_canonicalize_name() to produce an MN of the client's name.

  3. Use gss_export_name() to produce an “exported name,” a contiguous-string version of the client's name.

  4. Compare the exported client's name with each name in the ACL by using memcmp(), which is a fast, low-overhead function.

This process is shown in Figure 1–5; again, assume the server needs to import the name received from the client.

Figure 1–5 Comparing Names (Fast)

Diagram shows how internal client names are compared using the memcmp function.

Because gss_export_name() expects a Mechanism Name (MN), you must run gss_canonicalize_name() on the client's name first.

See the gss_canonicalize_name(3GSS), gss_export_name(3GSS), and gss_import_name(3GSS) man pages for more information.

OIDs

Object Identifiers (OIDs) are used to store the following kinds of data: security mechanisms, QOPs (Quality of Protection values), and name types. OIDs are stored in the GSS-API gss_OID_desc structure; the GSS-API provides a pointer to the structure, gss_OID, as shown here.


Example 1–3 OIDs

typedef struct gss_OID_desc_struct {
        OM_uint32   length;
        void        *elements;
     } gss_OID_desc, *gss_OID;

Further, one or more OIDs might be contained in a gss_OID_set_desc structure.


Example 1–4 OID Sets

typedef struct gss_OID_set_desc_struct {
        size_t    count;
        gss_OID   elements;
     } gss_OID_set_desc, *gss_OID_set;


Caution – Caution –

Applications should not attempt to deallocate OIDs with free().


Mechanisms and QOPs

Although the GSS-API allows applications to choose which underlying security mechanism to use, applications should use the default mechanism selected by the GSS-API if possible. Likewise, the GSS-API allows an application to specify the QOP it wants for protecting data — a QOP (Quality of Protection) is the algorithm used for encrypting data or generating a cryptographic identification tag — the default QOP should be used if possible. The default mechanism is represented by passing the value GSS_C_NULL_OID to functions that expect a mechanism or QOP as an argument.


Caution – Caution –

Specifying a security mechanism or QOP explicitly more or less defeats the purpose of using the GSS-API, because it limits the portability of an application. Other implementations of the GSS-API may not support that QOP or mechanism, or they may support it in limited or unexpected ways. Nonetheless, Appendix C, Specifying an OID briefly discusses how to find out which mechanisms and QOPs are available, and how to choose one.


Name Types

Besides QOPs and security mechanisms, OIDs are also used to indicate name types, which indicate the format for an associated name. For example, the function gss_import_name(), which converts the name of a principal from a string to a gss_name_t type, takes as one argument the format of the string to be converted. If the name type is (for example) GSS_C_NT_HOSTBASED_SERVICE, then the function knows that the name being input is of the form “service@host”, as in “nfs@swim2birds”; if it's equal to, for instance, GSS_C_NT_EXPORT_NAME, then the function knows that it's a GSS-API exported name. Applications can find out which name types are available for a given mechanism with the gss_inquire_names_for_mech() function. A list of name types used by the GSS-API is given in Name Types.

Status Codes

All GSS-API functions return two types of codes that provide information on the function's success or failure. Both types of status codes are returned as OM_uint32 values. The two types of return codes are as follows:

GSS-API Tokens

The basic unit of currency, so to speak, in the GSS-API is the token. Applications using the GSS-API communicate with each other by using tokens, both for exchanging data and for making security arrangements. Tokens are declared as gss_buffer_t data types and are opaque to applications.

The two types of tokens are: context-level tokens and per-message tokens. Context-level tokens are used primarily when a context is established (initiated and accepted), although they can also be passed afterward to manage a context.

Per-message tokens are used after a context has been established, and are used to provide protection services on data. For example, if an application wants to send a message to another application, it might use the GSS-API to generate a cryptographic identifier to go along that message; that identifier would be stored in a token.

Per-message tokens can be considered with regard to “messages” as follows. A message is a piece of data that an application sends to a peer; for example, the ls command sent to an ftp server. A per-message token is an object generated by the GSS-API for that message, such as a cryptographic tag, or the encrypted form of the message. (Semantically speaking, this last example is mildly inaccurate: an encrypted message is still a message, not a token, since a token is only the GSS-API-generated information. However, informally, message and per-message token are often used interchangeably.)

It is the responsibility of the application (not the GSS-API) to:

  1. Send and receive tokens. The developer usually needs to write generalized read and write functions for performing these actions. send_token() and recv_token() are examples of such functions.

  2. Distinguish between types of tokens and manipulate them accordingly.

    Because tokens are opaque to applications, there is no difference (to the application) between one token and another. Therefore, an application must be able to distinguish one token from another without explicitly knowing their contents, before passing them on to the appropriate GSS-API functions. The ways an application can distinguish tokens include:

    • By state — that is, through the control-flow of a program. For example, if an application is waiting to accept a context, it can assume that any token it receives is a context-level token related to context-establishment, because it expects peers to wait until the context is fully established before sending message (data) tokens. After the context is established, the application can assume that any tokens it receives are message tokens. This is a fairly common way to handle tokens; the sample programs in this book use this method.

    • An application might distinguish types of tokens when sending and receiving them. For example, if the application has its own function for sending tokens to peers, it can include a flag indicating what kind of token is being sent:


      gss_buffer_t token;     /* declare the token */
      OM_uint32 token_flag       /* flag for describing the type of token */
      
      <get token from a GSS-API function>
      
      token_flag = MIC_TOKEN;     /* specify what kind of token it is */
      send_a_token(&token, token_flag);

      Then the receiving application would have a receiving function (say, “get_a_token()”) that would check the token_flag argument.

    • A third way might be through explicit tagging; for example, applications might use their own “meta-tokens”: user-defined structures that contain tokens received from GSS-API functions, along with user-defined fields that signal how the GSS-API-provided tokens are to be used.

Interprocess Tokens

The GSS-API permits a security context to be passed from one process to another in a multiprocess application. Typically, this application has accepted a client's context and wants to share it among its processes. See Context Export and Import for information on multiprocess applications.

The gss_export_context() function creates an interprocess token that contains information allowing the context to be reconstituted by a second process. It is the responsibility of the application to pass this interprocess token from one process to the other, just as it is the application's responsibility to pass tokens to other applications.

Since this interprocess token might contain keys or other sensitive information, and since it cannot be guaranteed that all GSS-API implementations will cryptographically protect interprocess tokens, it is up to the application to protect them before exchange. This may involve encrypting them with gss_wrap(), if encryption is available.


Note –

Interprocess tokens cannot be assumed to be transferable across different GSS-API implementations.