From 4a8c14466d1c58bcca517f967347f6f6ec4f46d0 Mon Sep 17 00:00:00 2001
From: m_horde <m_horde@secure.mailbox.org>
Date: Fri, 26 Dec 2014 01:19:10 +0100
Subject: [PATCH] Implementation of peer verification in TLS connections

---
 .../Imap_Client/lib/Horde/Imap/Client/Base.php     | 22 +++++-
 .../Imap_Client/lib/Horde/Imap/Client/Socket.php   |  6 +-
 .../Socket_Client/lib/Horde/Socket/Client.php      | 88 +++++++++++++++++++---
 imp/config/backends.php                            | 29 +++++++
 imp/lib/Imap.php                                   | 11 +--
 imp/lib/Imap/Config.php                            |  4 +-
 6 files changed, 140 insertions(+), 20 deletions(-)

diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Base.php b/framework/Imap_Client/lib/Horde/Imap/Client/Base.php
index 96c4fc7..8cb7de7 100644
--- a/framework/Imap_Client/lib/Horde/Imap/Client/Base.php
+++ b/framework/Imap_Client/lib/Horde/Imap/Client/Base.php
@@ -152,6 +152,13 @@ implements Serializable, SplObserver
     protected $_params = array();
 
     /**
+     * TLS configuration parameters.
+     *
+     * @var array
+     */
+    protected $_tls_params = array();
+
+    /**
      * The currently selected mailbox.
      *
      * @var Horde_Imap_Client_Mailbox
@@ -230,6 +237,17 @@ implements Serializable, SplObserver
      *            DEFAULT: 30 seconds
      * - username: (string) [REQUIRED] The username.
      * </pre>
+     * @param array $tls_params   TLS options.
+     * <pre>
+     *   - cafile (the certificate authority bundle; turns peer verification on if given)
+     *   - name (the peer name; turns peer name verification on if given)
+     *   - verify_name (turns peer name verification on; guess peer name if not given by 'name')
+     *   - ciphers (the cipher string to pass unto openssl)
+     *   - depth (verification depth, only if cafile is set)
+     *   - sha1 (sha1 fingerprint of server certificate; only applicable if cafile is given; all lowercase)
+     *   - md5 (md5 fingerprint of server certificate; only applicable if cafile is given; all lowercase)
+     *   - sha256 (sha256 fingerprint of server certificate; only applicable if cafile is given; all lowercase)
+     * </pre>
      */
     public function __construct(array $params = array())
     {
@@ -268,7 +286,7 @@ implements Serializable, SplObserver
         if (isset($params['password'])) {
             $this->setParam('password', $params['password']);
         }
-
+        
         $this->changed = true;
         $this->_initOb();
     }
@@ -353,6 +371,7 @@ implements Serializable, SplObserver
         return serialize(array(
             'i' => $this->_init,
             'p' => $this->_params,
+            't' => $this->_tls_params,
             'v' => self::VERSION
         ));
     }
@@ -370,6 +389,7 @@ implements Serializable, SplObserver
 
         $this->_init = $data['i'];
         $this->_params = $data['p'];
+        $this->_tls_params = $data['t'];
 
         $this->_initOb();
     }
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php b/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php
index 2caac3c..469ee2b 100644
--- a/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php
+++ b/framework/Imap_Client/lib/Horde/Imap/Client/Socket.php
@@ -166,13 +166,14 @@ class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
      *                    Horde_Imap_Client_Base_Password object (since
      *                    2.14.0).
      */
