This weekend, I have spent some time investigating SSL certificate-based authentication and implementing it in Kolab web-based user interface.

This topic is fascinating but definitely too broad to be briefly described in a single blog post, so do not look at it as a complete solution, but treat it only as a proof of concept.

Table of contents

Certification Authority

Apache

Kolab – Web-based user interface

Notes

Prepare Certification Authority

At first, you need to create the Certification Authority on an off-line and secured system.

I have already created the required shell scripts (miniature-octo-ca) to ease the whole operation, so just clone the following repository and move it to the CA system.

$ git clone https://github.com/milosz/miniature-octo-ca.git
Cloning into 'miniature-octo-ca'...
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 10 (delta 2), reused 10 (delta 2)
Unpacking objects: 100% (10/10), done.

Please remember to change the working directory before executing any available shell script.

$ cd miniature-octo-ca

Configure Certification Authority

The next step is to configure CA by using common-ca-settings.sh configuration file.

$ vi common-ca-settings.sh
#!/bin/sh
# common CA settings

# simple protection - every script can be executed only from current directory
if [ "$(pwd)" != "$(dirname $(readlink -f  $0))" ]; then
  echo "Do not run CA scripts from outside of $(dirname $(readlink -f  $0)) directory"
  exit
fi

# ensure proper permissions by setting umask
umask 077

# kolab secret
# use 'openssl rand -hex 16' command to generate it
kolab_secret="d2d97d097eedb397edea79f52b56ea74"

# key length
key_length=4096

# certificates directory
cert_directory="root-ca"

# number of days to certify the certificate
cert_validfor=3650        # root   certificate
client_cert_validfor=365  # client certificate
server_cert_validfor=365  # server certificate

# default certificate settings
cert_country="PL"
cert_organization="example.org"
cert_state="state"
cert_city="city"
cert_name="example.org CA"
cert_unit="Certificate Authority"
cert_email=""

# certificate number
if [ -f "${cert_directory}/serial" ]; then
  serial=$(cat ${cert_directory}/serial)
fi

You need to modify kolab_secret variable, as it will be used as a key to encrypt/decrypt user password and common certificate settings to match your setup.

Initialize Certification Authority

Execute prepare_ca.sh shell script to build initial configuration and directory layout.

$ sh prepare_ca.sh

You can inspect the OpenSSL configuration (openssl.cnf file) and tune it a bit.

Create root certificate

Execute create_ca.sh shell script to create root certificate and private key.

$ sh create_ca.sh
Root certificate (private key) password: Generating a 4096 bit RSA private key
............................................................................++
.........................++
writing new private key to 'root-ca/ca/root-key.pem'
-----
No value provided for Subject Attribute emailAddress, skipped

Root certificate and private key will be stored inside root-ca/ca/ directory.

$ ls root-ca/ca/
root-cert.pem  root-key.pem

Create server certificate

Execute add_server.sh shell script to create new server certificate.

$ sh add_server.sh
Server name (eg. mail.example.com): mail.example.org
Email: admin@example.org
Root certificate (private key) password:
Server certificate (private key) password:
Generating a 4096 bit RSA private key
.........++++++
...................++++++
writing new private key to 'root-ca/private/01.pem'
-----
Using configuration from openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 1 (0x1)
        Validity
            Not Before: Jul  6 12:51:12 2014 GMT
            Not After : Jul  6 12:51:12 2015 GMT
        Subject:
            countryName               = PL
            stateOrProvinceName       = state
            organizationName          = example.org
            organizationalUnitName    = Certificate Authority
            commonName                = mail.example.org
            emailAddress              = admin@example.org
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Comment:
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier:
                EA:3E:05:51:EE:C2:90:53:58:91:E8:D5:56:47:15:7D:5A:26:E8:C4
            X509v3 Authority Key Identifier:
                keyid:A1:41:B0:72:60:29:1A:9B:B1:63:77:53:E7:93:71:1D:02:14:A4:7C

Certificate is to be certified until Jul  6 12:51:12 2015 GMT (365 days)

Write out database with 1 new entries
[..]
Data Base Updated
writing RSA key

The server certificate and private key (with password removed) will be stored inside root-ca/server_certs/ directory.

$ ls root-ca/server_certs/
01.crt  01.pem

Create client certificate

Execute add_client.sh shell script to create new client certificate.

