home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 5 - Maintaining State
Protecting Client-Side Information

In this section...

Introduction
Message Authentication Checks
Encrypting Client-Side State Information

Introduction

   Show Contents   Go to Top   Previous Page   Next Page

The cookie-based implementation of the hangman game is a lot classier than the first implementation. Not only does it have the advantage of maintaining state across browser sessions, but the game is also somewhat harder to cheat. While the user is actively playing the game, the cookie is kept in memory where it is difficult to read without the benefit of a debugger. However, after the user quits the browsing session, the cookie is written out to disk; determined cheaters could still find and edit the cookie database file if they wanted to make their statistics look better.

When you store information on the client side of the connection, peeking and tampering is a general problem. Fortunately, the cure is relatively simple. To prevent tampering, you can use a message authentication check (MAC)--a form of checksum that will detect if the user has altered the information in any way. To prevent peeking, you can encrypt the information using an encryption key that is known to you but not to the user.

Message Authentication Checks

   Show Contents   Go to Top   Previous Page   Next Page

Let's add a MAC to the cookie used in the last section's example. There are many ways to compute a checksum, but the most reliable use a class of algorithms known as message digests. A message digest algorithm takes a large amount of data (usually called the "message") and crunches it through a complex series of bit shifts, rotates, and other bitwise operations until it has been reduced to a smallish number known as a hash. The widely used MD5 message digest algorithm produces a 128-bit hash.

Because information is lost during the message digest operation, it is a one-way affair: given a hash, you can't reconstruct the original message. Because of the complexity of the digest operation, it is extremely difficult to deliberately create a message that will digest to a particular hash. Changing just one bit anywhere in a message will result in a hash that is utterly unlike the previous one. However, you can confirm that a particular message was likely to have produced a particular hash simply by running the message through the digest algorithm again and comparing the result to the hash.

To create a MAC, follow this general recipe:

  1. Choose a secret key. The key can be any combination of characters of any length. Long keys that don't spell out words or phrases are preferred. Keep the secret key well guarded.
  2. Select the fields that will be used for the MAC. You should include any field that you don't want the user to alter. You can also add consistency-checking fields such as the remote browser's IP address and an expiration date. This helps protect against the information being intercepted en route by some unscrupulous eavesdropper and used later to impersonate the user.
  3. Compute the MAC by concatenating the fields and the secret key and running them through the digest algorithm. You actually need to concatenate the key and run the digest algorithm twice. Otherwise a technically savvy user could take advantage of one of the mathematical properties of the algorithm to append his own data to the end of the fields. Assuming you're using the MD5 algorithm, the formula looks like this:3
$MAC = MD5->hexhash($secret .
      MD5->hexhash(join '', $secret, @fields));

The MAC is now sent to the user along with the other state information.

  1. When the state information is returned by the user, retrieve the various fields and the MAC. Repeat the digest process and compare it to the retrieved MAC. If they match, you know that the user hasn't modified or deleted any of the fields.

Example 5-3 shows the changes needed to add a MAC to the cookie-based hangman system.

use MD5 ();
use constant COOKIE_NAME => 'Hangman3';
use constant SECRET => '0mn1um ex 0vum';

At the top of the script, we add a line to bring in functions from the MD5 package. This module isn't a standard part of Perl, but you can easily obtain it at CPAN. You'll find it easy to compile and install. The only other change we need to make to the top of the script is to add a new constant: the secret key (an obscure Latin phrase with some of the letters replaced with numbers). In this case we hardcode the secret key. You might prefer to read it from a file, caching the information in memory until the file modification date changes.

We now define a function named MAC() whose job is to generate a MAC from the state information and, optionally, to compare the new MAC to the MAC already stored in the state information:

# Check or generate the MAC authentication information
sub MAC {
   my($state, $action) = @_;
   return undef unless ref($state);
   my(@fields) = @{$state}{qw(WORD GUESSES_LEFT GUESSED GAMENO WON TOTAL)};
   my $newmac = MD5->hexhash(SECRET .
                     MD5->hexhash(join '', SECRET, @fields));
   return $state->{MAC} = $newmac if $action eq 'generate';
   return $newmac eq $state->{MAC} if $action eq 'check';
   return undef;
}

MAC() takes two arguments: the $state hash reference and an $action variable that indicates whether we want to generate a new MAC or check an old one. As described in the MAC recipe, we fetch the various fields from $state, concatenate them with the secret key, and then take the MD5 digest. If $action indicates that we are to generate the MAC, we now save the digest into a new state variable field called MAC. If, on the other hand, $action indicates that we are to check the MAC, we compare the new MAC against the contents of this field and return a true value if the old field both exists and is identical to the newly calculated digest. Otherwise we return false.

We now modify get_state() and save_state() to take advantage of the MAC information:

# Retrieve an existing state
sub get_state {
    my %cookie = cookie(COOKIE_NAME);
    return undef unless %cookie;
    authentication_error() unless MAC(\%cookie, 'check');
    return \%cookie;
}

