Developer's Guide to Oracle Solaris Security

Chapter 3 Writing PAM Applications and Services

Pluggable authentication modules (PAM) provide system entry applications with authentication and related security services. This chapter is intended for developers of system entry applications who wish to provide authentication, account management, session management, and password management through PAM modules. There is also information for designers of PAM service modules.

The following topics are discussed:

PAM was originally developed at Sun. The PAM specification has since been submitted to X/Open, which is now the Open Group. The PAM specification is available in X/Open Single Sign-On Service (XSSO) - Pluggable Authentication, Open Group, UK ISBN 1-85912-144-6 June 1997. The Oracle Solaris implementation of PAM is described in the pam(3PAM), libpam(3LIB), and pam_sm(3PAM) man pages.

Introduction to the PAM Framework

The PAM framework consists of four parts:

The framework provides a uniform way for authentication-related activities to take place. This approach enables application developers to use PAM services without having to know the semantics of the policy. Algorithms are centrally supplied. The algorithms can be modified independently of the individual applications. With PAM, administrators can tailor the authentication process to the needs of a particular system without having to change any applications. Adjustments are made through pam.conf, the PAM configuration file.

The following figure illustrates the PAM architecture. Applications communicate with the PAM library through the PAM application programming interface (API). PAM modules communicate with the PAM library through the PAM service provider interface (SPI). Thus, the PAM library enables applications and modules to communicate with each other.

Figure 3–1 PAM Architecture

Figure shows how the PAM library is accessed by applications
and PAM service modules.

PAM Service Modules

A PAM service module is a shared library that provides authentication and other security services to system entry applications such as login, rlogin, and telnet.

The four types of PAM services are:

A PAM module can implement one or more of these services. The use of simple modules with well-defined tasks increases configuration flexibility. PAM services should thus be implemented in separate modules. The services can then be used as needed as defined in the pam.conf(4) file.

For example, the Oracle Solaris OS provides the pam_authtok_check(5) module for system administrators to configure the site's password policy. The pam_authtok_check(5) module checks proposed passwords for various strength criteria.

For a complete list of Oracle Solaris PAM modules, see man pages section 5: Standards, Environments, and Macros. The PAM modules have the prefix pam_.

PAM Library

The PAM library, libpam(3LIB), is the central element in the PAM architecture:

PAM Authentication Process

    As an example of how consumers use the PAM library for user authentication, consider how login authenticates a user:

  1. The login application initiates a PAM session by calling pam_start(3PAM) and by specifying the login service.

  2. The application calls pam_authenticate(3PAM), which is part of the PAM API that is exported by the PAM library, libpam(3LIB).

  3. The library searches for login entries in the pam.conf file.

  4. For each module in pam.conf that is configured for the login service, the PAM library calls pam_sm_authenticate(3PAM). The pam_sm_authenticate() function is part of the PAM SPI. The pam.conf control flag and results of each call determine whether the user is allowed access to the system. This process is described in more detail in PAM Configuration (Reference) in System Administration Guide: Security Services.

In this way, the PAM library connects PAM applications with the PAM modules that have been configured by the system administrator.

Requirements for PAM Consumers

PAM consumers must be linked with the PAM library libpam. Before an application can use any service that is provided by the modules, the application must initialize its instance of the PAM library by calling pam_start(3PAM). The call to pam_start() initializes a handle that must be passed to all subsequent PAM calls. When an application is finished with the PAM services, pam_end() is called to clean up any data that was used by the PAM library.

Communication between the PAM application and the PAM modules takes place through items. For example, the following items are useful for initialization:

For a complete list of available items, see pam_set_item(3PAM). Items can be set by the application through pam_set_item(3PAM). Values that have been set by the modules can be retrieved by the application through pam_get_item(3PAM). However, PAM_AUTHTOK and PAM_OLDAUTHTOK cannot be retrieved by the application. The PAM_SERVICE item cannot be set.

PAM Configuration

The PAM configuration file, pam.conf(4), is used to configure PAM service modules for system services, such as login, rlogin, su, and cron. The system administrator manages this file. An incorrect order of entries in pam.conf can cause unforeseen side effects. For example, a badly configured pam.conf can lock out users so that single-user mode becomes necessary for repair. For information on PAM configuration, see PAM Configuration (Reference) in System Administration Guide: Security Services.