-    public function __construct(array $params = array())
+    public function __construct(array $params = array(), $tls_params = array())
     {
         parent::__construct(array_merge(array(
             'debug_literal' => false,
             'envelope_addrs' => 1000,
             'envelope_string' => 2048
         ), $params));
+        $this->_tls_params = $tls_params;
     }
 
     /**
@@ -574,7 +575,8 @@ class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
                 array(
                     'debug' => $this->_debug,
                     'debugliteral' => $this->getParam('debug_literal')
-                )
+                ),
+                $this->_tls_params
             );
         } catch (Horde\Socket\Client\Exception $e) {
             $e2 = new Horde_Imap_Client_Exception(
diff --git a/framework/Socket_Client/lib/Horde/Socket/Client.php b/framework/Socket_Client/lib/Horde/Socket/Client.php
index 91a666a..7b43d11 100644
--- a/framework/Socket_Client/lib/Horde/Socket/Client.php
+++ b/framework/Socket_Client/lib/Horde/Socket/Client.php
@@ -43,6 +43,13 @@ class Client
     protected $_params;
 
     /**
+     * TLS configuration parameters.
+     *
+     * @var array
+     */
+    protected $_tls_params;
+
+    /**
      * Is the connection secure?
      *
      * @var boolean
@@ -67,11 +74,22 @@ class Client
      *   - true (TLS if available/necessary)
      * </pre>
      * @param array $params     Additional options.
+     * @param array $tls_params TLS options.
+     * <pre>
+     *   - cafile (the certificate authority bundle; turns peer verification on if given)
+     *   - name (the peer name; turns peer name verification on if given)
+     *   - verify_name (turns peer name verification on; guess peer name if not given by 'name')
+     *   - ciphers (the cipher string to pass unto openssl)
+     *   - depth (verification depth, only if cafile is set)
+     *   - sha1 (sha1 fingerprint of server certificate; only applicable if cafile is given; all lowercase)
+     *   - md5 (md5 fingerprint of server certificate; only applicable if cafile is given; all lowercase)
+     *   - sha256 (sha256 fingerprint of server certificate; only applicable if cafile is given; all lowercase)
+     * </pre>
      *
      * @throws Horde\Socket\Client\Exception
      */
     public function __construct(
-        $host, $port, $timeout = 30, $secure = false, array $params = array()
+        $host, $port, $timeout = 30, $secure = false, array $params = array(), array $tls_params = array()
     )
     {
         if ($secure && !extension_loaded('openssl')) {
@@ -83,6 +101,8 @@ class Client
 
         $this->_params = $params;
 
+        $this->_tls_params = $tls_params;
+
         $this->_connect($host, $port, $timeout, $secure);
     }
 
@@ -125,6 +145,28 @@ class Client
         if ($this->connected &&
             !$this->secure &&
             (@stream_socket_enable_crypto($this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT) === true)) {
+            if (function_exists(openssl_x509_fingerprint)) {
+                /**openssl_x509_fingerprint exist since PHP 5.6*/
+                openssl_x509_export(stream_context_get_params($this->_stream)["options"]["ssl"]["peer_certificate"], $pem_cert);
+                if (!empty($this->_tls_params['sha1'])) {
+                    $cert_fp = openssl_x509_fingerprint($pem_cert, 'sha1', false);
+                    if ($cert_fp != $this->_tls_params['sha1']) {
+                        return false;
+                    }
+                }
+                if (!empty($this->_tls_params['md5'])) {
+                    $cert_fp = openssl_x509_fingerprint($pem_cert, 'md5', false);
+                    if ($cert_fp != $this->_tls_params['md5']) {
+                        return false;
+                    }
+                }
+                if (!empty($this->_tls_params['sha256'])) {
+                    $cert_fp = openssl_x509_fingerprint($pem_cert, 'sha256', false);
+                    if ($cert_fp != $this->_tls_params['sha256']) {
+                        return false;
+                    }
+                }
+            }
             $this->_secure = true;
             return true;
         }
@@ -174,22 +216,46 @@ class Client
             break;
         }
 
