User Authentication on the Solaris OS: Part 1

   
By Rich Teer, July 2007  

User authentication on the Solaris OS is explained, and an example program shows how to compare a password entered by the user to their login password.

Introduction

There are many times when our programs need to verify the identity of a user, particularly when we want to perform a potentially dangerous or privileged operation. The example with which most Solaris OS users are familiar is logging in: before we can do anything with the system, we must correctly enter our user name and password. Another common example is unlocking the screen when the screen lock is engaged.

In this short series of articles, we'll see how to write programs that use passwords and other methods to authenticate the user. In this part we'll discuss the principles and illustrate them with an example that works only with password-based authentication. In part two we'll see how to make our program more flexible, with the use of PAM (Pluggable Authentication Modules).

Authentication and Authorisation

Two terms that often come up in discussions about passwords are authentication and authorisation. These terms are sometimes misused, so we'll define them here. Authentication is what happens when the computer verifies our identity: when we log in using a correct password, the operating system deems itself to have authenticated us. Authorisation, on the other hand, determines whether or not we are allowed to perform a given task or access a certain resource.

Just because we have authenticated ourselves, we don't necessarily have the authorisation to do what we want. For example, if we log in as our normal user, we can't normally affect other users' files: even though our identity has been authenticated, we do not have the appropriate authorisation. If we become root, however, we usually have authorisation to do with the system what we will. For the sake of simplicity, we are ignoring things like RBAC (Role Based Access Control) and fine-grained privileges. Readers interested in RBAC should refer to System Administration Guide: Security Services . Details of fine-grained privileges can be found in Programming in the Solaris OS With Privileges.

Although there are several ways we can authenticate ourselves (for example, biometric scans, identity cards, and so on), we'll only consider password authentication in these articles.

Password Storage and Encryption Algorithms

If the password we enter in response to a challenge is to be compared to the correct one, the latter must be stored somewhere on the system. On Solaris systems, users' passwords are stored in the file /etc/shadow, although back in the mists of time they were stored in /etc/passwd (hence its name). For various reasons /etc/passwd must be readable by all users. To avoid exposing encrypted passwords, System V variants of the UNIX platform (of which Solaris is one) store passwords in /etc/shadow, which is readable only by the superuser. (Note that the preceding text assumes the use of local files for password storage. Larger sites will probably keep most users' passwords in a centralised repository using NIS, LDAP, or NIS+.)

The passwords stored in /etc/shadow are encrypted using a one-way hashing algorithm, because storing passwords in plain text--that is, in their unencrypted form--is not a very good idea! The one-way nature of the algorithm used to encrypt passwords means that there is no way to derive the password from its encrypted form. Prior to Solaris 9 12/02, the encryption algorithm was based on DES (Data Encryption Standard) with variations intended to frustrate the use of hardware implementations of a key search.

The problems with DES are that computer processing speeds have increased significantly since 1977 (when DES was introduced), making brute-force cracking of DES-encrypted passwords a real threat, and that passwords are limited to a length of only eight characters. To mitigate these threats, Solaris 9 12/02 introduced a new encryption infrastructure which enables the use of stronger encryption algorithms such as MD5 and Blowfish. (In addition to their increased complexity, these new algorithms allow the use of passwords longer than 8 characters.) Because of their increased key length and algorithm complexity, brute-force cracking of passwords encrypted with either of these algorithms will take orders of magnitude longer than cracking DES-encrypted passwords.

One question that we could ask at this point is: "How can we determine which algorithm was used to encrypt a password?" To answer this, we need to examine a couple of shadow file entries. Here's an example of the author's shadow entry on one of his machines:

rich:TISFHhgBSAtgU:13608::::::

The shadow(4) man page describes these fields in detail. Our discussion focuses on the first two. The first field is the user name and the second field contains the encrypted password. If the first character of the password is not "$", we know that DES-based encryption is being used. The first two characters of this 13-character string are known as the salt, which is used to perturb the DES algorithm in one of 4096 ways, and the remaining 11 characters are the encrypted password itself. If the first character is "$", the characters up to the next "$" indicate the encryption algorithm used, and those between the second and third "$" are the salt.