Writing Applications That Use PAM Services

This section provides a sample application that uses several PAM functions.

A Simple PAM Consumer Example

The following PAM consumer application is provided as an example. The example is a basic terminal-lock application that validates a user trying to access a terminal.

    The example goes through the following steps:

  1. Initialize the PAM session.

    PAM sessions are initiated by calling the pam_start(3PAM) function. A PAM consumer application must first establish a PAM session before calling any of the other PAM functions.

    The pam_start(3PAM) function takes the following arguments:

    • plock – Service name, that is, the name of the application. The service name is used by the PAM framework to determine which rules in the configuration file, /etc/pam.conf, are applicable. The service name is generally used for logging and error-reporting.

    • pw->pw_name – The username is the name of the user that the PAM framework acts on.

    • &conv – The conversation function, conv, which provides a generic means for PAM to communicate with a user or application. Conversation functions are necessary because the PAM modules have no way of knowing how communication is to be conducted. Communication can be by means of GUIs, the command line, a smart card reader, or other devices. For more information, see Writing Conversation Functions.

    • &pamh – The PAM handle, pamh, which is an opaque handle that is used by the PAM framework to store information about the current operation. This handle is returned by a successful call to pam_start().


    Note –

    An application that calls PAM interfaces must be sufficiently privileged to perform any needed operations such as authentication, password change, process credential manipulation, or audit state initialization. In this example, the application must be able to read /etc/shadow to verify the passwords for local users.


  2. Authenticate the user.

    The application calls pam_authenticate(3PAM) to authenticate the current user. Generally, the user is required to enter a password or other authentication token depending on the type of authentication service. The PAM framework invokes the modules listed for the authentication service, auth, in /etc/pam.conf. The service name plock is used to determine which pam.conf entries are to be used. If there are no entries for plock, then the entries in other are used by default. If NULL passwords are explicitly disallowed in the application configuration file, then the PAM_DISALLOW_NULL_AUTHTOK flag should be passed. Oracle Solaris applications check the PASSREQ=YES setting in /etc/default/login.

  3. Check account validity.

    The example uses the pam_acct_mgmt(3PAM) function to check the validity of the authenticated user's account. In this example, pam_acct_mgmt() checks for expiration of the password.

    The pam_acct_mgmt() function also uses the PAM_DISALLOW_NULL_AUTHTOK flag. If pam_acct_mgmt() returns PAM_NEW_AUTHTOK_REQD, then pam_chauthtok(3PAM) should be called to allow the authenticated user to change the password.

  4. Force the user to change passwords if the system discovers that the password has expired.

    The example uses a loop to call pam_chauthtok() until success is returned. The pam_chauthtok() function returns success if the user successfully changes his or her authentication information, which is usually the password. In this example, the loop continues until success is returned. More commonly, an application would set a maximum number of tries before terminating.

  5. Call pam_setcred(3PAM).

    The pam_setcred(3PAM) function is used to establish, modify, or delete user credentials. pam_setcred() is typically called when a user has been authenticated. The call is made after the account has been validated, but before a session has been opened. The pam_setcred() function is used with the PAM_ESTABLISH_CRED flag to establish a new user session. If the session is the renewal of an existing session, such as for lockscreen, pam_setcred() with the PAM_REFRESH_CRED flag should be called. If the session is changing the credentials, such as using su or assuming a role, then pam_setcred() with the PAM_REINITIALIZE_CRED flag should be called.

  6. Close the PAM session.

    The PAM session is closed by calling the pam_end(3PAM) function. pam_end() frees all PAM resources as well.

The following example shows the source code for the sample PAM consumer application.


Note –

The source code for this example is also available through the Sun download center. See http://www.sun.com/download/products.xml?id=41912db5.



Example 3–1 Sample PAM Consumer Application

