Blame SOURCES/0023-Ticket-bz1358565-clear-and-unsalted-password-types-a.patch

7c7f29
From f0e03b5a51972a125fe78f448d1f68e288782d1e Mon Sep 17 00:00:00 2001
7c7f29
From: William Brown <firstyear@redhat.com>
7c7f29
Date: Thu, 21 Jul 2016 13:22:30 +1000
7c7f29
Subject: [PATCH 23/29] Ticket bz1358565 -  clear and unsalted password types
7c7f29
 are vulnerable to timing attack
7c7f29
7c7f29
Bug Description:  Clear and unsalted password types were vulnerable to a timing
7c7f29
attack. This is due to the use of memcmp and strcmp in their comparison.
7c7f29
7c7f29
Fix Description:  Add a constant time memcmp function, that does not shortcircuit.
7c7f29
Change all password comparison to use the constant time check. For the clear
7c7f29
scheme, alter the way we do the check to prevent length disclosure timing
7c7f29
attacks.
7c7f29
7c7f29
This resolves CVE-2016-5405
7c7f29
7c7f29
https://bugzilla.redhat.com/show_bug.cgi?id=1358565
7c7f29
7c7f29
https://access.redhat.com/security/cve/CVE-2016-5405
7c7f29
7c7f29
Author: wibrown
7c7f29
7c7f29
Review by: nhosoi (Thanks!)
7c7f29
7c7f29
(cherry picked from commit 9dcaa4a0c866d8696e0a2616ccf962af2833f0b8)
7c7f29
---
7c7f29
 dirsrvtests/tests/suites/password/pwd_algo_test.py | 143 +++++++++++++++++++++
7c7f29
 ldap/servers/plugins/pwdstorage/clear_pwd.c        |  33 ++++-
7c7f29
 ldap/servers/plugins/pwdstorage/crypt_pwd.c        |   2 +-
7c7f29
 ldap/servers/plugins/pwdstorage/md5_pwd.c          |   2 +-
7c7f29
 ldap/servers/plugins/pwdstorage/ns-mta-md5_pwd.c   |   1 +
7c7f29
 ldap/servers/plugins/pwdstorage/sha_pwd.c          |  15 ++-
7c7f29
 ldap/servers/plugins/pwdstorage/smd5_pwd.c         |   2 +-
7c7f29
 ldap/servers/slapd/ch_malloc.c                     |  22 ++++
7c7f29
 ldap/servers/slapd/slapi-plugin.h                  |  16 +++
7c7f29
 9 files changed, 226 insertions(+), 10 deletions(-)
7c7f29
 create mode 100644 dirsrvtests/tests/suites/password/pwd_algo_test.py
