Blob Blame History Raw
From 957ffd53b041c19d27753a028e6f514dcc75dfbd Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 26 Oct 2021 15:51:24 -0700
Subject: [PATCH 03/12] Issue 3584 - Fix PBKDF2_SHA256 hashing in FIPS mode
 (#4949)

Issue Description: Use PK11_Decrypt function to get hash data
because PK11_ExtractKeyValue function is forbidden in FIPS mode.
We can't extract keys while in FIPS mode. But we use PK11_ExtractKeyValue
for hashes, and it's not forbidden.

We can't use OpenSSL's PBKDF2-SHA256 implementation right now because
we need to support an upgrade procedure while in FIPS mode (update
hash on bind). For that, we should fix existing PBKDF2 usage, and we can
switch to OpenSSL's PBKDF2-SHA256 in the following versions.

Fix Description: Use PK11_Decrypt function to get the data.

Enable TLS on all CI test topologies while in FIPS because without
that we don't set up the NSS database correctly.

Add PBKDF2-SHA256 (OpenSSL) to ldif templates, so the password scheme is
discoverable by internal functions.

https://github.com/389ds/389-ds-base/issues/3584

Reviewed by: @progier389, @mreynolds389, @Firstyear, @tbordaz (Thanks!!)
---
 .../healthcheck/health_security_test.py       | 10 ---
 ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c  | 62 ++++++++++++++++---
 ldap/servers/slapd/main.c                     | 12 ++++
 src/lib389/lib389/__init__.py                 |  4 ++
 src/lib389/lib389/topologies.py               |  6 +-
 src/lib389/lib389/utils.py                    | 13 ++++
 6 files changed, 86 insertions(+), 21 deletions(-)

diff --git a/dirsrvtests/tests/suites/healthcheck/health_security_test.py b/dirsrvtests/tests/suites/healthcheck/health_security_test.py
index 6c0d27aaa..c1dc7938c 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_security_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_security_test.py
@@ -40,16 +40,6 @@ else:
 log = logging.getLogger(__name__)
 
 
-def is_fips():
-    if os.path.exists('/proc/sys/crypto/fips_enabled'):
-        with open('/proc/sys/crypto/fips_enabled', 'r') as f:
-            state = f.readline().strip()
-            if state == '1':
-                return True
-            else:
-                return False
-
-
 def run_healthcheck_and_flush_log(topology, instance, searched_code, json, searched_code2=None):
     args = FakeArgs()
     args.instance = instance.serverid
diff --git a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
index d310dc792..dcac4fcdd 100644
--- a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
+++ b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
@@ -91,10 +91,11 @@ pbkdf2_sha256_extract(char *hash_in, SECItem *salt, uint32_t *iterations)
 SECStatus
 pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem *salt, uint32_t iterations)
 {
-    SECItem *result = NULL;
     SECAlgorithmID *algid = NULL;
     PK11SlotInfo *slot = NULL;
     PK11SymKey *symkey = NULL;
+    SECItem *wrapKeyData = NULL;
+    SECStatus rv = SECFailure;
 
     /* We assume that NSS is already started. */
     algid = PK11_CreatePBEV2AlgorithmID(SEC_OID_PKCS5_PBKDF2, SEC_OID_HMAC_SHA256, SEC_OID_HMAC_SHA256, hash_out_len, iterations, salt);
@@ -104,7 +105,6 @@ pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem *s
         slot = PK11_GetBestSlotMultiple(mechanism_array, 2, NULL);
         if (slot != NULL) {
             symkey = PK11_PBEKeyGen(slot, algid, pwd, PR_FALSE, NULL);
-            PK11_FreeSlot(slot);
             if (symkey == NULL) {
                 /* We try to get the Error here but NSS has two or more error interfaces, and sometimes it uses none of them. */
                 int32_t status = PORT_GetError();
@@ -123,18 +123,60 @@ pbkdf2_sha256_hash(char *hash_out, size_t hash_out_len, SECItem *pwd, SECItem *s
         return SECFailure;
     }
 
-    if (PK11_ExtractKeyValue(symkey) == SECSuccess) {
-        result = PK11_GetKeyData(symkey);
-        if (result != NULL && result->len <= hash_out_len) {
-            memcpy(hash_out, result->data, result->len);
-            PK11_FreeSymKey(symkey);
+    /*
+     * First, we need to generate a wrapped key for PK11_Decrypt call:
+     * slot is the same slot we used in PK11_PBEKeyGen()
+     * 256 bits / 8 bit per byte
+     */
+    PK11SymKey *wrapKey = PK11_KeyGen(slot, CKM_AES_ECB, NULL, 256/8, NULL);
+    PK11_FreeSlot(slot);
+    if (wrapKey == NULL) {
+        slapi_log_err(SLAPI_LOG_ERR, "pbkdf2_sha256_hash", "Unable to generate a wrapped key.\n");
+        return SECFailure;
+	}
+
+    wrapKeyData = (SECItem *)PORT_Alloc(sizeof(SECItem));
+    /* Align the wrapped key with 32 bytes. */
+    wrapKeyData->len = (PK11_GetKeyLength(symkey) + 31) & ~31;
+    /* Allocate the aligned space for pkc5PBE key plus AESKey block */
+    wrapKeyData->data = (unsigned char *)slapi_ch_calloc(wrapKeyData->len, sizeof(unsigned char));
+
+    /* Get symkey wrapped with wrapKey - required for PK11_Decrypt call */
+    rv = PK11_WrapSymKey(CKM_AES_ECB, NULL, wrapKey, symkey, wrapKeyData);
+    if (rv != SECSuccess) {
+        PK11_FreeSymKey(symkey);
+        PK11_FreeSymKey(wrapKey);
+        SECITEM_FreeItem(wrapKeyData, PR_TRUE);
+        slapi_log_err(SLAPI_LOG_ERR, "pbkdf2_sha256_hash", "Unable to wrap the symkey. (%d)\n", rv);
+        return SECFailure;
+    }
+
+    /* Allocate the space for our result */
+    void *result = (char *)slapi_ch_calloc(wrapKeyData->len, sizeof(char));
+    unsigned int result_len = 0;
+
+    /* User wrapKey to decrypt the wrapped contents.
+     * result is the hash that we need;
+     * result_len is the actual lengh of the data;
+     * has_out_len is the maximum (the space we allocted for hash_out)
+     */
+    rv = PK11_Decrypt(wrapKey, CKM_AES_ECB, NULL, result, &result_len, hash_out_len, wrapKeyData->data, wrapKeyData->len);
+    PK11_FreeSymKey(symkey);
+    PK11_FreeSymKey(wrapKey);
+    SECITEM_FreeItem(wrapKeyData, PR_TRUE);
+
+    if (rv == SECSuccess) {
+        if (result != NULL && result_len <= hash_out_len) {
+            memcpy(hash_out, result, result_len);
+            slapi_ch_free((void **)&result);
         } else {
-            PK11_FreeSymKey(symkey);
-            slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to retrieve (get) hash output.\n");
+            slapi_log_err(SLAPI_LOG_ERR, "pbkdf2_sha256_hash", "Unable to retrieve (get) hash output.\n");
+            slapi_ch_free((void **)&result);
             return SECFailure;
         }
     } else {
-        slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to extract hash output.\n");
+        slapi_log_err(SLAPI_LOG_ERR, "pbkdf2_sha256_hash", "Unable to extract hash output. (%d)\n", rv);
+        slapi_ch_free((void **)&result);
         return SECFailure;
     }
 
diff --git a/ldap/servers/slapd/main.c b/ldap/servers/slapd/main.c
index 61ed40b7d..04d0494f8 100644
--- a/ldap/servers/slapd/main.c
+++ b/ldap/servers/slapd/main.c
@@ -2895,9 +2895,21 @@ slapd_do_all_nss_ssl_init(int slapd_exemode, int importexport_encrypt, int s_por
      * is enabled or not. We use NSS for random number generation and
      * other things even if we are not going to accept SSL connections.
      * We also need NSS for attribute encryption/decryption on import and export.
+     *
+     * It's important to remember that while in FIPS mode the administrator should always enable
+     * the security, otherwise we don't call slapd_pk11_authenticate which is a requirement for FIPS mode
      */
+    PRBool isFIPS = slapd_pk11_isFIPS();
     int init_ssl = config_get_security();
 
+    if (isFIPS && !init_ssl) {
+        slapi_log_err(SLAPI_LOG_WARNING, "slapd_do_all_nss_ssl_init",
+                      "ERROR: TLS is not enabled, and the machine is in FIPS mode. "
+                      "Some functionality won't work correctly (for example, "
+                      "users with PBKDF2_SHA256 password scheme won't be able to log in). "
+                      "It's highly advisable to enable TLS on this instance.\n");
+    }
+
     if (slapd_exemode == SLAPD_EXEMODE_SLAPD) {
         init_ssl = init_ssl && (0 != s_port) && (s_port <= LDAP_PORT_MAX);
     } else {
diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py
index 29ee5245a..e0299c5b4 100644
--- a/src/lib389/lib389/__init__.py
+++ b/src/lib389/lib389/__init__.py
@@ -1588,6 +1588,10 @@ class DirSrv(SimpleLDAPObject, object):
         :param post_open: Open the server connection after restart.
         :type post_open: bool
         """
+        if self.config.get_attr_val_utf8_l("nsslapd-security") == 'on':
+            self.restart(post_open=post_open)
+            return
+
         # If it doesn't exist, create a cadb.
         ssca = NssSsl(dbpath=self.get_ssca_dir())
         if not ssca._db_exists():
diff --git a/src/lib389/lib389/topologies.py b/src/lib389/lib389/topologies.py
index e9969f524..e7d56582d 100644
--- a/src/lib389/lib389/topologies.py
+++ b/src/lib389/lib389/topologies.py
@@ -15,7 +15,7 @@ import socket
 import pytest
 
 from lib389 import DirSrv
-from lib389.utils import generate_ds_params
+from lib389.utils import generate_ds_params, is_fips
 from lib389.mit_krb5 import MitKrb5
 from lib389.saslmap import SaslMappings
 from lib389.replica import ReplicationManager, Replicas
@@ -108,6 +108,10 @@ def _create_instances(topo_dict, suffix):
             if role == ReplicaRole.HUB:
                 hs[instance.serverid] = instance
                 instances.update(hs)
+            # We should always enable TLS while in FIPS mode because otherwise NSS database won't be
+            # configured in a FIPS compliant way
+            if is_fips():
+                instance.enable_tls()
             log.info("Instance with parameters {} was created.".format(args_instance))
 
     if "standalone1" in instances and len(instances) == 1:
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index b270784ce..5ba0c6676 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -1430,3 +1430,16 @@ def is_valid_hostname(hostname):
         hostname = hostname[:-1] # strip exactly one dot from the right, if present
     allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
     return all(allowed.match(x) for x in hostname.split("."))
+
+
+def is_fips():
+    if os.path.exists('/proc/sys/crypto/fips_enabled'):
+        with open('/proc/sys/crypto/fips_enabled', 'r') as f:
+            state = f.readline().strip()
+            if state == '1':
+                return True
+            else:
+                return False
+    else:
+        return False
+
-- 
2.31.1