get_state() retrieves the cookie as before, but before returning it to the main part of the program, it passes the cookie to MAC() with an action code of check. If MAC() returns a true result, we return the cookie to the caller. Otherwise, we call a new function, authentication_error(), which displays an error message and exits immediately.

# Save the current state
sub save_state {
   my $state = shift;
   MAC($state, 'generate');  # add MAC to the state
   return CGI::Cookie->new(-name => COOKIE_NAME,
                           -value => $state,
                           -expires => '+1M');
}

Before save_state() turns the state variable into a cookie, it calls MAC() with an action code of generate to add the MAC stamp to the state information. It then calls CGI::Cookie::new() as before in order to create a cookie that contains both the state information and the MAC code. You may notice that we've changed the cookie name from Hangman to Hangman3. This is in order to allow both versions of this script to coexist peacefully on the same server.

The authentication_error() subroutine is called if the MAC check fails:

# Authentication error page
sub authentication_error {
   my $cookie = CGI::Cookie->new(-name => COOKIE_NAME, -expires => '-1d');
   print header(-cookie => $cookie),
         start_html(-title => 'Authentication Error',
                   -bgcolor =>'white'),
         img({-src => sprintf("%s/h%d.gif",ICONS,TRIES),
              -align => 'LEFT'}),
         h1(font({-color => 'red'}, 'Authentication Error')),
         p('This application was unable to confirm the integrity of the',
          'cookie that holds your current score.',
          'Please reload the page to start a fresh session.'),
         p('If the problem persists, contact the webmaster.');
   exit 0;
}

This routine displays a little HTML page advising the user of the problem (Figure 5-3) and exits. Before it does so, however, it sends the user a new empty cookie named Hangman3 with the expiration time set to a negative number. This causes the browser to discard the cookie and effectively clears the session. This is necessary in order to allow the user to continue to play. Otherwise the browser would continue to display this error whenever the user tried to access the page.

Figure 5-3. If the cookie fails to verify, the hangman3 script generates this error page.

If you are following along with the working demo at www.modperl.com, you might want to try quitting your browser, opening up the cookie database file with a text editor, and making some changes to the cookie (try increasing your number of wins by a few notches). When you try to open the hangman script again, the program should bring you up short.

With minor changes, you can easily adapt this technique for use with the hidden field version of the hangman script.

There are a number of ways of calculating MACs; some are more suitable than others for particular applications. For a very good review of MAC functions, see Applied Cryptography, by Bruce Schneir (John Wiley & Sons, 1996). In addition, the Cryptobytes newsletter has published several excellent articles on MAC functions. Back issues are available online at http://www.rsa.com/rsalabs/pubs/cryptobytes/.

Example 5-3. The Cookie-Based Hangman Game with a Message Authentication Check

# file: hangman3.cgi
# hangman game using cookies and a MAC to save state
use IO::File ();
use CGI qw(:standard);
use CGI::Cookie ();
use MD5 ();
use strict;
use constant WORDS => '/usr/games/lib/hangman-words';
use constant ICONS => '/icons/hangman';
use constant TRIES => 6;
use constant COOKIE_NAME => 'Hangman3';
use constant SECRET => '0mn1um ex 0vum';
. . . everything in the middle remains the same . . .
# Check or generate the MAC authentication information
sub MAC {
   my($state, $action) = @_;
   return undef unless ref($state);
   my(@fields) = @{$state}{qw(WORD GUESSES_LEFT GUESSED GAMENO WON TOTAL)}; 
    my($newmac) = MD5->hexhash(SECRET .
                              MD5->hexhash(join '', SECRET, @fields));
   return $newmac eq $state->{MAC} if $action eq 'check';
   return $state->{MAC} = $newmac if $action eq 'generate';
   undef;
}
# Retrieve an existing state
sub get_state {
   my %cookie = cookie(COOKIE_NAME);
   return undef unless %cookie;
   authentication_error() unless MAC(\%cookie, 'check');
   return \%cookie;
}
# Save the current state
sub save_state {
   my $state = shift;
   MAC($state, 'generate');  # add MAC to the state
   return CGI::Cookie->new(-name => COOKIE_NAME,
                           -value => $state,
                           -expires => '+1M');
}
# Authentication error page
sub authentication_error {
   my $cookie = CGI::Cookie->new(-name => COOKIE_NAME, -expires => '-1d');
   print header(-cookie => $cookie),
         start_html(-title => 'Authentication Error',
                   -bgcolor =>'#f5deb3'),
         img({-src => sprintf("%s/h%d.gif", ICONS, TRIES),
              -align => 'LEFT'}),
         h1(font({-color => 'red'}, 'Authentication Error')),
         p('This application was unable to confirm the integrity of the',
          'cookie that holds your current score.',
          'Please reload the page to start a fresh session.'),
         p('If the problem persists, contact the webmaster.');
   exit 0;
}

   Show Contents   Go to Top   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.