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
- Prepare Certification Authority
- Configure Certification Authority
- Initialize Certification Authority
- Create root certificate
- Create server certificate
- Create client certificate
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.