$ sh add_client.sh
User name (eg. John Doe): Milosz
Email: milosz@example.org
Export password:
Kolab password:
Root certificate (private key) password:
Client certificate (private key) password: Generating a 4096 bit RSA private key
...++++++
.......++++++
writing new private key to 'root-ca/private/02.pem'
-----
Using configuration from openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 2 (0x2)
        Validity
            Not Before: Jul  6 12:57:39 2014 GMT
            Not After : Jul  6 12:57:39 2015 GMT
        Subject:
            countryName               = PL
            stateOrProvinceName       = state
            organizationName          = example.org
            organizationalUnitName    = Certificate Authority
            commonName                = Milosz
            emailAddress              = milosz@example.org
            kolabPasswordEnc          = RX3f071sOYKxwDBhNpDVHA==
            kolabPasswordIV           = 72a1e2086a765204122109382f8d4f5d
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Comment:
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier:
                3B:7A:BF:A5:B8:F4:C9:E0:0D:81:41:0D:EE:27:F4:B5:C3:B0:40:67
            X509v3 Authority Key Identifier:
                keyid:A1:41:B0:72:60:29:1A:9B:B1:63:77:53:E7:93:71:1D:02:14:A4:7C

Certificate is to be certified until Jul  6 12:57:39 2015 GMT (365 days)

Write out database with 1 new entries
[..]
Data Base Updated

Email field will be used to identify the user. Kolab password field is a password that will be encrypted using kolab_secret key and stored inside the certificate file (alongside the initialization vector).

The certificate will be stored inside root-ca/client_certs/ directory and protected using specified export password (can be easily imported into browser).

$ ls root-ca/client_certs/
02.p12

Apache – Enable HTTPS protocol

Enable SSL module.

# a2enmod ssl
Enabling module ssl.

Enable default SSL virtual host.

# a2ensite default-ssl
Enabling site default-ssl.

Disable default (non SSL) virtual host.

# a2dissite default
Site default disabled.

Create a simple virtual host listening on port 80 to redirect traffic to the HTTPS protocol.

cat << EOF > /etc/apache2/sites-available/default-rewrite
<VirtualHost *:80>
  ServerName mail.example.org
  Redirect / https://mail.example.org/
</VirtualHost>
EOF

Enable the site created above.

# a2ensite default-rewrite
Enabling site default-rewrite.

Change protocol used by the Kolab Files module.

# sed -i -e "s/http:/https:/" /etc/roundcubemail/kolab_files.inc.php

Restart Apache and test applied modifications.

# service apache2 restart

Apache – Switch to own Certification Authority

Create /etc/apache2/ssl/ directory.

# mkdir /etc/apache2/ssl

Copy root certificate root-cert.pem, server certificate server.crt , and server private key server.key to the directory created in the previous step.

Edit Apache configuration to use uploaded server certificate and private key.

# sed -i -e "/SSLCACertificateFile/ s/#//;s/ssl.crt\/ca-bundle.crt/ssl\/root-cert.pem/" /etc/apache2/sites-available/default-ssl
# sed -i -e "/SSLCertificateFile/ s/\/etc\/ssl\/certs\/ssl-cert-snakeoil.pem/\/etc\/apache2\/ssl\/server.crt/" /etc/apache2/sites-available/default-ssl
# sed -i -e "/SSLCertificateKeyFile/ s/\/etc\/ssl\/private\/ssl-cert-snakeoil.key/\/etc\/apache2\/ssl\/server.pem/" /etc/apache2/sites-available/default-ssl

Restart web-server and test applied changes.

# service apache2 restart

Import root certificate root-cert.pem into the browser as Certification Authority, then client certificate.

Alter web-server configuration to require valid client certificate, but allow direct API calls from the mail server (omit internal error when using kolab-admin).

# sed -i -e "/\/VirtualHost/i <Location />\nSSLRequireSSL\nSSLVerifyClient require\nSSLVerifyDepth 1\nOrder allow,deny\nallow from all\n</Location>\n\n<Location /kolab-webadmin/api/>\nSSLVerifyClient none\norder deny,allow\ndeny from all\nallow from mail.example.org\n</Location>" /etc/apache2/sites-available/default-ssl

Restart web-server and test client certificate.

# service apache2 restart

Kolab – Use client certificate to fill username filed

You can use a client certificate to fill username name inside the login form.

