Difference between revisions of "Secure login"

From EPrints Documentation
Jump to: navigation, search
(Receive data script)
 
(18 intermediate revisions by 2 users not shown)
Line 1: Line 1:
When logging into eprints, both username and password travel openly. Usually it is not a problem as eprints has its own authenticating mechanism, and stealing an eprints password gives the intruder very limited power. However when using centralized authentication (e.g., by [[LDAP]]) then protecting the password is more important. To prevent sniffing login pages usually use secure connection. Here we discuss an easy way to set up eprints to use encoded password traffic.
+
When logging into eprints, both username and password travel openly. Usually it is not a problem as eprints has its own authenticating mechanism, and stealing an eprints password gives the intruder very limited power. However when using centralized authentication (e.g., by [[LDAP]]) then protecting the password is more important. To prevent sniffing, login pages usually use secure connection. Here we discuss an easy way to set up eprints to use encoded password traffic.
  
 
__TOC__
 
__TOC__
Line 8: Line 8:
  
 
===Requirements===
 
===Requirements===
You need a ''secure http server'' on the same main domain as your your eprints server. For example, if eprints can be reached on the web address ''<nowiki>http://eprints.umb.edu</nowiki>'', then the secure server must have domain name ending with ''umb.edu'', for example ''<nowiki>https://secure.umb.edu</nowiki>.
+
You need a ''secure http server'' on the same main domain as your eprints server. For example, if eprints can be reached on the web address ''<nowiki>http://eprints.umb.edu</nowiki>'', then the secure server must have domain name ending with ''umb.edu'', for example ''<nowiki>https://secure.umb.edu</nowiki>.
  
 
You should set up a cgi executable directory on the secure host. The apache config extract below does exactly this so that ''<nowiki>https://secure.umb.edu/eprints/whatever</nowiki>'' requests the script ''/opt/cgi-bin/eprints/whatever'' to be executed:
 
You should set up a cgi executable directory on the secure host. The apache config extract below does exactly this so that ''<nowiki>https://secure.umb.edu/eprints/whatever</nowiki>'' requests the script ''/opt/cgi-bin/eprints/whatever'' to be executed:
Line 30: Line 30:
 
The login process now works as follows.
 
The login process now works as follows.
 
# when a page requires authentication, it creates a "login page" with destination at the secure web site
 
# when a page requires authentication, it creates a "login page" with destination at the secure web site
# the "login page" is sent encrypted to the secure site
+
# data entered to the "login page" is sent encrypted to the secure site
# the secure site encrypts the payload, and redirects the browser to an unencrypted eprints page ''receive_data''
+
# the secure site encrypts the payload, and redirects the browser to the unencrypted eprints page ''receive_data''
 
# ''receive_data'' decrypts the payload, checks credentials, logs in the user if everything checks, and then redirects the browser to the originating page.
 
# ''receive_data'' decrypts the payload, checks credentials, logs in the user if everything checks, and then redirects the browser to the originating page.
  
What is lost in this process of two redirects is the error message when authentication fails. ''Receive_data'' cannot display the error message as its URL should not be displayed on the user's screen. The final page - which is the original one as well - cannot distinguish between an unsuccessful login and a reload, thus cannot display the error message as well. ''C'est la vie''.
+
What is lost in this process of two redirects is the error message when authentication fails. ''Receive_data'' cannot display the error message as its URL should not be displayed on the user's screen. The final page - which is the original one as well - cannot distinguish between an unsuccessful login and a reload, thus cannot display the error message as well. ''[http://en.wikipedia.org/wiki/C'est_la_vie C'est la vie]''.
  
The encrypted data the ''receive_data'' page receives can be a replay of a sniffed older communication. Thus this data should contain a timestamp and checked to be recent.
+
The encrypted data the ''receive_data'' page receives can be a replay of a sniffed older communication. Thus this data should contain a timestamp which should be checked to be recent. It also means that '''the secure and eprint servers must synchronize their clocks'''.
  