/*
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <signal.h>
#include <pwd.h>
#include <errno.h>
#include <security/pam_appl.h>

extern int pam_tty_conv(int num_msg, struct pam_message **msg,
	     struct pam_response **response, void *appdata_ptr);

/* Disable keyboard interrupts (Ctrl-C, Ctrl-Z, Ctrl-\) */
static void
disable_kbd_signals(void)
{
	(void) signal(SIGINT, SIG_IGN);
	(void) signal(SIGTSTP, SIG_IGN);
	(void) signal(SIGQUIT, SIG_IGN);
}

/* Terminate current user session, i.e., logout */
static void
logout()
{
	pid_t pgroup = getpgrp();

	(void) signal(SIGTERM, SIG_IGN);
	(void) fprintf(stderr, "Sorry, your session can't be restored.\n");
	(void) fprintf(stderr, "Press return to terminate this session.\n");
	(void) getchar();
	(void) kill(-pgroup, SIGTERM);
	(void) sleep(2);
	(void) kill(-pgroup, SIGKILL);
	exit(-1);
}

int
/*ARGSUSED*/
main(int argc, char *argv)
{
	struct pam_conv conv = { pam_tty_conv, NULL };
	pam_handle_t *pamh;
	struct passwd *pw;
	int err;

	disable_kbd_signals();
	if ((pw = getpwuid(getuid())) == NULL) {
		(void) fprintf(stderr, "plock: Can't get username: %s\n",
		    strerror(errno));
		exit(1);
	}
	
	/* Initialize PAM framework */
	err = pam_start("plock", pw->pw_name, &conv, &pamh);
	if (err != PAM_SUCCESS) {
		(void) fprintf(stderr, "plock: pam_start failed: %s\n",
		    pam_strerror(pamh, err));
		exit(1);
	}

	/* Authenticate user in order to unlock screen */
	do {
		(void) fprintf(stderr, "Terminal locked for %s. ", pw->pw_name);
		err = pam_authenticate(pamh, 0);
		if (err == PAM_USER_UNKNOWN) {
			logout();
		} else if (err != PAM_SUCCESS) {
			(void) fprintf(stderr, "Invalid password.\n");
		}
	} while (err != PAM_SUCCESS);

	/* Make sure account and password are still valid */
	switch (err = pam_acct_mgmt(pamh, 0)) {
	case PAM_SUCCESS:
		break;
	case PAM_USER_UNKNOWN:
	case PAM_ACCT_EXPIRED:
		/* User not allowed in anymore */
		logout();
		break;
	case PAM_NEW_AUTHTOK_REQD:
		/* The user's password has expired. Get a new one */
		do {
			err = pam_chauthtok(pamh, 0);
		} while (err == PAM_AUTHTOK_ERR);
		if (err != PAM_SUCCESS)
			logout();
		break;
	default:
		logout();
	}

if (pam_setcred(pamh, PAM_REFRESH_CRED) != PAM_SUCCESS){
    logout();
}

	(void) pam_end(pamh, 0);
	return(0);
	/*NOTREACHED*/
}

Other Useful PAM Functions

The previous example, Example 3–1, is a simple application that demonstrates only a few of the major PAM functions. This section describes some other PAM functions that can be useful.

The pam_open_session(3PAM) function is called to open a new session after a user has been successfully authenticated.

The pam_getenvlist(3PAM) function is called to establish a new environment. pam_getenvlist() returns a new environment to be merged with the existing environment.

Writing Conversation Functions

A PAM module or application can communicate with a user in a number of ways: command line, dialog box, and so on. As a result, the designer of a PAM consumer that communicates with users needs to write a conversation function. A conversation function passes messages between the user and module independently of the means of communication. A conversation function derives the message type from the msg_style parameter in the conversation function callback pam_message parameter. See pam_start(3PAM).

Developers should make no assumptions about how PAM is to communicate with users. Rather, the application should exchange messages with the user until the operation is complete. Applications should display the message strings for the conversation function without interpretation or modification. An individual message can contain multiple lines, control characters, or extra blank spaces. Note that service modules are responsible for localizing any strings sent to the conversation function.

A sample conversation function, pam_tty_conv(), is provided below. The pam_tty_conv() takes the following arguments:

