| 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 | |
|---|
| 93 | class 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 | |
|---|