Writing PAM Modules, Part Two

PAM stands for Pluggable Authentication Modules, and is a way of providing application independence for authentication. A PAM-enabled application calls a stack of PAM modules to run authentication, open and close sessions, and check account validity.

This is Part Two of a three-part article on writing PAM modules. Part One discussed the background information needed to write modules, and Part Three discusses the critical functions the module must supply. This part discusses the support code a module author will need to use.

Packages and Files

These articles describe a C++ application because most large-scale application development is in that language, and its differences from the C language, while distinct, are not extreme. You can develop a Linux-PAM capable module in any language, provided you can call the necessary C functions.

To start with, you'll need the development files. In a Debian system, apt-get install libpam0g-dev. This installs the relevant source files in the right places for g++ to find them.

Setting and Getting PAM Items

Most of the information a module needs can be found or passed through PAM items. These items are related to a specific PAM session, which is identified with a PAM handle. The PAM handle is passed to each of the module's major functions as a parameter.

Use the pam_set_item() and pam_get_item() functions to store or retrieve PAM items.

The PAM name of the application, not necessarily the name the user sees.

PAM_CONV

The conversation structure. (See the next section.)

PAM_FAIL_DELAY

The pointer to the PAM_FAIL_DELAY function. Some applications replace the default PAM version.

About the user:

PAM_USER

The username to be authenticated against. It is usually safer to use pam_get_user() rather than asking for this directly.

PAM_USER_PROMPT

The prompt the module should use if asking for a username.

PAM_RUSER

The user requesting authentication, usually the username of the user calling the application.

About the machine:

PAM_RHOST

The hostname of the machine requesting authentication.

PAM_TTY

The terminal name (console-based apps) or $DISPLAY (GUI based apps). You can retrieve the terminal name with ttyname().

About authentication:

PAM_AUTHTOK

The authentication token. This should be ignored by anything other than authentication and password changing modules, and only used in the functions pam_sm_authenticate() and pam_sm_chauthtok().

PAM_OLDAUTHTOK

The previous authentication token. This should only be used by pam_sm_chauthtok() in the password module type.

Other than PAM_SUCCESS, the PAM item functions can return PAM_SYSTEM_ERR, PAM_PERM_DENIED, PAM_BUF_ERR or PAM_BAD_ITEM.

The Conversation Function

An application may display a message in any number of formats. A GUI displays messages very differently from a command line interface, and both are different from a secure HTTP session or an FTP session.

PAM insulates the module developer from these issues, by requiring the application to provide a conversation function. This function takes messages from the module, presents them to the user, and provides the module with responses.

A call to pam_get_item(pamhandle, PAM_CONV, &item) produces a pointer to a conversation structure. The conversation structure contains the conversation function and a pointer that the application can use to transfer data.

The module uses this function to pass messages to the user. The module may pass messages one at a time or as a cluster of several messages at once. The application will determine whether the messages are presented to the user as a group, or individually.

The module must create an array of pointers to pam_message structures to contain the message strings it needs the application to display. This array is the msg parameter, and each structure contains both the message and the message format. The num_msg parameter is the length of the message array.

struct pam_message {
int msg_style;
const char *msg;
};

Available message formats are:

PAM_ERROR_MSG

Display an error message.

PAM_PROMPT_ECHO_OFF

Receive a string but do not echo input.

PAM_PROMPT_ECHO_ON

Receive a string and echo input.

PAM_TEXT_INFO

Display a message.

The application will provide an array of pam_response structures on return. The module must provide a pointer, which the application can fill with a pointer to the array. Yes, this is a complicated piece of indirection. The module provides its pointer as the resp parameter.

The module should fill this pointer with a NULL or other testable value before calling the conversation function, so it can verify that the application has changed it.

struct pam_response {
char *resp;
int resp_retcode;
};

The resp_retcode in the response structure is currently meaningless, and is usually filled with a 0.

Note that the appdata_ptr is included as the last parameter of the function -- this is important. The application uses the appdata_ptr to transfer data it may need within the function, so be sure to send it that pointer.

Getting the Username

Getting a username can be involved -- first checking whether it's a PAM item, then calling the conversation function, and identifying the appropriate prompt to ask for the username. Fortunately PAM is willing to do the work for you, in the pam_get_user() function.

Use pam_get_user rather than doing the work yourself. It's easier, cleaner to read, and if the process changes, you don't have to do a thing!

Do not free the user data memory. A second call to pam_get_user() or a call to pam_end() will do that for you.

The meaning of the return value PAM_SUCCESS is obvious. Other return values are PAM_CONV_ERR which means no username could be obtained, and PAM_CONV_AGAIN.

If you receive PAM_CONV_AGAIN, return control to the application with PAM_INCOMPLETE, and, when next invoked with that PAM handle, return to the failed pam_get_user() and pick up where you left off.

Setting and Getting Data

The module author cannot predict how the module will be loaded, so you should avoid static variables. The functions to set and get data provide a way to associate data with a PAM handle. The data items set using this function are available to other modules, but not to the application.

The cleanup parameter is a function you need to provide to do a proper cleanup of the data item. If no special cleanup is required by this item, it may be set to NULL.

The error_status parameter may be logically ORed with PAM_DATA_REPLACE to signal a second call to set the same item, or PAM_DATA_SILENT to cause the cleanup function to avoid logging or sending messages to the user.

If an entry is present but has the value NULL, pam_get_data() will return PAM_NO_MODULE_DATA.

Managing the Environment

Linux-PAM comes with a separate environment, associated with the current PAM handle. The environment starts out empty.

extern const char *pam_getenv(pam_handle_t *pamh, const char *name);

Returns the value of the named Linux-PAM environment variable, or NULL if there is a failure.

extern const char * const *pam_getenvlist(pam_handle_t *pamh);

Returns a pointer to a read-only list of the current Linux-PAM environment.

extern int pam_putenv(pam_handle_t *pamh, const char *name_value);

Attempts to set, reset, or delete the named environment variable. The name_value argument is a NUL terminated (C style) string. Valid formats are:

name=value

Sets 'name' to 'value'

name=

Sets 'name' to the empty string

name

Deletes 'name'

Memory Security

Any memory that your module does not personally free must be allocated with the std::malloc() family of functions from the Standard C library. new and delete can be used, but only for memory you will be releasing yourself.

Final Words

This is Part Two of a three-part article on writing PAM modules. Part One discusses the underlying theory of PAM, and what behaviors are required of a module. Part Three discusses the functions a module is required to provide, security issues, and response codes.