The sample function gets user input from stdin. The routine needs to allocate memory for the response buffer. A maximum, PAM_MAX_NUM_MSG, can be set to limit the number of messages. If the conversation function returns an error, the conversation function is responsible for clearing and freeing any memory that has been allocated for responses. In addition, the conversation function must set the response pointer to NULL. Note that clearing memory should be accomplished using a zero fill approach. The caller of the conversation function is responsible for freeing any responses that have been returned to the caller. To conduct the conversation, the function loops through the messages from the user application. Valid messages are written to stdout, and any errors are written to stderr.


Note –

The source code for this example is also available through the Sun download center. See http://www.sun.com/download/products.xml?id=41912db5.



Example 3–2 PAM Conversation Function

/* 
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved. 
 * Use is subject to license terms. 
 */
 
#pragma ident	"@(#)pam_tty_conv.c	1.4	05/02/12 SMI"  

#define	__EXTENSIONS__	/* to expose flockfile and friends in stdio.h */ 
#include <errno.h>
#include <libgen.h>
#include <malloc.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <stropts.h>
#include <unistd.h>
#include <termio.h>
#include <security/pam_appl.h>

static int ctl_c;	/* was the conversation interrupted? */

/* ARGSUSED 1 */
static void
interrupt(int x)
{
	ctl_c = 1;
}

/* getinput -- read user input from stdin abort on ^C
 *	Entry	noecho == TRUE, don't echo input.
 *	Exit	User's input.
 *		If interrupted, send SIGINT to caller for processing.
 */
static char *
getinput(int noecho)
{
	struct termio tty;
	unsigned short tty_flags;
	char input[PAM_MAX_RESP_SIZE];
	int c;
	int i = 0;
	void (*sig)(int);

	ctl_c = 0;
	sig = signal(SIGINT, interrupt);
	if (noecho) {
		(void) ioctl(fileno(stdin), TCGETA, &tty);
		tty_flags = tty.c_lflag;
		tty.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
		(void) ioctl(fileno(stdin), TCSETAF, &tty);
	}

	/* go to end, but don't overflow PAM_MAX_RESP_SIZE */
	flockfile(stdin);
	while (ctl_c == 0 &&
	    (c = getchar_unlocked()) != '\n' &&
	    c != '\r' &&
	    c != EOF) {
		if (i < PAM_MAX_RESP_SIZE) {
			input[i++] = (char)c;
		}
	}
	funlockfile(stdin);
	input[i] = '\0';
	if (noecho) {
		tty.c_lflag = tty_flags;
		(void) ioctl(fileno(stdin), TCSETAW, &tty);
		(void) fputc('\n', stdout);
	}
	(void) signal(SIGINT, sig);
	if (ctl_c == 1)
		(void) kill(getpid(), SIGINT);

	return (strdup(input));
}

/* Service modules do not clean up responses if an error is returned.
 * Free responses here.
 */
static void
free_resp(int num_msg, struct pam_response *pr)
{
	int i;
	struct pam_response *r = pr;

	if (pr == NULL)
		return;

	for (i = 0; i < num_msg; i++, r++) {

		if (r->resp) {
			/* clear before freeing -- may be a password */
			bzero(r->resp, strlen(r->resp));
			free(r->resp);
			r->resp = NULL;
		}
	}
	free(pr);
}

/* ARGSUSED */
int
pam_tty_conv(int num_msg, struct pam_message **mess,
    struct pam_response **resp, void *my_data)
{
	struct pam_message *m = *mess;
	struct pam_response *r;
	int i;

	if (num_msg <= 0 || num_msg >= PAM_MAX_NUM_MSG) {
		(void) fprintf(stderr, "bad number of messages %d "
		    "<= 0 || >= %d\n",
		    num_msg, PAM_MAX_NUM_MSG);
		*resp = NULL;
		return (PAM_CONV_ERR);
	}
	if ((*resp = r = calloc(num_msg,
	    sizeof (struct pam_response))) == NULL)
		return (PAM_BUF_ERR);