7c7f29
7c7f29
diff --git a/dirsrvtests/tests/suites/password/pwd_algo_test.py b/dirsrvtests/tests/suites/password/pwd_algo_test.py
7c7f29
new file mode 100644
7c7f29
index 0000000..aa8cbf5
7c7f29
--- /dev/null
7c7f29
+++ b/dirsrvtests/tests/suites/password/pwd_algo_test.py
7c7f29
@@ -0,0 +1,143 @@
7c7f29
+import os
7c7f29
+import sys
7c7f29
+import time
7c7f29
+import ldap
7c7f29
+import logging
7c7f29
+import pytest
7c7f29
+from lib389 import DirSrv, Entry, tools, tasks
7c7f29
+from lib389.tools import DirSrvTools
7c7f29
+from lib389._constants import *
7c7f29
+from lib389.properties import *
7c7f29
+from lib389.tasks import *
7c7f29
+from lib389.utils import *
7c7f29
+
7c7f29
+DEBUGGING = True
7c7f29
+USER_DN = 'uid=user,ou=People,%s' % DEFAULT_SUFFIX
7c7f29
+
7c7f29
+if DEBUGGING:
7c7f29
+    logging.getLogger(__name__).setLevel(logging.DEBUG)
7c7f29
+else:
7c7f29
+    logging.getLogger(__name__).setLevel(logging.INFO)
7c7f29
+
7c7f29
+
7c7f29
+log = logging.getLogger(__name__)
7c7f29
+
7c7f29
+
7c7f29
+class TopologyStandalone(object):
7c7f29
+    """The DS Topology Class"""
7c7f29
+    def __init__(self, standalone):
7c7f29
+        """Init"""
7c7f29
+        standalone.open()
7c7f29
+        self.standalone = standalone
7c7f29
+
7c7f29
+
7c7f29
+@pytest.fixture(scope="module")
7c7f29
+def topology(request):
7c7f29
+    """Create DS Deployment"""
7c7f29
+
7c7f29
+    # Creating standalone instance ...
7c7f29
+    if DEBUGGING:
7c7f29
+        standalone = DirSrv(verbose=True)
7c7f29
+    else:
7c7f29
+        standalone = DirSrv(verbose=False)
7c7f29
+    args_instance[SER_HOST] = HOST_STANDALONE
7c7f29
+    args_instance[SER_PORT] = PORT_STANDALONE
7c7f29
+    args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE
7c7f29
+    args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
7c7f29
+    args_standalone = args_instance.copy()
7c7f29
+    standalone.allocate(args_standalone)
7c7f29
+    instance_standalone = standalone.exists()
7c7f29
+    if instance_standalone:
7c7f29
+        standalone.delete()
7c7f29
+    standalone.create()
7c7f29
+    standalone.open()
7c7f29
+
7c7f29
+    def fin():
7c7f29
+        """If we are debugging just stop the instances, otherwise remove
7c7f29
+        them
7c7f29
+        """
7c7f29
+        if DEBUGGING:
7c7f29
+            standalone.stop()
7c7f29
+        else:
7c7f29
+            standalone.delete()
7c7f29
+
7c7f29
+    request.addfinalizer(fin)
7c7f29
+
7c7f29
+    # Clear out the tmp dir
7c7f29
+    standalone.clearTmpDir(__file__)
7c7f29
+
7c7f29
+    return TopologyStandalone(standalone)
7c7f29
+
7c7f29
+def _test_bind(inst, password):
7c7f29
+    result = True
7c7f29
+    userconn = ldap.initialize("ldap://%s:%s" % (HOST_STANDALONE, PORT_STANDALONE))
7c7f29
+    try:
7c7f29
+        userconn.simple_bind_s(USER_DN, password)
7c7f29
+        userconn.unbind_s()
7c7f29
+    except ldap.INVALID_CREDENTIALS:
7c7f29
+        result = False
7c7f29
+    return result
7c7f29
+
7c7f29
+def _test_algo(inst, algo_name):
7c7f29
+    inst.config.set('passwordStorageScheme', algo_name)
7c7f29
+
7c7f29
+    if DEBUGGING:
7c7f29
+        print('Testing %s', algo_name)
7c7f29
+
7c7f29
+    # Create the user with a password
7c7f29
+    inst.add_s(Entry((
7c7f29
+                USER_DN, {
7c7f29
+                    'objectClass': 'top account simplesecurityobject'.split(),
7c7f29
+                     'uid': 'user',
7c7f29
+                     'userpassword': 'Secret123'
7c7f29
+                })))
7c7f29
+
7c7f29
+    # Make sure when we read the userPassword field, it is the correct ALGO
7c7f29
+    pw_field = inst.search_s(USER_DN, ldap.SCOPE_BASE, '(objectClass=*)', ['userPassword']  )[0]
7c7f29
+
7c7f29
+    if DEBUGGING:
7c7f29
+        print(pw_field.getValue('userPassword'))
7c7f29
+
7c7f29
+    if algo_name != 'CLEAR':
7c7f29
+        assert(algo_name.lower() in pw_field.getValue('userPassword').lower())
7c7f29
+    # Now make sure a bind works
7c7f29
+    assert(_test_bind(inst, 'Secret123'))
7c7f29
+    # Bind with a wrong shorter password, should fail
7c7f29
+    assert(not _test_bind(inst, 'Wrong'))
7c7f29
+    # Bind with a wrong longer password, should fail
7c7f29
+    assert(not _test_bind(inst, 'This is even more wrong'))
7c7f29
+    # Bind with a wrong exact length password.
7c7f29
+    assert(not _test_bind(inst, 'Alsowrong'))
7c7f29
+    # Bind with a subset password, should fail
7c7f29
+    assert(not _test_bind(inst, 'Secret'))
7c7f29
+    if algo_name != 'CRYPT':
7c7f29
+        # Bind with a subset password that is 1 char shorter, to detect off by 1 in clear
7c7f29
+        assert(not _test_bind(inst, 'Secret12'))
7c7f29
+        # Bind with a superset password, should fail
7c7f29
+        assert(not _test_bind(inst, 'Secret123456'))
7c7f29
+    # Delete the user
7c7f29
+    inst.delete_s(USER_DN)
7c7f29
+    # done!
7c7f29
+
7c7f29
+def test_pwd_algo_test(topology):
7c7f29
+    """
7c7f29
+    Assert that all of our password algorithms correctly PASS and FAIL varying
7c7f29
+    password conditions.
7c7f29
+
7c7f29
+    """
7c7f29
+    if DEBUGGING:
7c7f29
+        # Add debugging steps(if any)...
7c7f29
+        pass
7c7f29
+
7c7f29
+    for algo in ('CLEAR', 'CRYPT', 'MD5', 'SHA', 'SHA256', 'SHA384', 'SHA512', 'SMD5', 'SSHA', 'SSHA256', 'SSHA384', 'SSHA512'):
7c7f29
+        _test_algo(topology.standalone, algo)
7c7f29
+
7c7f29
+    log.info('Test PASSED')
7c7f29
+
7c7f29
+
7c7f29
+if __name__ == '__main__':
7c7f29
+    # Run isolated
7c7f29
+    # -s for DEBUG mode
7c7f29
+    CURRENT_FILE = os.path.realpath(__file__)
7c7f29
+    pytest.main("-s %s" % CURRENT_FILE)
7c7f29
+
7c7f29
diff --git a/ldap/servers/plugins/pwdstorage/clear_pwd.c b/ldap/servers/plugins/pwdstorage/clear_pwd.c
7c7f29
index 84dac2a..2afe16e 100644
7c7f29
--- a/ldap/servers/plugins/pwdstorage/clear_pwd.c
7c7f29
+++ b/ldap/servers/plugins/pwdstorage/clear_pwd.c
7c7f29
@@ -25,7 +25,38 @@
7c7f29
 int
