Categories
SysOps

Kolab – SSL certificate authentication (web-based interface)

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.