Ticket #1485224: certificatelogon.php

File certificatelogon.php, 8.3 KB (added by dan, 5 years ago)

fixed to account for the r2384 API (#1485801) (once it gets ported to the devel-api svn report)

Line 
1<?php
2
3/**
4  Author: Daniel Black (daniel@cacert.org)
5
6 This class uses client side certificates to authenticate a user.
7
8 Client certificate information will be used to determine the 
9 username. The SSL_CLIENT_S_DN_EMAIL or subsequent email addresses are used
10 as the email address. If these are not present then the SSL_CLIENT_S_DN is
11 parsed. If these are not present the subjectAltName of the certificate
12 is used. For an email to be accepted it must pass the webserver's
13 validation protocol. The email address selected must match the
14 $rcmail_config['mail_domain'].
15
16 Email the selected email address is first checked against virtuser_query and
17 virtuser_file. If found the username is the value from the file/query.
18 Otherwise the username is the local part of the email address.
19
20 Finally the $rcmail_config['client_cert_username'] can be used to perform
21 a final manipulation on the username.
22
23 Used configuration options:
24    * $rcmail_config['mail_domain']
25    * $rcmail_config['virtuser_file']
26
27 New Configuration options:
28    // As no password is provided in client certificate authentication, the 
29    // 'client_cert_password' here is used to access all accounts. The IMAP and
30    // SMTP server (if smtp_pass has %p), also needs to accept this password.
31    // 'client_cert_password' cannot be empty.
32    $rcmail_config['client_cert_password'] = 'thebigscarypassword';
33     
34    // This is the format from the email in the client certificate that is used
35    // as the username, %fu => full email, %u => username, %d => domain.
36    // Defaults to %u
37    $rcmail_config['client_cert_username'] = null;
38 
39
40 Apache configuration:
41 Full instructions on how to get Apache using client side
42 certificate verification. http://httpd.apache.org/docs/trunk/mod/mod_ssl.html
43 The basic configuration, in additional to your https server config, is:
44        SSLVerifyClient optional
45        SSLVerifyDepth 3
46        SSLCACertificatePath /usr/share/ca-certificates/cacert.org/
47        SSLCADNRequestPath /usr/share/ca-certificates/cacert.org/
48        SSLOptions +StdEnvVars
49
50 If subjectAltName verification is required then SSLOptions +ExportCertData
51 is also needed. subjectAltName validation is dependent on an unstable PHP
52 API http://www.php.net/openssl_x509_parse. Test well before use.
53
54 If SSL is the only verification then make SSLVerifyClient 'require'.
55
56 You can make Apache enforce some requirements with SSLRequire however note
57 that the documentation at this time (20090417) says its not threadsafe.
58
59 IMAP configuration:
60
61 The IMAP server must be configured to accept $rcmail_config['client_cert_password']
62 as a valid authentication for all users.
63
64 Dovecot example:
65  # /etc/dovecot/dovecot.conf
66  passdb pam {
67   args = session=yes mail
68  }
69  passdb sql {
70    args = /etc/dovecot/dovecot-sql-masterpassword-webmail.conf
71  }
72
73  # dovecot-sql-masterpassword-webmail.conf
74  driver = mysql
75  connect = host=localhost dbname=cacertusers user=nss-user password=connection-pass
76  password_query = SELECT username AS user, '{plain}bigscarymasterpassword' AS password \
77                        FROM nssuser WHERE username = '%u'
78  #password_query = SELECT username AS user, '{plain}bigscarymasterpassword' AS password, \
79                       '172.16.2.20' AS allow_nets FROM nssuser WHERE username = '%u'
80  user_query = SELECT homedir AS home, uid, gid FROM nssuser WHERE username = '%u'
81
82  Notes:
83    * if using allow_nets above and Postfix, note that postfix does not pass source IP
84      to the dovecot authentication daemon when authenticating sending ('smtp_pass')
85    * If certificate only authentication then passdb pam can be removed.
86    * Having two authentication mechanisms will cause dovecot to log an error,
87      'pam_authenticate() failed: Authentication failure' before accepting the SQL
88      authentication.
89 
90 */
91
92
93class certificatelogon extends rcube_plugin
94{
95
96  function init()
97  {
98    $this->add_hook('startup', array($this, 'startup'));
99    $this->add_hook('authenticate', array($this, 'authenticate'));
100    $this->add_hook('outgoing_message_headers', array($this, 'outgoing_message_headers'));
101    $this->add_texts('localization/', FALSE);
102  }
103
104  function startup($args)
105  {
106    $rcmail = rcmail::get_instance();
107    $cfg = $rcmail->config;
108    $pass = $cfg->get('client_cert_password');
109
110    if (empty($pass)) {
111       $rcmail->output->command('display_message', $this->gettext('client_cert_password not set - certificate logon disabled'), 'error');
112    }
113    if ($args['task'] == 'mail' && empty($args['action']) && empty($_SESSION['user_id'])
114      && !empty($pass) 
115      && $this->get_username($cfg)) {
116
117      // change action to login
118      $args['action'] = 'login';
119    }
120
121    return $args;
122  }
123
124  function authenticate($args)
125  {
126    $rcmail = rcmail::get_instance();
127    $cfg = $rcmail->config;
128
129    if ($user=$this->get_username($cfg)) {
130      $args['user'] = $user;
131      $args['pass'] = $cfg->get('client_cert_password');
132    }
133    return $args;
134  }
135
136  function get_username($cfg)
137  {
138    if ( $_SERVER['SSL_CLIENT_VERIFY'] != 'SUCCESS') 
139      return FALSE;
140
141    $c_mail_domain = $cfg->get('mail_domain'); 
142    // We need to search for emails in the certificate that match the mail_domain
143    if (is_array($c_mail_domain)) { 
144      $mail_domains = array_values($c_mail_domain); 
145    } else { 
146      $mail_domains = array($c_mail_domain); 
147    } 
148    $email = $_SERVER['SSL_CLIENT_S_DN_EMAIL']; 
149
150    $username = $this->validate_email($cfg,$email,$mail_domains);
151
152    if (empty($username)) { 
153      $d=0;
154      while ($email=$_SERVER["SSL_CLIENT_S_DN_EMAIL_$d"]) {
155        if ($username = $this->validate_email($cfg,$email,$mail_domains)) {
156          break;
157        }
158        ++$d;
159      }
160    }
161 
162    if (empty($username)) { 
163      $dn=$_SERVER['SSL_CLIENT_S_DN']; 
164      # match on the following DN
165      # emailAddress= (current cacert issued ones 20090402) https://bugs.cacert.org/view.php?id=672
166
167      if (preg_match_all('/\/emailAddress=([^\/]*)/',$dn,$reg,PREG_SET_ORDER)) { 
168        foreach ($reg as $emailarr) { 
169          $email = $emailarr[1]; 
170          if ($username = $this->validate_email($cfg,$email,$mail_domains)) {
171            break;
172          }
173        } 
174      } 
175    }
176
177    if (empty($username) && $_SERVER['SSL_CLIENT_CERT']) {
178      # subjectAltName unpresented by Apache http://httpd.apache.org/docs/trunk/mod/mod_ssl.html
179      # subjectAltName http://tools.ietf.org/html/rfc5280#section-4.2.1.6
180      # WARNING WARNING openssl_x509_parse is an unstable PHP API
181      $x509 = openssl_x509_parse($_SERVER['SSL_CLIENT_CERT']);
182      $subjectAltName = $x509['extensions']['subjectAltName']; // going off https://foaf.me/testSSL.php
183      #print_r(split("[, ]",$subjectAltName));
184      #print_r($x509);
185      #echo $subjectAltName;
186      if (preg_match_all('/email:([^, ]*)/',$subjectAltName,$reg,PREG_SET_ORDER)) {
187        foreach ($reg as $emailarr) { 
188          $email = $emailarr[1]; 
189          #echo $email;
190          if ($username = $this->validate_email($cfg,$email,$mail_domains)) {
191            break;
192          }
193        } 
194      }
195    } 
196   
197    if (!empty($username)) { 
198      $client_cert_username = $cfg->get('client_cert_username'); 
199      if (!empty($client_cert_username)) { 
200        $username = str_replace(array("%fu", "%u", "%d"), 
201                                array($email,$username['user'],$username['dom']), 
202                                $client_cert_username); 
203      } 
204      return $username['user'];
205    }
206    return FALSE;
207  }
208
209  function validate_email($cfg,$email,$mail_domains)
210  {
211      list($username, $domain) = explode('@',$email); 
212
213      if (!empty($domain) && in_array($domain,$mail_domains)) { 
214        if ($vusername = rcube_user::email2user($email)) 
215          $username = $vusername; 
216        return array( 'user' => $username, 'dom' => $domain);
217      } else { 
218        return FALSE;
219      } 
220  }
221
222  function outgoing_message_headers($args)
223  {
224    if (isset($_SERVER['SSL_CLIENT_S_DN'])) {
225      $header = ' with certificate (' . $_SERVER['SSL_CLIENT_S_DN'] 
226        . (isset($_SERVER['SSL_CLIENT_M_SERIAL']) ? ',serial='.$_SERVER['SSL_CLIENT_M_SERIAL'].')' : ',noserial)')
227        . (isset($_SERVER['SSL_CLIENT_I_DN']) ? ',issuer=(' . $_SERVER['SSL_CLIENT_I_DN'] . ')' : ',noissuer');
228      $args['headers']['Received'] = substr_replace($args['headers']['Received'], $header,strpos($args['headers']['Received'],';'),0);
229    }
230    return $args;
231  }
232
233}
234