User Authentication on the Solaris OS
Part 2: Introduction to PAM
Introduction
In the first article in this series we covered the basics of password-based user authentication. We defined the terms authentication and authorisation, discussed local file-based password storage and encryption algorithms, and described the API that Solaris provides for reading and encrypting passwords. Finally, we showed an example program that asks the invoker for a password and compares it to their login password.
Solaris provides an extensible mechanism for user authentication, called the PAM (Pluggable Authentication Modules) framework. In this article we’ll provide an overview of the PAM facility, developing a PAM-based version of the program that we wrote in the first article in this series. Although an application that uses PAM for user authentication is more complex than one that doesn’t, using PAM has the advantage of decoupling the authentication mechanism from the application. This means that new authentication mechanisms can be added (e.g., biometric scans) or existing ones modified without changing the application.
PAM Overview
The PAM facility is made up of two parts: the PAM library which provides
the API and SPI (Service Provider Interface), and various stackable
service modules. A configuration file, /etc/pam.conf,
is used to configure the service modules for various system services,
such as login or cron. Applications communicate
with the PAM library via the API whereas service modules use the SPI.
PAM Service Modules
A service module is a shared library that provides one or more of the following authentication and security services:
- Authentication. These service modules are used to authenticate access to an account or service, and to set up user credentials.
- Account management. These service modules determine the validity of the current user’s account. For example, an account management module could check password or account expiration, or enforce access time restrictions.
- Session management. These service modules are responsible for setting up and tearing down login sessions.
- Password management. These service modules enforce password strength and previous-use rules, and perform authentication token updates.
Ideally, PAM services should be implemented in service modules which
have simple well-defined tasks, so that configuration flexibility is
increased. The service modules can then be used as required by appropriate
definitions in /etc/pam.conf.
PAM Configuration File
We stated previously that the PAM facility is configured with its
configuration file, /etc/pam.conf, which is managed by the
system administrator. Each system service may have one or more entries
in pam.conf, the order of which is very important (system
services that don’t have their own set of entries use those for the
“other” service): a mis-configured pam.conf
can result in a system that locks out all users in multiuser mode.
Each entry in the configuration file consists of several space-separated fields, the first four of which are mandatory:
- Service name. This is the name of the service this
entry pertains to. Examples include
cron,login, andpasswd. An application can use one or more service names, as determined by its needs. The service nameotheris reserved as a wildcard: if the service name specified by the application isn’t found in the configuration file, the entries forotherare used. An example of an application that uses more than one service name is the SSH daemon,sshd, which uses per authentication method keywords (see the SECURITY section of thesshdmanual page for more details). - Module type. This is the type of service module, which is one
of
account,auth,password, orsession.accountservice modules verify the validity of the account (e.g., password or account expiry, and access hours restrictions),authservice modules authenticate account access and set up user credentials,passwordservice modules are used for managing users’ password changes, andsessionservice modules are used to set up and tear down login sessions. - Control flag. This determines the role of the module in
determining the success or failure of the service. Valid
values are
binding,optional,required,requisite, andsufficient. We describe these in more detail in the next section. - Module path. This is the file system path to the shared
object that implements the service module. Relative path names are
assumed to be relative to
/usr/lib/security/$ISA, where$ISAis a macro that causes the PAM library to look in the directory for the application’s specific architecture. - Module options. This lists any options that are to be passed to the service module, and should be documented in the module’s manual page.
Stacking PAM Modules
We stated previously that applications can use multiple
service modules, and that an entry for each module appears
in /etc/pam.conf. When an application calls
pam_authenticate, pam_acct_mgmt,
pam_setcred, pam_open_session,
pam_close_session, or pam_chauthtok, the PAM
library reads pam.conf to determine which modules to use.
If the service has only one entry in pam.conf the
result of that module determines the outcome of an operation that
uses that service. However, if the service has multiple entries in
pam.conf, the modules are stacked, and the outcome of
operations is determined by an integration process that takes into
account the results from all modules in the service’s PAM stack (unless
prematurely terminated).
For a given service, each module is executed in the order they appear
in pam.conf. The success or failure of each module is
integrated into the overall operation result, as determined by the
value of the module’s control flag. This control flag must be one of
the following five values:
binding. If no previous modules with arequiredcontrol flag set have failed, successfully meeting this module’s requirements will immediately return a successful result to the application, bypassing any other modules in the stack. A failure causes a “required failure” to be recorded and subsequent modules in the stack are executed.optional. If this module fails, an “optional failure” is recorded and subsequent modules in the stack are executed. Otherwise subsequent modules in the stack are executed.required. In order to use the service, this module’s requirements must be met. If they are not met, a “required failure” is recorded and subsequent modules in the stack are executed. Only if allrequiredorbindingmodules have no errors is success returned to the application.requisite. In order to use the service, this module’s requirements must be met. If they are not met, a “required failure” is immediately returned to the application and subsequent modules in the stack are not executed. Otherwise, subsequent modules in the stack are executed. Only if allrequisitemodules have no errors is success returned to the application.sufficient. If this module fails, an “optional failure” is recorded. Otherwise, if no previous errors have been recorded success is immediately returned to the application and subsequent modules in the stack are not executed.
The preceding description is fairly complicated, so let’s take a look at a real example from pam.conf, in this case for the rlogin service. Here are rlogin’s entries in pam.conf:
rlogin auth sufficient pam_rhosts_auth.so.1
rlogin auth requisite pam_authtok_get.so.1
rlogin auth required pam_dhkeys.so.1
rlogin auth required pam_unix_cred.so.1
rlogin auth required pam_unix_auth.so.1
The first observation we can make is that all of
rlogin’s modules are for authentication,
and that first one (pam_rhosts_auth) is
flagged as sufficient. This means that when the
rlogin service requests authentication, a positive result
from pam_rhosts_auth will halt processing and immediately
return success to the application.
If pam_rhosts_auth fails, the next module
(pam_authtok_get) is executed. This is flagged as
requisite which means that if pam_authtok_get
fails, processing is halted and failure is immediately returned to the
application. If pam_authtok_get is successful, the remaining
three modules (pam_dhkeys, pam_unix_cred,
and pam_unix_auth) are executed in sequence. Each of these
modules is flagged as required, which means that all of
them must return successfully in order for success to be returned to
the application. However, if one of them fails, the failure is noted
and is returned to the application only after all three modules have
been executed.
Once pam_unix_auth has been executed, there are no more
modules to execute so the indication of success or failure is returned
to the application; if failure is indicated, the user will be denied
access to the system via rlogin.
Some API Functions
Now that we’ve described the PAM framework and how it’s
configured, let’s take a look at some of the PAM API functions. For
a very simple PAM-aware application, there are three functions we
need to consider: pam_start, pam_end, and
pam_authenticate.
The pam_start Function
The pam_start function is used to initiate a PAM
authentication transaction.
#include <security/pam_appl.h>
int pam_start (const char *service, const char *user,
const struct pam_conv *pam_conv, pam_handle_t **pamh);
This function initiates an authentication transaction for the service and
user specified by service and user
respectively. The pam_conv argument specifies the
conversation function to be used; on success, pamh
points to a handle which must be used with subsequent PAM functions
(in much the same way that file descriptors are used for files). We
discuss conversation functions in the next part of this series, but for
now we can state that as the name implies, conversation functions handle
conversations with the user (e.g., prompting for their password).
The pam_end Function
The pam_end function is used to terminate a PAM
authentication transaction.
#include <security/pam_appl.h>
int pam_end (pam_handle_t *pamh, int status);
The PAM authentication transaction specified by pamh
is terminated, and the status argument is passed to
the cleanup function (if any) stored within the PAM handle.
The pam_authenticate Function
The pam_authenticate function is used to authenticate the
current user.
#include <security/pam_appl.h>
int pam_authenticate (pam_handle_t *pamh, int flags);
This function is used to authenticate the current user (i.e., the user
specified at the time the handle, pamh, was created by
calling pam_start). Exactly how the user is authenticated is
module-dependant, but authentication usually involves asking the user for
their password, prompting the user for their one-time password or token,
or some other method of validating the user’s identity. Other
authentication mechanisms include biometric scans and smart cards. The
flags argument may be used to set various flags that
are used by the authentication service.
An Example PAM Application
We’ve covered enough material now to take a look at an example application that uses PAM for authentication. As in the previous article, our example will repeatedly ask for the current user’s (as determined by the real user ID of the process) password until the correct one is entered, which will cause the program to end. Here’s the source code for our example program:
1 #include <sys/types.h>
2 #include <unistd.h>
3 #include <pwd.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <security/pam_appl.h>
8 extern int check_conv (int num_msg, struct pam_message **msg,
9 struct pam_response **resp, void *app_data);
10 int main (void)
11 {
12 struct passwd *pwd_info;
13 struct pam_conv conv = {check_conv, NULL};
14 pam_handle_t *ph;
15 int error;
16 if ((pwd_info = getpwuid (getuid ())) == NULL) {
17 fprintf (stderr, "Call to getpwuid failed\n");
18 exit (1);
19 }
20 if ((error = pam_start ("check_pass", pwd_info -> pw_name,
21 &conv, &ph)) != PAM_SUCCESS) {
22 fprintf (stderr, "Call to pam_start failed: %s\n",
23 pam_strerror (ph, error));
24 exit (1);
25 }
26 for (;;) {
27 error = pam_authenticate (ph, 0);
28 if (error == PAM_SUCCESS) {
29 printf ("Passwords match.\n");
30 break;
31 } else {
32 printf ("Passwords don’t match.\n");
33 }
34 }
35 pam_end (ph, 0);
36 return (0);
37 }
Let’s take a closer look at this 37-line program.
1-9 Include header files and declare the
conversation function. We describe conversation functions in the next
part of this series, but show the function we use for this example below
for readers who want to experiment now.
16-19 Call getpwuid to obtain the password file
information for the current user, as identified by the process’ real
user ID (we could use getlogin or cuserid instead, but security conscious
programs should avoid these functions because they rely on (and trust)
the contents of /var/adm/utmpx. On some UNIX platforms—although not
Solaris—this file is world-writable so a malicious user could change
their entry to that of an authorised user, and our program would be none
the wiser). We do this because we must pass the user’s user name
to pam_start in the next step.
20-25 Call pam_start to initiate the PAM session, using
“check_pass” as the service name, and the user name we ascertained
previously. Note that because the service name we’re using
doesn’t exist in the default /etc/pam.conf, the authentication
entries for the other service will be used. If an error occurs, inform
the user and exit.
26-34 In an infinite loop, call pam_authenticate to
authenticate the user, using the conversation function we established
when we called pam_start. Very briefly, it is this conversation function
that prompts for and reads a password from the user. If pam_authenticate
returns PAM_SUCCESS, print a message stating that the passwords match
and break out of the loop, otherwise print a failure message and try
again.
35 Call pam_end to terminate the PAM
transaction.
Here’s the source code for our conversation function (we’ll take a detailed look at it next time):
1 #include <sys/types.h>
2 #include <unistd.h>
3 #include <pwd.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <security/pam_appl.h>
8 int check_conv (int num_msg, struct pam_message **msg,
9 struct pam_response **resp, void *app_data)
10 {
11 struct pam_message *m = *msg;
12 struct pam_response *r;
13 int i;
14 char *ct_passwd;
15 if ((num_msg <= 0) || (num_msg >= PAM_MAX_NUM_MSG)) {
16 fprintf (stderr, "Invalid number of messages\n");
17 *resp = NULL;
18 return (PAM_CONV_ERR);
19 }
20 if ((*resp = r = calloc (num_msg, sizeof (struct pam_response))) == NULL)
21 return (PAM_BUF_ERR);
22 for (i = 0; i < num_msg; i++) {
23 switch (m->msg_style) {
24 case PAM_PROMPT_ECHO_OFF:
25 ct_passwd = getpassphrase ("Enter password: ");
26 r->resp = strdup (ct_passwd);
27 m++;
28 r++;
29 break;
30 case PAM_PROMPT_ECHO_ON:
31 if (m->msg)
32 fputs (m->msg, stdout);
33 r->resp = NULL;
34 m++;
35 r++;
36 break;
37 case PAM_ERROR_MSG:
38 if (m->msg)
39 fprintf (stderr, "%s\n", m->msg);
40 m++;
41 r++;
42 break;
43 case PAM_TEXT_INFO:
44 if (m->msg)
45 printf ("%s\n", m->msg);
46 m++;
47 r++;
48 break;
49 }
50 return (PAM_SUCCESS);
51 }
52 }
After compiling this program, lets’s run it and see what happens:
rich@marrakesh4112# make pam_check_pass
cc -o pam_check_pass -lpam pam_check_pass.c pam_check_pass_conv.c
pam_check_pass.c:
pam_check_pass_conv.c:
rich@marrakesh4113# ./pam_check_pass
Enter password:
Passwords don’t match.
Enter password:
Passwords don’t match.
Notice that no matter what password we type in, we are told that they
don’t match. This is because the user rich is not privileged,
and behind the scenes we need to read the shadow file to compare the
typed in password to our login one (because rich is a local user),
and the shadow file (/etc/shadow) is readable only by
root. Let’s become root and try again:
rich@marrakesh4114# su
Password:
# ./pam_check_pass
Enter password:
Passwords don’t match.
Enter password:
Passwords match.
This time, after first entering a wrong password, we succeed. (We should note that using least privilege isn’t an option for us when using PAM. This is because all zone privileges are required.)
As before, with a little bit of work, this example could be modified to become a simple terminal locking program, and again this is an exercise which is left to the reader.
Summary
We started this article with a brief overview of PAM, and then described
various parts of the PAM framework. We talked about PAM service modules,
the PAM configuration file, /etc/pam.conf, and described
how service modules can be stacked.
We then described a few important functions in the PAM
API: pam_start, pam_end, and
pam_authenticate. Finally, we wrote an example program
that tied all these things together: a PAM-aware version of the password
comparison program we wrote in the first instalment of this series.
In the next article in this series, we’ll see how to write PAM conversation functions, and take a look at some of the other API functions.
Acknowledgements
Many thanks to Glenn Brunette for reviewing this article.
Recommended Reading
The author’s book, Solaris Systems Programming [Teer 2005], is essential for readers developing Solaris applications. Many books in the Solaris 10 Software Developer Collection at docs.sun.com/app/docs/coll/45.20 are also worth a read—or at least, being aware of.
PAM is the subject of two Sun BluePrints books [Sun 2002a and Sun 2002b]. Chapter 3 of [Sun 2006] also contains much useful information.
Bibliography
Sun Microsystems. 2002a. Extending Authentication in the Solaris 9 Operating Environment Using Pluggable Authentication Modules (PAM): Part 1, www.sun.com/blueprints/0902/816-7669-10.pdf.
Sun Microsystems. 2002b. Extending Authentication in the Solaris 9 Operating Environment Using Pluggable Authentication Modules (PAM): Part 2, www.sun.com/blueprints/1002/816-7670-10.pdf.
Sun Microsystems. 2006. Solaris Security for Developers Guide, docs.sun.com/app/docs/doc/816-4863.
Teer, Rich. 2005. Solaris Systems Programming, ISBN 0-201-75039-2. See richteer.ca/ssp for more details.
Author’s Bio
Rich Teer is an independent Solaris consultant who has been an active member of the Solaris community for more than ten years. He is the author of the best-selling Sun Microsystems Press book, Solaris Systems Programming, and several Solaris-related articles. He was a member of the OpenSolaris pilot program, and currently serves on the OpenSolaris Community Advisory Board (CAB). Rich lives in Kelowna, British Columbia, with his wife, Jenny, and their canine child, Judge. His web site can be found at richteer.ca.