On another of the author's machines, a different encryption algorithm is used. Here's the corresponding shadow file entry:

rich:$2a$04$NZJWn7W2skvQRC5lW3H7q.ZTE8bz4xbCAtU1ttzUOy63si3phphUu:13613::::::

Here we see that the first character is "$", indicating that a different algorithm is used (in this case we're using the algorithm denoted by "2a", which indicates Blowfish). The mapping of identifiers to their respective libraries (and therefore their specific algorithms) is defined in /etc/security/crypt.conf. Each algorithm has its own man page which contains more detailed information, including any options that may be applied. We've used Blowfish in our example, which is described in crypt_bsdbf(5).

The algorithm used for comparing existing passwords is derived from the shadow file entry, but how do we decide which algorithm is used to create new ones? The answer is that new passwords are created using the algorithm specified by CRYPT_DEFAULT in /etc/security/policy.conf. The exception is if the password is being changed and the old password was encrypted using one of the algorithms specified by CRYPT_ALGORITHMS_ALLOW in the same file, in which case that algorithm is used.

Regardless of which encryption algorithm is used, before we can authenticate a user we must collect the password they enter.

Reading Passwords From Users

When prompting a user for their password, one method that comes to mind is to use the printf and fgets functions to print the prompt and read the password respectively. The problem with this approach is that unless we take steps to do otherwise, the entered password will be echoed on the screen: an obvious security flaw! We could write functions to handle the echo toggling, but rather than reinvent the wheel we should use the provided functions. The first of these is named getpass:

char *getpass (const char *
                   prompt);
                

The getpass function prints the prompt, turns off echoing and reads a newline-terminated line of text from the terminal, and then restores the terminal's previous state, returning a pointer to the entered string. Although it is standards compliant, we should avoid using getpass because it has one fatal flaw: only the first eight characters of the entered string are returned to the caller, thereby limiting passwords (no matter what algorithm is used) to eight significant characters. To avoid this problem we should use getpassphrase:

char *getpassphrase (const char *
                   prompt);
                

This function is identical to getpass, except that it reads and returns a string up to 257 characters in length. We'll use it in the example that follows. Although it is not currently specified by any formal standard, getpassphrase is common enough that we can use it without fear of non-portability. In other words, getpassphrase is a de facto standard.

Encrypting Passwords

Having read a password from the user, we need to compare it to the one stored in the shadow file. The first step in doing that is to encrypt it. If we are not interested in supporting the new encryption algorithms, this is as simple as calling the crypt function:

char *crypt (const char *
                   key, const char *
                   salt);
                

If we are comparing entered passwords to the one stored in the shadow file, the easiest way of passing the correct salt to crypt is to pass it the user's encrypted password. We show how to read a user's shadow file entry in the next section.

If, instead of comparing an entered password to an existing one, we wanted to create a new password and we wanted to support the newer encryption algorithms, we would call crypt_gensalt to generate the salt we pass to crypt:

char *crypt_gensalt (const char *
                   oldsalt, const struct passwd *
                   userinfo);
                

If oldsalt is NULL, the algorithm specified by CRYPT_DEFAULT in /etc/security/policy.conf is used. The userinfo structure is populated by using the getpwnam family of functions (the description of which is beyond the scope of this article).

Reading a User's Shadow File Entry

We stated previously that when encrypting a password for comparison with a user's current one, we must pass it the appropriate salt. The easiest way of doing that is to pass the user's encrypted password to the crypt function. And to do that we need to read the user's password from the shadow file. For this task, we use the getspnam function:

struct spwd *getspnam (const char *
                   name);
                

The getspnam function returns a pointer to a structure containing the shadow file information for the user specified by name. The structure consists of several members, but for now we're only interested in one: sp_pwdp, which is a pointer to the encrypted password. This pointer is what we pass as the salt to crypt.

Note that because /etc/shadow is readable only by root, only processes with an effective user ID of 0 will be able to call getspnam successfully. (Privilege-aware processes require the FILE_DAC_READ privilege to be in their Effective privilege set.)

Putting It All Together

We're now ready to apply what we've learned so far to writing a simple application that performs password authentication. Our example will repeatedly ask for the current user's password until the correct one is entered, which will cause the program to end. (The current user is determined by the real user ID of the process.) 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 <shadow.h>
 7 #include <crypt.h>
 8 #include <string.h>
   
 9 int main (void)
10 {
11     struct passwd *pwd_info;
12     struct spwd *spwd_info;
13     char *ct_passwd;
14     char *enc_passwd;
   
15     if ((pwd_info = getpwuid (getuid ())) == NULL) {
16             fprintf (stderr, "Call to getpwuid failed\n");
17             exit (1);
18     }
   
19     for (;;) {
20         ct_passwd = getpassphrase ("Enter password: ");
   
21         if ((spwd_info = getspnam (pwd_info -> pw_name)) == NULL) {
22             fprintf (stderr, "Call to getspnam failed\n");
23             exit (1);
24         }
   
25         enc_passwd = crypt (ct_passwd, spwd_info -> sp_pwdp);
   
26         if (strcmp (enc_passwd, spwd_info -> sp_pwdp) == 0) {
27             printf ("Passwords match.\n");
28             break;
29         } else
30             printf ("Passwords don't match.\n");
31     }
   
32     return (0);
33 }

Let's take a closer look at this 33-line program.

1-14: Include header files and declare variables.

15-18: 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 the Solaris platform, this file is world-writable, so a malicious user could change their entry to that of an authorised user, and our program would not detect the change. We do this because we must pass the user's user name to getspnam later.

20: Prompt for and read a password from the user.

21-24: Call getspnam to retrieve the user's shadow file entry. Amongst other things, this information includes the encrypted form of the user's password. We do this for every iteration through the loop in case the user's password is changed while running this program.

25: Encrypt the password the user entered, passing the encrypted version of the password as the salt.

26-31: Compare the newly encrypted password with the one we retrieved from the shadow file. Print a message saying so and exit if they match, otherwise print a failure message and try again.

After compiling this program, lets's run it and see what happens:

rich@marrakesh4156#  
                   make check_pass
cc    -o check_pass check_pass.c
rich@marrakesh4157#  
                   ./check_pass
Enter password:
Call to getspnam failed
                

The call to getspnam failed because the user "rich" is not privileged. Let's become root and try again:

rich@marrakesh4158#  
                   su
Password:
#  
                   ./check_pass
Enter password:
Passwords don't match.
Enter password:
Passwords match.
                

This time, after first entering a wrong password, we succeed.

With a little bit of work, this example could be modified to become a simple terminal locking program, an exercise which is left to the reader.

Summary

In this article we have explored simple password-based user authentication. We started off by defining and describing the differences between two terms that are important when talking about passwords: authentication and authorisation, the former being the act of proving our identity, and the latter being whether or not we are allowed to perform a certain task or access a given resource.

We then described how passwords are stored when the local file repository is used, and how Solaris systems administrators can define which password encryption algorithms may be used. (Out of the box, the Solaris OS provides the original DES-based algorithm, in addition to newer, more secure, algorithms such as Blowfish and MD5.)

Having described some of the theory behind password authentication, we then looked at the API that the Solaris OS provides for performing various related tasks: prompting for and obtaining a password from the user (stating that getpassphrase is the preferred function for doing this), encrypting that password using crypt, and reading a user's shadow file entry using getspnam.

Finally, we tied all of these concepts together by writing a simple example program that prompts the user for a password and compares it to their login password.

In the next article in this series, we'll see how to use PAM (Pluggable Authentication Modules) when writing programs that must perform more flexible password authentication.

Acknowledgements

Many thanks to Glenn Brunette for reviewing this article.

Reference
About the Author

Rich Teer is CEO of My Online Home Inventory and an independent Solaris OS 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 numerous related articles. He was a member of the OpenSolaris pilot program, and currently serves on the OpenSolaris Governing Board (OGB). Rich lives in Kelowna, British Columbia, with his wife, Jenny. His web site can be found at www.rite-group.com/rich.

Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.