The secure site can also check the credentials, and relay the result only. Interestingly, the result should be sent encrypted, as otherwise the secure site could be used as an oracle to tell whether the password is correct or not (ideal for a [http://en.wikipedia.org/wiki/Dictionary_attack dictionary attack]).  
+
The secure site can also check the credentials, and relay the result only. Interestingly, the result should be sent encrypted, as otherwise the secure site could be used as an [http://en.wikipedia.org/wiki/Oracle oracle] to tell whether the password is correct or not (ideal utility for a [http://en.wikipedia.org/wiki/Dictionary_attack dictionary attack]).
  
 
==Secure login script==
 
==Secure login script==
 
This script receives the login page over a secure connection. It encrypts the username, password, and other arguments, and relocates the page to eprints' ''receive_data'' page.
 
This script receives the login page over a secure connection. It encrypts the username, password, and other arguments, and relocates the page to eprints' ''receive_data'' page.
  
Using the secure site configuration above, this script should be copied to ''/opt/cgi-bin/eprints/login'' after changing the address to your own eprint server.
+
Using the secure site configuration above, this script should be copied to ''/opt/cgi-bin/eprints/login'' after changing the address to your own eprint server. And, of course, the script must be executable by the apache process.
  
 
  ##!/usr/bin/perl -w
 
  ##!/usr/bin/perl -w
Line 53: Line 53:
 
  sub _RelocateTo {
 
  sub _RelocateTo {
 
     my $arg=shift;
 
     my $arg=shift;
 +
    ## CHANGE THIS ADDRESS TO YOUR EPRINT SERRVER'S
 
     "<nowiki>http://eprints.umb.edu/cgi/receive_data?Q=$arg</nowiki>";
 
     "<nowiki>http://eprints.umb.edu/cgi/receive_data?Q=$arg</nowiki>";
 
  }
 
  }
  <-- COPY THE CRYPTO ROUTINES HERE -->
+
  <nowiki><-- COPY THE CRYPTO ROUTINES HERE --></nowiki>
 
  sub _html2ascii {
 
  sub _html2ascii {
 
     my $v=shift;
 
     my $v=shift;
Line 131: Line 132:
 
       $list[$i] =~ s/%c/:/g; $list[$i] =~ s/%a/%/g;
 
       $list[$i] =~ s/%c/:/g; $list[$i] =~ s/%a/%/g;
 
     }
 
     }
     $OK=0 if( $list[4] !~ /^\d$/ );
+
     $OK=0 if( $list[4] !~ /^\d+$/ );
 
     if($OK){
 
     if($OK){
 
       my $timediff = int(time/60) - $list[4];
 
       my $timediff = int(time/60) - $list[4];
Line 157: Line 158:
 
  exit 0;
 
  exit 0;
 
  <nowiki><!-- COPY CRYPTO ROUTINES HERE --></nowiki>
 
  <nowiki><!-- COPY CRYPTO ROUTINES HERE --></nowiki>
 +
While we made a lot of effort to protect the integrity and secrecy of the username and the data, nothing prevents a bad guy to submit bogus data to our secure script! Thus we  relocate to the eprints repository addresses only, and accept the "relocate" argument only if it starts with a slash.
  
 
==Crypto routines==
 
==Crypto routines==
 
These routines perform encryption and decryption. It is a lightweight version of more sophisticated ones, but (at least I hope) it is sufficient for this application. After the source there are some remarks for those interested.
 
These routines perform encryption and decryption. It is a lightweight version of more sophisticated ones, but (at least I hope) it is sufficient for this application. After the source there are some remarks for those interested.
  
<div style="padding:4px; background-color: #ffe0e0;border:1px solid #a06060;">'''Warning!''' The secret phrase in the second line should be changed to some secret value.</div>
+
<div style="padding:4px; background-color: #ffe0e0;border:1px solid #a06060;">'''Warning!''' The secret phrase in the second line should be changed to some secret value. And it should be the same in both copies.</div>
 
  sub _secret {  
 
  sub _secret {  
 
   "Whatever is your secret phrase, give anything here";  
 
   "Whatever is your secret phrase, give anything here";  
Line 236: Line 238:
 
*Both encoding and decoding uses MD5 as the building block. The first eight characters of the ciphertext (encoded string) is used for two purposes. First, it acts as a salt, namely it is added to the secret phrase to derive the seed of the random number generator; second it is used to check message integrity, namely it should match the MD5 hash of the message ''and'' the secret phrase.
 
*Both encoding and decoding uses MD5 as the building block. The first eight characters of the ciphertext (encoded string) is used for two purposes. First, it acts as a salt, namely it is added to the secret phrase to derive the seed of the random number generator; second it is used to check message integrity, namely it should match the MD5 hash of the message ''and'' the secret phrase.
 
*Messages to be encrypted (plaintext) should not end by a space (spaces are used to pad the plaintext to have length divisible by three)
 
*Messages to be encrypted (plaintext) should not end by a space (spaces are used to pad the plaintext to have length divisible by three)
*The encryption is deterministic. Such a scheme is insecure in general. In our case, however, the plaintext contains a timestamp, thus - supposedly - the same message is not encrypted twice.
+
*The encryption is deterministic. Such a scheme is insecure in general. In our case, however, the plaintext contains a timestamp, thus - supposedly - the same message is never encrypted twice.
 
*Weak points of the scheme: only 32 bit MAC (and seed) value, and relying on perl's pseudorandom function.
 
*Weak points of the scheme: only 32 bit MAC (and seed) value, and relying on perl's pseudorandom function.
  
 
==Patching Apache::Login==
 
==Patching Apache::Login==
 +
We are making two changes to the ''perl_lib/EPrints/Apache/Login.pm'' module. First, always store the URI of the referring page in the hidden ''target'' variable. Second, set the address of the login page to the value of the configuration variable ''external_login_address'' whenever it is defined.
  
 +
The instructions below are for Eprints version 3.1.3. Locate the line
 +
my $form = $session->render_form( "POST" );
 +
in the above module (it should be line 122). Replace it by the following:
 +
##CSL Always use the "target" variable, and
 +
## redirect to "external_login_address"
 +
my $target = $session->param( "target" );
 +
if(! defined($target) ){
 +
      $target = $session->get_uri;
 +
}
 +
my $form = $session->render_form(
 +
  "POST",
 +
  $session->get_repository->get_conf("external_login_address") );
 +
If ''external_login_address'' is not defined, it has the same effect as the old code.
 +
Roll down to line 142 and comment out this line (add # signs at the beginning):
 +
## my $target = $session->param( "target" );
 +
Finally save the file.
 +
 +
==Define "external_login_address"==
 +
Add the definition of this variable to the file [[archives/ARCHIVEID/cfg/cfg.d/]][[user_login.pl]]:
 +
  $c->{external_login_address} = "<nowiki>https://secure.umb.edu/eprints/login</nowiki>";
 +
In the same file you can define the sub ''check_user_password'' as explained in [[LDAP]] and [[LDAP user_login.pl]].
  
[[Category:Howto]]
+
[[Category:Authentication]]

Latest revision as of 13:03, 20 March 2010

When logging into eprints, both username and password travel openly. Usually it is not a problem as eprints has its own authenticating mechanism, and stealing an eprints password gives the intruder very limited power. However when using centralized authentication (e.g., by LDAP) then protecting the password is more important. To prevent sniffing, login pages usually use secure connection. Here we discuss an easy way to set up eprints to use encoded password traffic.

The idea

Change the destination of the eprints login page to a secure host. On the secure host a script receives the data: login name and password (and maybe others), encodes it, and forwards the request with the encrypted data as payload to an unencrypted eprints page. The eprints page decrypts the data and uses it for authentication.

Requirements

You need a secure http server on the same main domain as your eprints server. For example, if eprints can be reached on the web address http://eprints.umb.edu, then the secure server must have domain name ending with umb.edu, for example https://secure.umb.edu.

You should set up a cgi executable directory on the secure host. The apache config extract below does exactly this so that https://secure.umb.edu/eprints/whatever requests the script /opt/cgi-bin/eprints/whatever to be executed:

<VirtualHost *:443>
 DocumentRoot "/opt/secure"
 SSLEngine on
 ScriptAlias /eprints/  /opt/cgi-bin/eprints/
 <Directory "/opt/cgi-bin/eprints">
     AllowOverride None
     Options +ExecCGI -MultiView +SymLinksIfOwnerMatch
     Order allow,deny
     Allow from all
 </Directory>
</VirtualHost>

Copy the Secure login script below into the /opt/cgi-bin/eprints/ directory (or whatever directory you've set up above); copy the receive data script into eprints' /cgi/ directory.

Finally patch the Apache::Login module of eprints so that everything works smoothly.

Caveats

The login process now works as follows.

  1. when a page requires authentication, it creates a "login page" with destination at the secure web site
  2. data entered to the "login page" is sent encrypted to the secure site
  3. the secure site encrypts the payload, and redirects the browser to the unencrypted eprints page receive_data
  4. receive_data decrypts the payload, checks credentials, logs in the user if everything checks, and then redirects the browser to the originating page.

What is lost in this process of two redirects is the error message when authentication fails. Receive_data cannot display the error message as its URL should not be displayed on the user's screen. The final page - which is the original one as well - cannot distinguish between an unsuccessful login and a reload, thus cannot display the error message as well. C'est la vie.

The encrypted data the receive_data page receives can be a replay of a sniffed older communication. Thus this data should contain a timestamp which should be checked to be recent. It also means that the secure and eprint servers must synchronize their clocks.

The secure site can also check the credentials, and relay the result only. Interestingly, the result should be sent encrypted, as otherwise the secure site could be used as an oracle to tell whether the password is correct or not (ideal utility for a dictionary attack).

Secure login script

This script receives the login page over a secure connection. It encrypts the username, password, and other arguments, and relocates the page to eprints' receive_data page.

Using the secure site configuration above, this script should be copied to /opt/cgi-bin/eprints/login after changing the address to your own eprint server. And, of course, the script must be executable by the apache process.

##!/usr/bin/perl -w
## secure login script for eprints
## encrypt and automatically forward to /eprints
## Author: Laszlo Csirmaz, 2009
## You are free to use this script
use strict;
sub _RelocateTo {
   my $arg=shift;
   ## CHANGE THIS ADDRESS TO YOUR EPRINT SERRVER'S
   "http://eprints.umb.edu/cgi/receive_data?Q=$arg";
}
<-- COPY THE CRYPTO ROUTINES HERE -->
sub _html2ascii {
   my $v=shift;
   $v="" if(! defined $v);
   $v=~ tr/+/ /;
   $v=~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
   return $v;
}
my $P={};
my $ct=$ENV{'CONTENT_TYPE');
if($ct && $ct =~ /multipart/i ){
   use CGI qw( :cgi :form );
   my $req=new CGI; # read all data in
   my @pm=$req->param;
   foreach my $key (@pm) {
     $P->{$key} = _html2ascii($req->param($ley));
   } 
} elsif ( $ENV{'REQUEST_METHOD'} !~ /^get/i ) {
   my $pm=""; while(<>){ $pm .= $_; }
   my @pms=split(/&/,$pm);
   foreach my $elem(@pms) {
      my($t,$v)=split(/=/,$elem);
      next if(! defined($t) );
      $P->{_html2ascii($t)} = _html2ascii($v);
   }
}
## we return the following arguments:
##  target, loginparams, username, pw, timestamp
my $arg="";
for my $item ($P->{target},$P->{loginparams},
     $P->{login_username}, $P->{login_password} ){
   $item="" if(! $item)[
   # replace '%' by '%a' and replace ':' by '%c'
   $item =~ s/%/%a/g; $item =~ s/:/%c/g;
   $arg .= "$item:";
}
my $addr = _RelocateTo( _EncodeValue( $arg . int(time/60) ) );
print "Content-Type: text/html; charset=utf-8\n",
   "Location: $addr\n",
   "\n",
   "<html><head><title>Redirected</title></head>\n",
   "<body>\n",
   "This page should automatically relocated. If not,\n",
   "please <a href=\"$addr\">click here</a>.\n",
   "</body>\n",
   "</html>\n",
   "\n";
exit 0;

Receive data script

This script receives the encrypted data from the secure server, decrypts it and logs in the user. A 1 second delay is inserted if there is a problem with the credentials.

This script should be copied to the eprints/cgi/receive_data. It calls the check_user_password configuration procedure if it is defined.

######################################################
# receive_data
# Part of EPrints 3.1
# Author: Laszlo Csirmaz
# You are free to use/modify this script
######################################################
use EPrints;
use strict;
my $session = new EPrints::Session;
exit( 0 ) unless( defined $session );
my $redirect_to="/cgi/users/home";
my $loginparams="";
my $OK=1;
my $arg = $session->param('Q');
if( $arg && length($arg) > 10 ){ #sanity check
   $arg = _DecodeValue($arg);
   ## it is target:loginparams:uname:pw:timenow
   $arg = "/cgi/users/home::::" if(!$arg);
   my @list = split(/:/,$arg);
   for my $i(0..4){
      $list[$i]="" if(!$list[$i]);
      $list[$i] =~ s/%c/:/g; $list[$i] =~ s/%a/%/g;
   }
   $OK=0 if( $list[4] !~ /^\d+$/ );
   if($OK){
      my $timediff = int(time/60) - $list[4];
      #allow a 2 minute window
      $OK=0 if( $timediff<-1 || $timediff > 2 );
   }
   $redirect_to = $list[0] if($list[0] =~ m#/# );
   $loginparams = $list[1];
   if($OK){
      if( $session->valid_login($list[2],$list[3] ) ){
         my $user = EPrints::DataObj::User::user_with_username(
               $session,$list[2]);
         $session->login($user);
         $OK=2;
      }
   }
}
sleep 1 if( $OK != 2 ); # some problem, don't hurry
my $host = $session->{repository}->get_conf( "host" );
my $port = $session->{repository}->get_conf( "port" );
$redirect_to = "http://$host".($port!=80 :":$port":""). $redirect_to;
$redirect_to .= "?$loginparams" if($loginparams);
$session->redirect( $redirect_to );
$session->terminate;
exit 0;
<!-- COPY CRYPTO ROUTINES HERE -->

While we made a lot of effort to protect the integrity and secrecy of the username and the data, nothing prevents a bad guy to submit bogus data to our secure script! Thus we relocate to the eprints repository addresses only, and accept the "relocate" argument only if it starts with a slash.

Crypto routines

These routines perform encryption and decryption. It is a lightweight version of more sophisticated ones, but (at least I hope) it is sufficient for this application. After the source there are some remarks for those interested.

Warning! The secret phrase in the second line should be changed to some secret value. And it should be the same in both copies.
sub _secret { 
  "Whatever is your secret phrase, give anything here"; 
}
sub _intto64 { #a MIME64 type encoding
   my $c=shift;
   $c &=0x3F;
   if($c<10){ return chr(48+$c); }
   if($c<10+26){ return chr($c-10+65); }
   if($c<10+26+26){ return chr($c-10-26+97); }
   if($c==62){ return '_'; }
   return '-';
}
sub _encode64 { #encoding three bytes to four chars
   my($a,$b,$c)=@_;
   $a &= 0xFF; $b &= 0xFF; $c &= 0xFF;
   return _intto64($a>>2)._intto64(($a<<4)|($b>>4)).
      _intto64(($b<<2)|($c>>6))._intto64($c);
}
sub _from64 { #decoding of the above
   my $c=ord(shift||"0");
   if($c==95){ return 62;}
   if($c==45){ return 63;}
   if($c<48+10){ return $c-48; }
   if($c<65+26){ return $c-65+10; }
   return $c-97+10+26;
}
sub _decode64 { # decoding four chars to three bytes
   my($s)=shift;
   my ($r1,$r2,$r3,$r4)=(
      _from64($s),_from64(substr($s,1)),
      _from64(substr($s,2)),_from64(substr($s,3)));
   return ($r1<<18)|($r2<<12)|($r3<<6)|$r4;
}
sub _EncodeValue { ## encoded value should not end in a space
   my($id) = @_;
   use Digest::MD5;
   my $salt=substr(Digest::MD5::md5_hex($id, _secret()),0,8);
   my $sstr=Digest::MD5::md5($salt, _secret());
   my $seed=0; my $ssid="";
   for(my $i=0;$i<8;$i++){$seed=($seed<<8)+ord(substr($sstr,$i));}
   srand($seed); my $idlen=length($id);
   $id .= "  "; # pad message by spaces
   for(my $i=0;$i<$idlen;$i+=3){
      #encode $id[0],$id[1],$id[2] as 4 chars
      my $v=rand(0x1000000);
      $ssid .= _encode64(ord(substr($id,$i))^($v>>16),
                         ord(substr($id,$i+1))^($v>>8),
                         ord(substr($id,$i+2))^$v);
   }
   return $salt.$ssid;
}
sub _DecodeValue {
   my($s)=@_;
   use Digest::MD5;
   my($salt,$ssid)=(substr($s,0,8),substr($s,8));
   my $sstr=Digest::MD5::md5($salt, _secret());
   my $seed=0;
   for(my $i=0;$i<8;$i++){$seed=($seed<<8)+ord(substr($sstr,$i));}
   srand($seed);
   my $id="";
   for(my $i=0;$i<length($ssid)-3;$i+=4){
       #decode next four chars
       my $v=rand(0x1000000)^_decode64(substr($ssid,$i));
       $id .= chr($v>>16) . chr(0xff&($v>>8)) . chr(0xff&$v);
   }
   $id =~ s/ +$//; ## strip spaces at the end
   # $salt and $chksum should be equal
   $id="" if ($salt ne substr(Digest::MD5::md5_hex($id, _secret()),0,8));
   return $id;
}

Remarks:

  • The MIME64-like encoding uses letters (both lower and upper case), digits and the characters - (minus) and _ (underscore). These are safe to use in an URL.
  • Both encoding and decoding uses MD5 as the building block. The first eight characters of the ciphertext (encoded string) is used for two purposes. First, it acts as a salt, namely it is added to the secret phrase to derive the seed of the random number generator; second it is used to check message integrity, namely it should match the MD5 hash of the message and the secret phrase.
  • Messages to be encrypted (plaintext) should not end by a space (spaces are used to pad the plaintext to have length divisible by three)
  • The encryption is deterministic. Such a scheme is insecure in general. In our case, however, the plaintext contains a timestamp, thus - supposedly - the same message is never encrypted twice.
  • Weak points of the scheme: only 32 bit MAC (and seed) value, and relying on perl's pseudorandom function.

Patching Apache::Login

We are making two changes to the perl_lib/EPrints/Apache/Login.pm module. First, always store the URI of the referring page in the hidden target variable. Second, set the address of the login page to the value of the configuration variable external_login_address whenever it is defined.

The instructions below are for Eprints version 3.1.3. Locate the line

my $form = $session->render_form( "POST" );

in the above module (it should be line 122). Replace it by the following:

##CSL Always use the "target" variable, and
## redirect to "external_login_address"
my $target = $session->param( "target" );
if(! defined($target) ){
      $target = $session->get_uri;
}
my $form = $session->render_form(
  "POST",
  $session->get_repository->get_conf("external_login_address") );

If external_login_address is not defined, it has the same effect as the old code. Roll down to line 142 and comment out this line (add # signs at the beginning):

## my $target = $session->param( "target" );

Finally save the file.

Define "external_login_address"

Add the definition of this variable to the file archives/ARCHIVEID/cfg/cfg.d/user_login.pl:

 $c->{external_login_address} = "https://secure.umb.edu/eprints/login";

In the same file you can define the sub check_user_password as explained in LDAP and LDAP user_login.pl.