10.5 Authenticating Users

Often, if you have a setuid program, you don't want to offer its services to everyone. For example, the su program lets you become root only if you know the root password. The program makes you prove that you are entitled to become root before going ahead with its actions. This process is called authentication—the su program is checking to see that you are authentic.

If you're administering a very secure system, you probably don't want to let people log in just by typing an ordinary password. Users tend to write down passwords, and black hats tend to find them. Users tend to pick passwords that involve their birthdays, the names of their pets, and so forth. [7] Passwords just aren't all that secure.

[7] It has been found that system administrators tend to pick the word god as their password more often than any other password. (Make of that what you will.) So, if you ever need root access on a machine and the sysadmin isn't around, a little divine inspiration might be just what you need.

For example, many organizations now require the use of special "one-time" passwords that are generated by special electronic ID cards that users keep with them. The same password can't be used twice, and you can't get a valid password out of the ID card without entering a PIN. So, an attacker must obtain both the physical card and the PIN to break in. In a really secure facility, retinal scans or other kinds of biometric testing are used.

If you're writing a program that must perform authentication, you should allow the system administrator to use whatever means of authentication is appropriate for that installation. GNU/Linux comes with a very useful library that makes this very easy. This facility, called Pluggable Authentication Modules, or PAM, makes it easy to write applications that authenticate their users as the system administrator sees fit.

It's easiest to see how PAM works by looking at a simple PAM application. Listing 10.4 illustrates the use of PAM.

Listing 10.4 (pam.c) PAM Example
#include <security/pam_appl.h> 
#include <security/pam_misc.h> 
#include <stdio.h> 
int main () 
  pam_handle_t* pamh; 
  struct pam_conv pamc; 
  /* Set up the PAM conversation.                  */ 
  pamc.conv = &misc_conv; 
  pamc.appdata_ptr = NULL; 
  /* Start a new authentication session.           */ 
  pam_start ("su", getenv ("USER"), &pamc, &pamh); 
  /* Authenticate the user.                        */ 
  if (pam_authenticate (pamh, 0) != PAM_SUCCESS) 
    fprintf (stderr, "Authentication failed!\n"); 
    fprintf (stderr, "Authentication OK.\n"); 
  /* All done.                                     */ 
  pam_end (pamh, 0); 
  return 0; 

To compile this program, you have to link it with two libraries: the libpam library and a helper library called libpam_misc:

% gcc -o pam pam.c -lpam -lpam_misc 

This program starts off by building up a PAM conversation object. This object is used by the PAM library whenever it needs to prompt the user for information. The misc_conv function used in this example is a standard conversation function that uses the terminal for input and output. You could write your own function that pops up a dialog box, or that uses speech for input and output, or that provides even more exotic input and output methods.

The program then calls pam_start. This function initializes the PAM library. The first argument is a service name. You should use a name that uniquely identifies your application. For example, if your application is named whizbang, you should probably use that for the service name, too. However, the program probably won't work until the system administrator explicitly configures the system to work with your service. So, in this example, we use the su service, which says that our program should authenticate users in the same way that the su command does. You should not use this technique in a real program. Pick a real service name, and have your installation scripts help the system administrator to set up a correct PAM configuration for your application.

The second argument is the name of the user whom you want to authenticate. In this example, we use the value of the USER environment variable. (Normally, this is the username that corresponds to the effective user ID of the current process, but that's not always the case.) In most real programs, you would prompt for a username at this point. The third argument indicates the PAM conversation, discussed previously. The call to pam_start fills in the handle provided as the fourth argument. Pass this handle to subsequent calls to PAM library routines.

Next, the program calls pam_authenticate. The second argument enables you to pass various flags; the value 0 means to use the default options. The return value from this function indicates whether authentication succeeded.

Finally, the programs calls pam_end to clean up any allocated data structures.

Let's assume that the valid password for the current user is "password" (an exceptionally poor password). Then, running this program with the correct password produces the expected:

% ./pam 
Password: password 
Authentication OK. 

If you run this program in a terminal, the password probably won't actually appear when you type it in; it's hidden to prevent others from peeking at your password over your shoulder as you type.

However, if a hacker tries to use the wrong password, the PAM library will correctly indicate failure:

% ./pam 
Password: badguess 
Authentication failed! 

The basics covered here are enough for most simple programs. Full documentation about how PAM works is available in /usr/doc/pam on most GNU/Linux systems.