	/* Loop through messages */
	for (i = 0; i < num_msg; i++) {
		int echo_off;

		/* bad message from service module */
		if (m->msg == NULL) {
			(void) fprintf(stderr, "message[%d]: %d/NULL\n",
			    i, m->msg_style);
			goto err;
		}

		/*
		 * fix up final newline:
		 * 	removed for prompts
		 * 	added back for messages
		 */
		if (m->msg[strlen(m->msg)] == '\n')
			m->msg[strlen(m->msg)] = '\0';

		r->resp = NULL;
		r->resp_retcode = 0;
		echo_off = 0;
		switch (m->msg_style) {

		case PAM_PROMPT_ECHO_OFF:
			echo_off = 1;
			/*FALLTHROUGH*/

		case PAM_PROMPT_ECHO_ON:
			(void) fputs(m->msg, stdout);

			r->resp = getinput(echo_off);
			break;

		case PAM_ERROR_MSG:
			(void) fputs(m->msg, stderr);
			(void) fputc('\n', stderr);
			break;

		case PAM_TEXT_INFO:
			(void) fputs(m->msg, stdout);
			(void) fputc('\n', stdout);
			break;

		default:
			(void) fprintf(stderr, "message[%d]: unknown type "
			    "%d/val=\"%s\"\n",
			    i, m->msg_style, m->msg);
			/* error, service module won't clean up */
			goto err;
		}
		if (errno == EINTR)
			goto err;

		/* next message/response */
		m++;
		r++;
	}
	return (PAM_SUCCESS);

err:
	free_resp(i, r);
	*resp = NULL;
	return (PAM_CONV_ERR);
}

Writing Modules That Provide PAM Services

This section describes how to write PAM service modules.

Requirements for PAM Service Providers

PAM service modules use pam_get_item(3PAM) and pam_set_item(3PAM) to communicate with applications. To communicate with each other, service modules use pam_get_data(3PAM) and pam_set_data(3PAM). If service modules from the same project need to exchange data, then a unique data name for that project should be established. The service modules can then share this data through the pam_get_data() and pam_set_data() functions.

Service modules must return one of three classes of PAM return code:

If a service module performs multiple functions, these functions should be split up into separate modules. This approach gives system administrators finer-grained control for configuring policy.

Man pages should be provided for any new service modules. Man pages should include the following items:

Service modules are required to honor the PAM_SILENT flag for preventing display of messages. The debug argument is recommended for logging debug information to syslog. Use syslog(3C) with LOG_AUTH and LOG_DEBUG for debug logging. Other messages should be sent to syslog() with LOG_AUTH and the appropriate priority. openlog(3C), closelog(3C), and setlogmask(3C) must not be used as these functions interfere with the applications settings.

Sample PAM Provider Service Module

A sample PAM service module follows. This example checks to see if the user is a member of a group that is permitted access to this service. The provider then grants access on success or logs an error message on failure.

    The example goes through the following steps:

  1. Parse the options passed to this module from the configuration line in /etc/pam.conf.

    This module accepts the nowarn and debug options as well as a specific option group. With the group option, the module can be configured to allow access for a particular group other than the group root that is used by default. See the definition of DEFAULT_GROUP in the source code for the example. For example, to allow telnet(1) access by users that belong to group staff, one could use the following line, which is in the telnet stack in /etc/pam.conf:

    telnet  account  required  pam_members_only.so.1 group=staff
  2. Get the username, service name and hostname.

    The username is obtained by calling pam_get_user(3PAM) which retrieves the current user name from the PAM handle. If the user name has not been set, access is denied. The service name and the host name are obtained by calling pam_get_item(3PAM).

  3. Validate the information to be worked on.

    If the user name is not set, deny access. If the group to be worked on is not defined, deny access.

  4. Verify that the current user is a member of the special group that allows access to this host and grant access.

    In the event that the special group is defined but contains no members at all, PAM_IGNORE is returned to indicate that this module does not participate in any account validation process. The decision is left to other modules on the stack.

  5. If the user is not a member of the special group, display a message to inform the user that access is denied.

    Log a message to record this event.

The following example shows the source code for the sample PAM provider.


Note –

The source code for this example is also available through the Sun download center. See http://www.sun.com/download/products.xml?id=41912db5.



Example 3–3 Sample PAM Service Module

