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.