7c7f29
 clear_pw_cmp( const char *userpwd, const char *dbpwd )
7c7f29
 {
7c7f29
-    return( strcmp( userpwd, dbpwd ));
7c7f29
+    int result = 0;
7c7f29
+    int len = 0;
7c7f29
+    int len_user = strlen(userpwd);
7c7f29
+    int len_dbp = strlen(dbpwd);
7c7f29
+    if ( len_user != len_dbp ) {
7c7f29
+        result = 1;
7c7f29
+    }
7c7f29
+    /* We have to do this comparison ANYWAY else we have a length timing attack. */
7c7f29
+    if ( len_user >= len_dbp ) {
7c7f29
+        /* 
7c7f29
+         * If they are the same length, result will be 0 here, and if we pass
7c7f29
+         * the check, we don't update result either. IE we pass.
7c7f29
+         * However, even if the first part of userpw matches dbpwd, but len !=, we
7c7f29
+         * have already failed anyawy. This prevents substring matching.
7c7f29
+         */
7c7f29
+        if (slapi_ct_memcmp(userpwd, dbpwd, len_dbp) != 0) {
7c7f29
+            result = 1;
7c7f29
+        }
7c7f29
+    } else {
7c7f29
+        /* 
7c7f29
+         * If we stretched the userPassword, we'll allow a new timing attack, where
7c7f29
+         * if we see a delay on a short pw, we know we are stretching.
7c7f29
+         * when the delay goes away, it means we've found the length.
7c7f29
+         * Instead, because we don't want to use the short pw for comp, we just compare
7c7f29
+         * dbpwd to itself. We have already got result == 1 if we are here, so we are
7c7f29
+         * just trying to take up time!
7c7f29
+         */
7c7f29
+        if (slapi_ct_memcmp(dbpwd, dbpwd, len_dbp)) {
7c7f29
+            /* Do nothing, we have the if to fix a coverity check. */
7c7f29
+        }
7c7f29
+    }
7c7f29
+    return result;
7c7f29
 }
7c7f29
 
7c7f29
 char *
7c7f29
diff --git a/ldap/servers/plugins/pwdstorage/crypt_pwd.c b/ldap/servers/plugins/pwdstorage/crypt_pwd.c
7c7f29
index 29355a2..93b54b2 100644
7c7f29
--- a/ldap/servers/plugins/pwdstorage/crypt_pwd.c
7c7f29
+++ b/ldap/servers/plugins/pwdstorage/crypt_pwd.c
7c7f29
@@ -54,7 +54,7 @@ crypt_pw_cmp( const char *userpwd, const char *dbpwd )
7c7f29
     /* we use salt (first 2 chars) of encoded password in call to crypt() */