To achieve this simple task, you need to edit login_form function found in /usr/share/roundcubemail/program/include/rcmail_output_html.php file.

--- /usr/share/roundcubemail/program/include/rcmail_output_html.php.orig	2014-07-06 16:24:08.005325038 +0200
+++ /usr/share/roundcubemail/program/include/rcmail_output_html.php	2014-07-06 16:40:54.429360653 +0200
@@ -1551,40 +1551,47 @@
     protected function login_form($attrib)
     {
         $default_host = $this->config->get('default_host');
         $autocomplete = (int) $this->config->get('login_autocomplete');

         $_SESSION['temp'] = true;

         // save original url
         $url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
         if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))
             $url = $_SERVER['QUERY_STRING'];

         // Disable autocapitalization on iPad/iPhone (#1488609)
         $attrib['autocapitalize'] = 'off';

+        $email="";
+        if ($_SERVER["HTTPS"] == "on" &&  $_SERVER["SSL_CLIENT_VERIFY"] == "SUCCESS") {
+          if (preg_match('/\/emailAddress=([^\/]*)\//',$_SERVER['SSL_CLIENT_S_DN'],$matches)) {
+            $email=$matches[1];
+          }
+        }
+
         // set atocomplete attribute
         $user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
         $host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
         $pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');

         $input_task   = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
         $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
         $input_tzone  = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
         $input_url    = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
-        $input_user   = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required')
+        $input_user   = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required', 'value' => $email)
             + $attrib + $user_attrib);
         $input_pass   = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'required' => 'required')
             + $attrib + $pass_attrib);
         $input_host   = null;

         if (is_array($default_host) && count($default_host) > 1) {
             $input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost'));

             foreach ($default_host as $key => $value) {
                 if (!is_array($value)) {
                     $input_host->add($value, (is_numeric($key) ? $value : $key));
                 }
                 else {
                     $input_host = null;
                     break;

Use client certificate to login user

Generated client certificate already contains an encrypted password (using kolab_secret key) and initialization vector, so you can use them to automatically login user using /usr/share/roundcubemail/index.php file.

--- /usr/share/roundcubemail/index.php.orig	2014-07-06 18:32:40.830414058 +0200
+++ /usr/share/roundcubemail/index.php	2014-07-06 18:37:07.462423513 +0200
@@ -88,17 +88,26 @@
 $RCMAIL->action = $startup['action'];

 // try to log in
-if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
-    $request_valid = $_SESSION['temp'] && $RCMAIL->check_request(rcube_utils::INPUT_POST, 'login');
+if ($RCMAIL->task == 'login' && $_SERVER["HTTPS"] == "on" &&  $_SERVER["SSL_CLIENT_VERIFY"] == "SUCCESS") {
+    $request_valid = 1;
+    if (preg_match('/\/emailAddress=([^\/]*)\//',$_SERVER['SSL_CLIENT_S_DN'],$matches)) {
+      $email=$matches[1];
+    }
+    if (preg_match('/\/1.2.3.4.5.6.7.1=([^\/]*)/',$_SERVER['SSL_CLIENT_S_DN'],$matches)) {
+      $pass=$matches[1];
+    }
+    if (preg_match('/\/1.2.3.4.5.6.7.2=([^\/]*)/',$_SERVER['SSL_CLIENT_S_DN'],$matches)) {
+      $iv=$matches[1];
+    }
+    $pass=rtrim(openssl_decrypt(base64_decode($pass),'aes-128-cbc', hex2bin("d2d97d097eedb397edea79f52b56ea74"), true,hex2bin($iv)));

     // purge the session in case of new login when a session already exists
     $RCMAIL->kill_session();

     $auth = $RCMAIL->plugins->exec_hook('authenticate', array(
         'host' => $RCMAIL->autoselect_host(),
-        'user' => trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST)),
-        'pass' => rcube_utils::get_input_value('_pass', rcube_utils::INPUT_POST, true,
-            $RCMAIL->config->get('password_charset', 'ISO-8859-1')),
+        'user' => $email,
+        'pass' => $pass,
         'cookiecheck' => true,
         'valid'       => $request_valid,
     ));

Future improvements

kolab_secret can be stored using the Roundcube configuration file. The login_ form_ can be modified further to remove input fields and include more information.

There should be no problem to add a shell script to generate CRL.

PHP code could be simplified a bit.

Please inspect shell scripts to get an idea of additional certificate parameters.