initial commit
authorDaniel Black <Daniel Black@3d6f21ca-528f-11de-a2b4-010000000000>
Sat, 6 Jun 2009 14:51:18 +0000 (14:51 +0000)
committerDaniel Black <Daniel Black@3d6f21ca-528f-11de-a2b4-010000000000>
Sat, 6 Jun 2009 14:51:18 +0000 (14:51 +0000)
git-svn-id: http://svn.cacert.cl/certificate_authentication@1 3d6f21ca-528f-11de-a2b4-010000000000

certificate_authentication.php [new file with mode: 0644]

diff --git a/certificate_authentication.php b/certificate_authentication.php
new file mode 100644 (file)
index 0000000..8322598
--- /dev/null
@@ -0,0 +1,234 @@
+<?php
+
+/**
+  Author: Daniel Black (daniel@cacert.org)
+
+ This class uses client side certificates to authenticate a user.
+
+ Client certificate information will be used to determine the  
+ username. The SSL_CLIENT_S_DN_EMAIL or subsequent email addresses are used 
+ as the email address. If these are not present then the SSL_CLIENT_S_DN is 
+ parsed. If these are not present the subjectAltName of the certificate
+ is used. For an email to be accepted it must pass the webserver's 
+ validation protocol. The email address selected must match the 
+ $rcmail_config['mail_domain'].
+
+ Email the selected email address is first checked against virtuser_query and
+ virtuser_file. If found the username is the value from the file/query. 
+ Otherwise the username is the local part of the email address.
+
+ Finally the $rcmail_config['client_cert_username'] can be used to perform
+ a final manipulation on the username.
+
+ Used configuration options:
+    * $rcmail_config['mail_domain']
+    * $rcmail_config['virtuser_file']
+
+ New Configuration options:
+    // As no password is provided in client certificate authentication, the  
+    // 'client_cert_password' here is used to access all accounts. The IMAP and 
+    // SMTP server (if smtp_pass has %p), also needs to accept this password. 
+    // 'client_cert_password' cannot be empty. 
+    $rcmail_config['client_cert_password'] = 'thebigscarypassword'; 
+     
+    // This is the format from the email in the client certificate that is used 
+    // as the username, %fu => full email, %u => username, %d => domain. 
+    // Defaults to %u 
+    $rcmail_config['client_cert_username'] = null; 
+
+ Apache configuration:
+ Full instructions on how to get Apache using client side
+ certificate verification. http://httpd.apache.org/docs/trunk/mod/mod_ssl.html
+ The basic configuration, in additional to your https server config, is:
+        SSLVerifyClient optional
+        SSLVerifyDepth 3
+        SSLCACertificatePath /usr/share/ca-certificates/cacert.org/
+        SSLCADNRequestPath /usr/share/ca-certificates/cacert.org/
+        SSLOptions +StdEnvVars
+
+ If subjectAltName verification is required then SSLOptions +ExportCertData
+ is also needed. subjectAltName validation is dependent on an unstable PHP
+ API http://www.php.net/openssl_x509_parse. Test well before use.
+
+ If SSL is the only verification then make SSLVerifyClient 'require'.
+
+ You can make Apache enforce some requirements with SSLRequire however note
+ that the documentation at this time (20090417) says its not threadsafe.
+
+ IMAP configuration:
+
+ The IMAP server must be configured to accept $rcmail_config['client_cert_password']
+ as a valid authentication for all users.
+
+ Dovecot example:
+  # /etc/dovecot/dovecot.conf
+  passdb pam {
+   args = session=yes mail
+  }
+  passdb sql {
+    args = /etc/dovecot/dovecot-sql-masterpassword-webmail.conf
+  }
+
+  # dovecot-sql-masterpassword-webmail.conf
+  driver = mysql
+  connect = host=localhost dbname=cacertusers user=nss-user password=connection-pass
+  password_query = SELECT username AS user, '{plain}bigscarymasterpassword' AS password \
+                        FROM nssuser WHERE username = '%u'
+  #password_query = SELECT username AS user, '{plain}bigscarymasterpassword' AS password, \
+  #                    '172.16.2.20' AS allow_nets FROM nssuser WHERE username = '%u'
+  user_query = SELECT homedir AS home, uid, gid FROM nssuser WHERE username = '%u'
+
+  Notes:
+    * if using allow_nets above and Postfix, note that postfix does not pass source IP
+      to the dovecot authentication daemon when authenticating sending ('smtp_pass') 
+    * If certificate only authentication then passdb pam can be removed.
+    * Having two authentication mechanisms will cause dovecot to log an error, 
+      'pam_authenticate() failed: Authentication failure' before accepting the SQL
+      authentication.
+ */
+
+
+class certificate_authentication extends rcube_plugin
+{
+
+  function init()
+  {
+    $this->add_hook('startup', array($this, 'startup'));
+    $this->add_hook('authenticate', array($this, 'authenticate'));
+    $this->add_hook('outgoing_message_headers', array($this, 'outgoing_message_headers'));
+    $this->add_texts('localization/', FALSE);
+  }
+
+  function startup($args)
+  {
+    $rcmail = rcmail::get_instance();
+    $cfg = $rcmail->config;
+    $pass = $cfg->get('client_cert_password');
+
+    if (empty($pass)) {
+       $rcmail->output->command('display_message', $this->gettext('client_cert_password not set - certificate logon disabled'), 'error');
+    }
+    if ($args['task'] == 'mail' && empty($args['action']) && empty($_SESSION['user_id'])
+      && !empty($pass) 
+      && $this->get_username($cfg)) {
+
+      // change action to login
+      $args['action'] = 'login';
+    }
+
+    return $args;
+  }
+
+  function authenticate($args)
+  {
+    $rcmail = rcmail::get_instance();
+    $cfg = $rcmail->config;
+
+    if ($user=$this->get_username($cfg)) {
+      $args['user'] = $user;
+      $args['pass'] = $cfg->get('client_cert_password');
+    }
+    return $args;
+  }
+
+  function get_username($cfg)
+  {
+    if ( $_SERVER['SSL_CLIENT_VERIFY'] != 'SUCCESS') 
+      return FALSE;
+
+    $c_mail_domain = $cfg->get('mail_domain'); 
+    // We need to search for emails in the certificate that match the mail_domain 
+    if (is_array($c_mail_domain)) { 
+      $mail_domains = array_values($c_mail_domain); 
+    } else { 
+      $mail_domains = array($c_mail_domain); 
+    } 
+    $email = $_SERVER['SSL_CLIENT_S_DN_EMAIL']; 
+
+    $username = $this->validate_email($cfg,$email,$mail_domains);
+
+    if (empty($username)) { 
+      $d=0;
+      while ($email=$_SERVER["SSL_CLIENT_S_DN_EMAIL_$d"]) {
+        if ($username = $this->validate_email($cfg,$email,$mail_domains)) {
+          break;
+        }
+        ++$d;
+      }
+    }
+  
+    if (empty($username)) { 
+      $dn=$_SERVER['SSL_CLIENT_S_DN']; 
+      # match on the following DN
+      # emailAddress= (current cacert issued ones 20090402) https://bugs.cacert.org/view.php?id=672
+
+      if (preg_match_all('/\/emailAddress=([^\/]*)/',$dn,$reg,PREG_SET_ORDER)) { 
+        foreach ($reg as $emailarr) { 
+          $email = $emailarr[1]; 
+          if ($username = $this->validate_email($cfg,$email,$mail_domains)) {
+            break;
+          }
+        } 
+      } 
+    }
+
+    if (empty($username) && $_SERVER['SSL_CLIENT_CERT']) {
+      # subjectAltName unpresented by Apache http://httpd.apache.org/docs/trunk/mod/mod_ssl.html
+      # subjectAltName http://tools.ietf.org/html/rfc5280#section-4.2.1.6
+      # WARNING WARNING openssl_x509_parse is an unstable PHP API
+      $x509 = openssl_x509_parse($_SERVER['SSL_CLIENT_CERT']);
+      $subjectAltName = $x509['extensions']['subjectAltName']; // going off https://foaf.me/testSSL.php
+      #print_r(split("[, ]",$subjectAltName));
+      #print_r($x509);
+      #echo $subjectAltName;
+      if (preg_match_all('/email:([^, ]*)/',$subjectAltName,$reg,PREG_SET_ORDER)) {
+        foreach ($reg as $emailarr) { 
+          $email = $emailarr[1]; 
+          #echo $email;
+          if ($username = $this->validate_email($cfg,$email,$mail_domains)) {
+            break;
+          }
+        } 
+      }
+    } 
+    
+    if (!empty($username)) { 
+      $client_cert_username = $cfg->get('client_cert_username'); 
+      if (!empty($client_cert_username)) { 
+        $username = str_replace(array("%fu", "%u", "%d"), 
+                                array($email,$username['user'],$username['dom']), 
+                                $client_cert_username); 
+      } 
+      return $username['user'];
+    }
+    return FALSE;
+  }
+
+  function validate_email($cfg,$email,$mail_domains)
+  {
+      list($username, $domain) = explode('@',$email); 
+
+      if (!empty($domain) && in_array($domain,$mail_domains)) { 
+        if ($vusername = rcube_user::email2user($email)) 
+          $username = $vusername; 
+        return array( 'user' => $username, 'dom' => $domain);
+      } else { 
+        return FALSE;
+      } 
+  }
+
+  function outgoing_message_headers($args)
+  {
+    if (isset($_SERVER['SSL_CLIENT_S_DN'])) {
+      $header = ' with certificate (' . $_SERVER['SSL_CLIENT_S_DN'] 
+       . (isset($_SERVER['SSL_CLIENT_M_SERIAL']) ? ',serial='.$_SERVER['SSL_CLIENT_M_SERIAL'].')' : ',noserial)')
+       . (isset($_SERVER['SSL_CLIENT_I_DN']) ? ',issuer=(' . $_SERVER['SSL_CLIENT_I_DN'] . ')' : ',noissuer');
+      $args['headers']['Received'] = substr_replace($args['headers']['Received'], $header,strpos($args['headers']['Received'],';'),0);
+    }
+    return $args;
+  }
+
+}
+