7c7f29
     cp = crypt( userpwd, dbpwd );
7c7f29
     if (cp) {
7c7f29
-       rc= strcmp( dbpwd, cp);
7c7f29
+       rc= slapi_ct_memcmp( dbpwd, cp, strlen(dbpwd));
7c7f29
     } else {
7c7f29
        rc = -1;
7c7f29
     }
7c7f29
diff --git a/ldap/servers/plugins/pwdstorage/md5_pwd.c b/ldap/servers/plugins/pwdstorage/md5_pwd.c
7c7f29
index 0bc8f3c..181661a 100644
7c7f29
--- a/ldap/servers/plugins/pwdstorage/md5_pwd.c
7c7f29
+++ b/ldap/servers/plugins/pwdstorage/md5_pwd.c
7c7f29
@@ -57,7 +57,7 @@ md5_pw_cmp( const char *userpwd, const char *dbpwd )
7c7f29
    bver = NSSBase64_EncodeItem(NULL, (char *)b2a_out, sizeof b2a_out, &binary_item);
7c7f29
    /* bver points to b2a_out upon success */
7c7f29
    if (bver) {
7c7f29
-	   rc = strcmp(bver,dbpwd);
7c7f29
+	   rc = slapi_ct_memcmp(bver,dbpwd, strlen(dbpwd));
7c7f29
    } else {
7c7f29
 	   slapi_log_error(SLAPI_LOG_PLUGIN, MD5_SUBSYSTEM_NAME,
7c7f29
 					   "Could not base64 encode hashed value for password compare");
7c7f29
diff --git a/ldap/servers/plugins/pwdstorage/ns-mta-md5_pwd.c b/ldap/servers/plugins/pwdstorage/ns-mta-md5_pwd.c
7c7f29
index 2fed61f..ae1f7b8 100644
7c7f29
--- a/ldap/servers/plugins/pwdstorage/ns-mta-md5_pwd.c
7c7f29
+++ b/ldap/servers/plugins/pwdstorage/ns-mta-md5_pwd.c
7c7f29
@@ -84,6 +84,7 @@ ns_mta_md5_pw_cmp(const char * clear, const char *mangled)
7c7f29
 
7c7f29
   mta_hash[32] = mta_salt[32] = 0;
7c7f29
 
7c7f29
+  /* This is salted, so we don't need to change it for constant time */
7c7f29
   return( strcmp(mta_hash,ns_mta_hash_alg(buffer,mta_salt,clear)));
7c7f29
 }
7c7f29
 
7c7f29
diff --git a/ldap/servers/plugins/pwdstorage/sha_pwd.c b/ldap/servers/plugins/pwdstorage/sha_pwd.c
7c7f29
index 9594ac9..2e4973b 100644
7c7f29
--- a/ldap/servers/plugins/pwdstorage/sha_pwd.c
7c7f29
+++ b/ldap/servers/plugins/pwdstorage/sha_pwd.c
7c7f29
@@ -120,13 +120,16 @@ sha_pw_cmp (const char *userpwd, const char *dbpwd, unsigned int shaLen )
7c7f29
     }
7c7f29
 
7c7f29
     /* the proof is in the comparison... */
7c7f29
-    result = ( hash_len >= shaLen ) ?
7c7f29
-        ( memcmp( userhash, dbhash, shaLen ) ) : /* include salt */
7c7f29
-        ( memcmp( userhash, dbhash + OLD_SALT_LENGTH,
7c7f29
-                  hash_len - OLD_SALT_LENGTH ) ); /* exclude salt */
7c7f29
+    if ( hash_len >= shaLen ) {
7c7f29
+        result = slapi_ct_memcmp( userhash, dbhash, shaLen );
7c7f29
+    } else {
7c7f29
+        result = slapi_ct_memcmp( userhash, dbhash + OLD_SALT_LENGTH, hash_len - OLD_SALT_LENGTH );
7c7f29
+    }
7c7f29
 
7c7f29
-    loser:
7c7f29
-    if ( dbhash && dbhash != quick_dbhash ) slapi_ch_free_string( &dbhash );
7c7f29
+loser:
7c7f29
+    if ( dbhash && dbhash != quick_dbhash ) {
7c7f29
+        slapi_ch_free_string( &dbhash );
7c7f29
+    }
7c7f29
     return result;