+        $stream_context_array = array(
+            'ssl' => array(
+                'verify_peer' => false,
+                'verify_peer_name' => false,
+                'ciphers' => 'ALL',
+                'verify_depth' => 10,
+                )
+            );
+
+        if (!empty($this->_tls_params['cafile'])) {
+            $stream_context_array['ssl']['verify_peer'] = true;
+            $stream_context_array['ssl']['cafile'] = $this->_tls_params['cafile'];
+        }
+        if (!empty($this->_tls_params['name']) || $this->_tls_params['verify_name'])
+        {
+            $stream_context_array['ssl']['verify_peer_name'] = true;
+        }
+        if (!empty($this->_tls_params['name']))
+        {
+            $stream_context_array['ssl']['peer_name'] = $this->_tls_params['name'];
+        }
+        if (!empty($this->_tls_params['ciphers'])) {
+            $stream_context_array['ssl']['ciphers'] = $this->_tls_params['ciphers'];
+        }
+        if (!empty($this->_tls_params['depth'])) {
+            $stream_context_array['ssl']['depth'] = $this->_tls_params['depth'];
+        }
+        if (!empty($this->_tls_params['sha1']) || !empty($this->_tls_params['md5']) ||
+            !empty($this->_tls_params['sha256'])) {
+            $stream_context_array['ssl']['capture_peer_cert'] = true;
+        }
+ 
         $this->_stream = @stream_socket_client(
             $conn . $host . ':' . $port,
             $error_number,
             $error_string,
             $timeout,
             STREAM_CLIENT_CONNECT,
-            /* @todo: As of PHP 5.6, TLS connections require valid certs.
-             * However, this is BC-breaking to this library. For now, keep
-             * pre-5.6 behavior. */
-            stream_context_create(array(
-                'ssl' => array(
-                    'verify_peer' => false,
-                    'verify_peer_name' => false
-                )
-            ))
-        );
+            stream_context_create($stream_context_array)
+            );
 
         if ($this->_stream === false) {
             /* From stream_socket_client() page: a function return of false,
diff --git a/imp/config/backends.php b/imp/config/backends.php
index 5cf7b2d..3653543 100644
--- a/imp/config/backends.php
+++ b/imp/config/backends.php
@@ -219,6 +219,26 @@
  *   - true:  Enable ACLs. (Not all IMAP servers support this feature).
  *   - false:  [DEFAULT] Disable ACLs.
  *
+ * tls_params: (array) TLS parameter to ensure a secure conncection
+ *   to a remote IMAP server.
+ *   
+ *   The following parameters are available:
+ *     - cafile: (string) The certificate authority bundle; turns peer
+ *               verification on if given.
+ *     - name: (string) The peer name; turns peer name verification on
+ *             if given.
+ *     - verify_name: (boolean) Turns peer name verification on; guess
+ *                    peer name if not given by 'name'.
+ *     - ciphers: (string) The cipher string to pass unto openssl.
+ *     - depth: (integer) Verification depth, only applicable if
+ *              cafile is set.
+ *     - sha1: (string) sha1 fingerprint of server certificate; only
+ *             applicable if cafile is given; all lowercase.
+ *     - md5: (string) md5 fingerprint of server certificate; only
+ *            applicable if cafile is given; all lowercase.
+ *     - sha256: (string) sha256 fingerprint of server certificate;
+ *               only applicable if cafile is given; all lowercase.
+ *
  * admin: (array) Use this if you want to enable mailbox management for
  *   administrators via Horde's user administration interface. The mailbox
  *   management gets enabled if you let IMP handle the Horde authentication
@@ -385,6 +405,15 @@ $servers['advanced'] = array(
     'protocol' => 'imap',
     'port' => 143,
     'secure' => 'tls',
+    'tls_params' => array(
+        'cafile' => '/path/to/cafile.pem',
+        'name' => 'your.domain.org',
+        'verify_name' => true,
+        'ciphers' => '!SSLv2:!MD5:!eNULL:!RC4:!RC2:!DES:!3DES:!kSRP:+SHA384:+SHA256:+SHA1:+DH:+ECDH:+ECDSA:+ADH:+AECDH:+ECDSA:+AES:+IDEA:+RSA:+kRSA:TLSv1:SSLv3',
+        'sha1' => 'a7467bc1ce43741cb3b1f015e678559a7c7071b9',
+        'md5' => '6b41eb055c4f5980cfa17122929e1b8a',
+        'sha256' => 'bb65981c105737bc7e5f59e65c4d518a997e1c5e0470c4ce642ffcf5e830cac0',
+        ),
     'maildomain' => '',
     'smtp' => array(
     //    'auth' => true,
diff --git a/imp/lib/Imap.php b/imp/lib/Imap.php
index 07afba0..b172e1a 100644
--- a/imp/lib/Imap.php
+++ b/imp/lib/Imap.php
@@ -255,7 +255,7 @@ class IMP_Imap implements Serializable
         $this->_config = $config;
 
         try {
-            return $this->createImapObject($imap_config, ($config->protocol == 'imap'));
+            return $this->createImapObject($imap_config, ($config->protocol == 'imap'), $config->tls_params);
         } catch (IMP_Imap_Exception $e) {
             unset($this->_config);
             throw $e;
@@ -265,13 +265,14 @@ class IMP_Imap implements Serializable
     /**
      * Create a Horde_Imap_Client object.
      *
-     * @param array $config  The IMAP configuration.
-     * @param boolean $imap  True if IMAP connection, false if POP3.
+     * @param array $config      The IMAP configuration.
+     * @param boolean $imap      True if IMAP connection, false if POP3.
+     * @param array $tls_config  The TLS configuration
      *
      * @return Horde_Imap_Client_Base  Client object.
      * @throws IMP_Imap_Exception
      */
-    public function createImapObject($config, $imap = true)
+    public function createImapObject($config, $imap = true, $tls_config = array())
     {
         if ($this->init) {
             return $this->_ob;
@@ -291,7 +292,7 @@ class IMP_Imap implements Serializable
 
         try {
             $this->_ob = $imap
-                ? new Horde_Imap_Client_Socket($config)
+                ? new Horde_Imap_Client_Socket($config, $tls_config)
                 : new Horde_Imap_Client_Socket_Pop3($config);
             return $this->_ob;
         } catch (Horde_Imap_Client_Exception $e) {
diff --git a/imp/lib/Imap/Config.php b/imp/lib/Imap/Config.php
index 746a7b5..47c7261 100644
--- a/imp/lib/Imap/Config.php
+++ b/imp/lib/Imap/Config.php
@@ -55,6 +55,7 @@
  * @property-read array $user_special_mboxes  List of user special mailboxes.
  * @property string $thread  The preferred thread sort algorithm.
  * @property string $timeout  The connection timeout (in seconds).
+ * @property array $tls_params  List of TLS parameters for peer verification.
  */
 class IMP_Imap_Config implements Serializable
 {
@@ -68,7 +69,8 @@ class IMP_Imap_Config implements Serializable
      */
     private $_aoptions = array(
         'admin', 'cache_params', 'capability_ignore', 'id', 'lang',
-        'namespace', 'preferred', 'quota', 'smtp', 'spam', 'special_mboxes'
+        'namespace', 'preferred', 'quota', 'smtp', 'spam', 'special_mboxes',
+        'tls_params'
     );
 
     /**
-- 
1.9.1