/*
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <stdio.h>
#include <stdlib.h>
#include <grp.h>
#include <string.h>
#include <syslog.h>
#include <libintl.h>
#include <security/pam_appl.h>

/*
 * by default, only users who are a member of group "root" are allowed access
 */
#define	DEFAULT_GROUP "root"

static char *NOMSG =
	"Sorry, you are not on the access list for this host - access denied.";

int
pam_sm_acct_mgmt(pam_handle_t * pamh, int flags, int argc, const char **argv)
{
	char *user = NULL;
	char *host = NULL;
	char *service = NULL;
	const char *allowed_grp = DEFAULT_GROUP;
	char grp_buf[4096];
	struct group grp;
	struct pam_conv *conversation;
	struct pam_message message;
	struct pam_message *pmessage = &message;
	struct pam_response *res = NULL;
	int i;
	int nowarn = 0;
	int debug = 0;

	/* Set flags to display warnings if in debug mode. */
	for (i = 0; i < argc; i++) {
		if (strcasecmp(argv[i], "nowarn") == 0)
			nowarn = 1;
		else if (strcasecmp(argv[i], "debug") == 0)
			debug = 1;
		else if (strncmp(argv[i], "group=", 6) == 0)
			allowed_grp = &argv[i][6];
	}
	if (flags & PAM_SILENT)
		nowarn = 1;

	/* Get user name,service name, and host name. */
	(void) pam_get_user(pamh, &user, NULL);
	(void) pam_get_item(pamh, PAM_SERVICE, (void **) &service);
	(void) pam_get_item(pamh, PAM_RHOST, (void **) &host);

	/* Deny access if user is NULL. */
	if (user == NULL) {
		syslog(LOG_AUTH|LOG_DEBUG,
		    "%s: members_only: user not set", service);
		return (PAM_USER_UNKNOWN);
	}

	if (host == NULL)
		host = "unknown";

	/*
	 * Deny access if vuser group is required and user is not in vuser
	 * group
	 */
	if (getgrnam_r(allowed_grp, &grp, grp_buf, sizeof (grp_buf)) == NULL) {
		syslog(LOG_NOTICE|LOG_AUTH,
		    "%s: members_only: group \"%s\" not defined",
		    service, allowed_grp);
		return (PAM_SYSTEM_ERR);
	}

	/* Ignore this module if group contains no members. */
	if (grp.gr_mem[0] == 0) {
		if (debug)
			syslog(LOG_AUTH|LOG_DEBUG,
			    "%s: members_only: group %s empty: "
			    "all users allowed.", service, grp.gr_name);
		return (PAM_IGNORE);
	}

	/* Check to see if user is in group. If so, return SUCCESS. */
	for (; grp.gr_mem[0]; grp.gr_mem++) {
		if (strcmp(grp.gr_mem[0], user) == 0) {
			if (debug)
				syslog(LOG_AUTH|LOG_DEBUG,
				    "%s: user %s is member of group %s. "
				    "Access allowed.",
				    service, user, grp.gr_name);
			return (PAM_SUCCESS);
		}
	}

	/*
	 * User is not a member of the group.
	 * Set message style to error and specify denial message.
	 */
	message.msg_style = PAM_ERROR_MSG;
	message.msg = gettext(NOMSG);

	/* Use conversation function to display denial message to user. */
	(void) pam_get_item(pamh, PAM_CONV, (void **) &conversation);
	if (nowarn == 0 && conversation != NULL) {
		int err;
		err = conversation->conv(1, &pmessage, &res,
		    conversation->appdata_ptr);
		if (debug && err != PAM_SUCCESS)
			syslog(LOG_AUTH|LOG_DEBUG,
			    "%s: members_only: conversation returned "
			    "error %d (%s).", service, err,
			    pam_strerror(pamh, err));

		/* free response (if any) */
		if (res != NULL) {
			if (res->resp)
				free(res->resp);
			free(res);
		}
	}

	/* Report denial to system log and return error to caller. */
	syslog(LOG_NOTICE | LOG_AUTH, "%s: members_only: "
	    "Connection for %s not allowed from %s", service, user, host);

	return (PAM_PERM_DENIED);
}