7c7f29
 }
7c7f29
 
7c7f29
diff --git a/ldap/servers/plugins/pwdstorage/smd5_pwd.c b/ldap/servers/plugins/pwdstorage/smd5_pwd.c
7c7f29
index f4c92f1..79c2846 100644
7c7f29
--- a/ldap/servers/plugins/pwdstorage/smd5_pwd.c
7c7f29
+++ b/ldap/servers/plugins/pwdstorage/smd5_pwd.c
7c7f29
@@ -80,7 +80,7 @@ smd5_pw_cmp( const char *userpwd, const char *dbpwd )
7c7f29
    PK11_DestroyContext(ctx, 1);
7c7f29
 
7c7f29
    /* Compare everything up to the salt. */
7c7f29
-   rc = memcmp( userhash, dbhash, MD5_LENGTH );
7c7f29
+   rc = slapi_ct_memcmp( userhash, dbhash, MD5_LENGTH );
7c7f29
 
7c7f29
 loser:
7c7f29
    if ( dbhash && dbhash != quick_dbhash ) slapi_ch_free_string( (char **)&dbhash );
7c7f29
diff --git a/ldap/servers/slapd/ch_malloc.c b/ldap/servers/slapd/ch_malloc.c
7c7f29
index 10870df..a38268c 100644
7c7f29
--- a/ldap/servers/slapd/ch_malloc.c
7c7f29
+++ b/ldap/servers/slapd/ch_malloc.c
7c7f29
@@ -365,3 +365,25 @@ slapi_ch_smprintf(const char *fmt, ...)
7c7f29
 	return p;
7c7f29
 }
7c7f29
 #endif
7c7f29
+
7c7f29
+/* Constant time memcmp. Does not shortcircuit on failure! */
7c7f29
+/* This relies on p1 and p2 both being size at least n! */
7c7f29
+int
7c7f29
+slapi_ct_memcmp( const void *p1, const void *p2, size_t n)
7c7f29
+{
7c7f29
+    int result = 0;
7c7f29
+    const unsigned char *_p1 = (const unsigned char *)p1;
7c7f29
+    const unsigned char *_p2 = (const unsigned char *)p2;
7c7f29
+
7c7f29
+    if (_p1 == NULL || _p2 == NULL) {
7c7f29
+        return 2;
7c7f29
+    }
7c7f29
+
7c7f29
+    for (size_t i = 0; i < n; i++) {
7c7f29
+        if (_p1[i] ^ _p2[i]) {
7c7f29
+            result = 1;
7c7f29
+        }
7c7f29
+    }
7c7f29
+    return result;
7c7f29
+}
7c7f29
+
7c7f29
diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h
7c7f29
index a7e544a..165fb05 100644
7c7f29
--- a/ldap/servers/slapd/slapi-plugin.h
7c7f29
+++ b/ldap/servers/slapd/slapi-plugin.h
7c7f29
@@ -5825,6 +5825,22 @@ char * slapi_ch_smprintf(const char *fmt, ...)
7c7f29
 #else
7c7f29
         ;
7c7f29
 #endif
7c7f29
+/**
7c7f29
+ * slapi_ct_memcmp is a constant time memory comparison function. This is for
7c7f29
+ * use with password hashes and other locations which could lead to a timing
7c7f29
+ * attack due to early shortcut returns. This function *does not* shortcircuit
7c7f29
+ * during the comparison, always checking every byte regardless if it has already
7c7f29
+ * found that the memory does not match.
7c7f29
+ *
7c7f29
+ * WARNING! p1 and p2 must both reference content that is at least of size 'n'.
7c7f29
+ * Else this function may over-run (And will certainly fail).
7c7f29
+ *
7c7f29
+ * \param p1 pointer to first value to check.
7c7f29
+ * \param p2 pointer to second value to check.
7c7f29
+ * \param n length in bytes of the content of p1 AND p2.
7c7f29
+ * \return 0 on match. 1 on non-match. 2 on presence of NULL pointer in p1 or p2.
7c7f29
+ */
7c7f29
+int slapi_ct_memcmp( const void *p1, const void *p2, size_t n);
7c7f29
 
7c7f29
 /*
7c7f29
  * syntax plugin routines
7c7f29
-- 
7c7f29
2.4.11
7c7f29