From 3603e5465ee35cae3dadd176bf1391d9ee5164d4 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Tue, 3 Dec 2024 18:26:38 -0800 Subject: [PATCH 1/2] Issue 6269 - RFE - Add nsslapd-pwdPBKDF2Rounds configuration to PBKDF2-* plugins Description: Add nsslapd-pwdPBKDF2Rounds attribute that can be configured in PBKDF2-* password storage plugin entries. This was password hashing round value can be adjusted. Certain compliance requirements (like from BSI) require specific hashing round values greater than what we currently provide. Add CLI, Web UI option, and CI tests. Fixes: https://github.com/389ds/389-ds-base/issues/6269 Reviewed by: ? --- .../suites/openldap_2_389/migrate_hdb_test.py | 1 - .../openldap_2_389/migrate_memberof_test.py | 1 - .../openldap_2_389/migrate_monitor_test.py | 1 - .../suites/openldap_2_389/migrate_test.py | 1 - .../password/pbkdf2_upgrade_plugin_test.py | 8 +- .../tests/suites/pwp_storage/storage_test.py | 301 ++++++++++++- ldap/ldif/template-dse-minimal.ldif.in | 4 + ldap/ldif/template-dse.ldif.in | 4 + ldap/schema/01core389.ldif | 2 + ldap/servers/slapd/config.c | 1 + ldap/servers/slapd/fedse.c | 3 + .../src/lib/database/globalPwp.jsx | 190 +++++++- .../389-console/src/lib/server/settings.jsx | 2 + src/lib389/lib389/cli_conf/plugin.py | 2 + .../lib389/cli_conf/plugins/pwstorage.py | 78 ++++ src/lib389/lib389/password_plugins.py | 74 +++- src/plugins/pwdchan/src/lib.rs | 409 +++++++++++++++--- src/plugins/pwdchan/src/pbkdf2.rs | 44 -- src/plugins/pwdchan/src/pbkdf2_sha1.rs | 44 -- src/plugins/pwdchan/src/pbkdf2_sha256.rs | 43 -- src/plugins/pwdchan/src/pbkdf2_sha512.rs | 43 -- src/slapi_r_plugin/src/error.rs | 2 + src/slapi_r_plugin/src/pblock.rs | 4 +- 23 files changed, 1004 insertions(+), 258 deletions(-) create mode 100644 src/lib389/lib389/cli_conf/plugins/pwstorage.py delete mode 100644 src/plugins/pwdchan/src/pbkdf2.rs delete mode 100644 src/plugins/pwdchan/src/pbkdf2_sha1.rs delete mode 100644 src/plugins/pwdchan/src/pbkdf2_sha256.rs delete mode 100644 src/plugins/pwdchan/src/pbkdf2_sha512.rs diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py index 95597898e6..da67ed244c 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_hdb_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py index 4092bb36df..e5f749c01a 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_memberof_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py index bf056f0e05..6935900a55 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_monitor_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py b/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py index 492c94fc8f..fa41a9daf3 100644 --- a/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py +++ b/dirsrvtests/tests/suites/openldap_2_389/migrate_test.py @@ -9,7 +9,6 @@ import pytest import os from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin from lib389.utils import ds_is_older from lib389.migrate.openldap.config import olConfig from lib389.migrate.openldap.config import olOverlayType diff --git a/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py b/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py index 90dae36ec7..65c9bed939 100644 --- a/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py +++ b/dirsrvtests/tests/suites/password/pbkdf2_upgrade_plugin_test.py @@ -8,7 +8,7 @@ # import pytest from lib389.topologies import topology_st -from lib389.password_plugins import PBKDF2Plugin +from lib389.password_plugins import PBKDF2SHA512Plugin from lib389.utils import ds_is_older pytestmark = pytest.mark.tier1 @@ -35,18 +35,18 @@ def test_pbkdf2_upgrade(topology_st): """ # Remove the pbkdf2 plugin config - p1 = PBKDF2Plugin(topology_st.standalone) + p1 = PBKDF2SHA512Plugin(topology_st.standalone) assert(p1.exists()) p1._protected = False p1.delete() # Restart topology_st.standalone.restart() # check it's been readded. - p2 = PBKDF2Plugin(topology_st.standalone) + p2 = PBKDF2SHA512Plugin(topology_st.standalone) assert(p2.exists()) # Now restart to make sure we still work from the non-bootstrap form topology_st.standalone.restart() - p3 = PBKDF2Plugin(topology_st.standalone) + p3 = PBKDF2SHA512Plugin(topology_st.standalone) assert(p3.exists()) diff --git a/dirsrvtests/tests/suites/pwp_storage/storage_test.py b/dirsrvtests/tests/suites/pwp_storage/storage_test.py index ed0dd9e533..db254cdc73 100644 --- a/dirsrvtests/tests/suites/pwp_storage/storage_test.py +++ b/dirsrvtests/tests/suites/pwp_storage/storage_test.py @@ -18,13 +18,54 @@ from lib389.topologies import topology_st as topo from lib389.idm.user import UserAccounts, UserAccount -from lib389._constants import DEFAULT_SUFFIX +from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD, ErrorLog from lib389.config import Config -from lib389.password_plugins import PBKDF2Plugin, SSHA512Plugin +from lib389.password_plugins import ( + SSHA512Plugin, + PBKDF2SHA1Plugin, + PBKDF2SHA256Plugin, + PBKDF2SHA512Plugin +) from lib389.utils import ds_is_older pytestmark = pytest.mark.tier1 +PBKDF2_SCHEMES = [ + ('PBKDF2-SHA1', PBKDF2SHA1Plugin, 70000), + ('PBKDF2-SHA256', PBKDF2SHA256Plugin, 30000), + ('PBKDF2-SHA512', PBKDF2SHA512Plugin, 10000), +] + + +@pytest.fixture(scope="function") +def new_user(request, topo): + """Fixture to create and clean up a test user for each test""" + # Generate unique user ID based on test name + uid = f'new_user_{request.node.name[:20]}' + + # Create user + users = UserAccounts(topo.standalone, DEFAULT_SUFFIX) + user = users.create(properties={ + 'uid': uid, + 'cn': 'Test User', + 'sn': 'User', + 'uidNumber': '1000', + 'gidNumber': '2000', + 'homeDirectory': f'/home/{uid}' + }) + + def fin(): + try: + # Ensure we're bound as DM before cleanup + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + if user.exists(): + user.delete() + except Exception as e: + log.error(f"Error during user cleanup: {e}") + + request.addfinalizer(fin) + return user + def user_config(topo, field_value): """ @@ -62,6 +103,248 @@ def test_check_password_scheme(topo, value): user.delete() +@pytest.mark.parametrize('scheme_name,plugin_class,default_rounds', PBKDF2_SCHEMES) +def test_pbkdf2_default_rounds(topo, new_user, scheme_name, plugin_class, default_rounds): + """Test PBKDF2 schemes with default iteration rounds. + + :id: bd58cd76-14f9-4d54-9793-ee7bba8e5369 + :parametrized: yes + :setup: Standalone + :steps: + 1. Remove any existing rounds configuration + 2. Verify default rounds are used + 3. Set password and verify hash format + 4. Test authentication + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + plugin = plugin_class(topo.standalone) + plugin.remove_all('nsslapd-pwdpbkdf2numiterations') + topo.standalone.restart() + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + current_rounds = plugin.get_rounds() + assert current_rounds == default_rounds, \ + f"Expected default {default_rounds} rounds, got {current_rounds}" + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert pwd_hash.startswith('{' + scheme_name.upper() + '}') + assert str(default_rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + + assert topo.standalone.searchErrorsLog( + f'Number of iterations for {scheme_name} password scheme set to {default_rounds} from default' + ) + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + +@pytest.mark.parametrize('scheme_name,plugin_class,default_rounds', PBKDF2_SCHEMES) +def test_pbkdf2_rounds_reset(topo, new_user, scheme_name, plugin_class, default_rounds): + """Test PBKDF2 schemes rounds reset to defaults. + + :id: 59bf95c5-6a07-4db1-81eb-d59b54436826 + :parametrized: yes + :setup: Standalone + :steps: + 1. Set custom rounds for PBKDF2 plugin + 2. Verify custom rounds are used + 3. Remove rounds configuration + 4. Verify defaults are restored + 5. Test password operations with default rounds + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + 5. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + test_rounds = 25000 + plugin = plugin_class(topo.standalone) + plugin.set_rounds(test_rounds) + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == test_rounds, \ + f"Expected {test_rounds} rounds, got {current_rounds}" + + plugin.remove_all('nsslapd-pwdpbkdf2numiterations') + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == default_rounds, \ + f"Expected default {default_rounds} rounds after reset, got {current_rounds}" + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert pwd_hash.startswith('{' + scheme_name.upper() + '}') + assert str(default_rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + + assert topo.standalone.searchErrorsLog( + f'Number of iterations for {scheme_name} password scheme set to {default_rounds} from default' + ) + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + +@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES) +@pytest.mark.parametrize('rounds', [10000, 20000, 50000]) +def test_pbkdf2_custom_rounds(topo, new_user, scheme_name, plugin_class, _, rounds): + """Test PBKDF2 schemes with custom iteration rounds. + + :id: 6bec6542-ed8d-4a0e-89d6-e047757767c2 + :parametrized: yes + :setup: Standalone + :steps: + 1. Set custom rounds for PBKDF2 plugin + 2. Verify rounds are set correctly + 3. Set password and verify hash format + 4. Test authentication + 5. Verify rounds in password hash + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + 5. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + plugin = plugin_class(topo.standalone) + plugin.set_rounds(rounds) + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == rounds, \ + f"Expected {rounds} rounds, got {current_rounds}" + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert pwd_hash.startswith('{' + scheme_name.upper() + '}') + assert str(rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + + assert topo.standalone.searchErrorsLog( + f'Number of iterations for {scheme_name}' + ) + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + +@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES) +def test_pbkdf2_invalid_rounds(topo, scheme_name, plugin_class, _): + """Test PBKDF2 schemes with invalid iteration rounds. + + :id: 4e5b4f37-c97b-4f58-b5c5-726495d9fa4e + :parametrized: yes + :setup: Standalone + :steps: + 1. Try to set invalid rounds (too low and too high) + 2. Verify appropriate errors are raised + 3. Verify original rounds are maintained + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + """ + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + plugin = plugin_class(topo.standalone) + plugin.enable() + + original_rounds = plugin.get_rounds() + + with pytest.raises(ValueError) as excinfo: + plugin.set_rounds(5000) + assert "rounds must be between 10,000 and 10,000,000" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + plugin.set_rounds(20000000) + assert "rounds must be between 10,000 and 10,000,000" in str(excinfo.value) + + current_rounds = plugin.get_rounds() + assert current_rounds == original_rounds, \ + f"Rounds changed from {original_rounds} to {current_rounds}" + + +@pytest.mark.parametrize('scheme_name,plugin_class,_', PBKDF2_SCHEMES) +def test_pbkdf2_rounds_persistence(topo, new_user, scheme_name, plugin_class, _): + """Test PBKDF2 rounds persistence across server restarts. + + :id: b15de1ae-53ac-429f-991b-cea5e6a7b383 + :parametrized: yes + :setup: Standalone + :steps: + 1. Set custom rounds for PBKDF2 plugin + 2. Restart server + 3. Verify rounds are maintained + 4. Set password and verify hash + 5. Test authentication + :expectedresults: + 1. Pass + 2. Pass + 3. Pass + 4. Pass + 5. Pass + """ + try: + # Flush logs + topo.standalone.restart() + topo.standalone.config.loglevel((ErrorLog.DEFAULT, ErrorLog.PLUGIN)) + topo.standalone.deleteErrorLogs() + + test_rounds = 15000 + plugin = plugin_class(topo.standalone) + plugin.set_rounds(test_rounds) + topo.standalone.restart() + + current_rounds = plugin.get_rounds() + assert current_rounds == test_rounds, \ + f"Expected {test_rounds} rounds after restart, got {current_rounds}" + + topo.standalone.config.replace('passwordStorageScheme', scheme_name) + + new_user.set('userPassword', 'Secret123') + pwd_hash = new_user.get_attr_val_utf8('userPassword') + assert str(test_rounds) in pwd_hash + + topo.standalone.simple_bind_s(new_user.dn, 'Secret123') + finally: + topo.standalone.simple_bind_s(DN_DM, PASSWORD) + + def test_clear_scheme(topo): """Check clear password scheme. @@ -106,27 +389,27 @@ def test_check_two_scheme(topo): user.delete() @pytest.mark.skipif(ds_is_older('1.4'), reason="Not implemented") -def test_check_pbkdf2_sha256(topo): - """Check password scheme PBKDF2_SHA256. +def test_check_pbkdf2_sha512(topo): + """Check password scheme PBKDF2-SHA512. :id: 31612e7e-33a6-11ea-a750-8c16451d917b :setup: Standalone :steps: - 1. Try to delete PBKDF2_SHA256. - 2. Should not deleted PBKDF2_SHA256 and server should up. + 1. Try to delete PBKDF2-SHA512. + 2. Should not deleted PBKDF2-SHA512 and server should up. :expectedresults: 1. Pass 2. Pass """ - value = 'PBKDF2_SHA256' + value = 'PBKDF2-SHA512' user = user_config(topo, value) assert '{' + f'{value.lower()}' + '}' in \ UserAccount(topo.standalone, user.dn).get_attr_val_utf8('userpassword').lower() - plg = PBKDF2Plugin(topo.standalone) + plg = PBKDF2SHA512Plugin(topo.standalone) plg._protected = False plg.delete() topo.standalone.restart() - assert Config(topo.standalone).get_attr_val_utf8('passwordStorageScheme') == 'PBKDF2_SHA256' + assert Config(topo.standalone).get_attr_val_utf8('passwordStorageScheme') == 'PBKDF2-SHA512' assert topo.standalone.status() user.delete() diff --git a/ldap/ldif/template-dse-minimal.ldif.in b/ldap/ldif/template-dse-minimal.ldif.in index d2b02f8be9..264cc51e06 100644 --- a/ldap/ldif/template-dse-minimal.ldif.in +++ b/ldap/ldif/template-dse-minimal.ldif.in @@ -195,6 +195,7 @@ nsslapd-pluginenabled: on dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init @@ -208,6 +209,7 @@ nsslapd-pluginDescription: PBKDF2 dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA1 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init @@ -221,6 +223,7 @@ nsslapd-pluginDescription: PBKDF2-SHA1\ dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA256 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init @@ -234,6 +237,7 @@ nsslapd-pluginDescription: PBKDF2-SHA256\ dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA512 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in index ce44d75cd1..ccb736eb86 100644 --- a/ldap/ldif/template-dse.ldif.in +++ b/ldap/ldif/template-dse.ldif.in @@ -252,6 +252,7 @@ nsslapd-pluginenabled: on dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init @@ -265,6 +266,7 @@ nsslapd-pluginDescription: PBKDF2 dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA1 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init @@ -278,6 +280,7 @@ nsslapd-pluginDescription: PBKDF2-SHA1\ dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA256 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init @@ -291,6 +294,7 @@ nsslapd-pluginDescription: PBKDF2-SHA256\ dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config objectclass: top objectclass: nsSlapdPlugin +objectClass: pwdPBKDF2PluginConfig cn: PBKDF2-SHA512 nsslapd-pluginpath: libpwdchan-plugin nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif index c98e5b34b3..bfe8259f82 100644 --- a/ldap/schema/01core389.ldif +++ b/ldap/schema/01core389.ldif @@ -332,6 +332,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2391 NAME 'dsEntryDN' DESC '389 Director attributeTypes: ( 2.16.840.1.113730.3.1.2392 NAME 'nsslapd-return-original-entrydn' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) attributeTypes: ( 2.16.840.1.113730.3.1.2393 NAME 'nsslapd-auditlog-display-attrs' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) attributeTypes: ( 2.16.840.1.113730.3.1.2398 NAME 'nsslapd-haproxy-trusted-ip' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN '389 Directory Server' ) +attributeTypes: ( 2.16.840.1.113730.3.1.2400 NAME 'nsslapd-pwdPBKDF2NumIterations' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Directory Server' ) # # objectclasses # @@ -353,3 +354,4 @@ objectClasses: ( 2.16.840.1.113730.3.2.327 NAME 'rootDNPluginConfig' DESC 'Netsc objectClasses: ( 2.16.840.1.113730.3.2.328 NAME 'nsSchemaPolicy' DESC 'Netscape defined objectclass' SUP top MAY ( cn $ schemaUpdateObjectclassAccept $ schemaUpdateObjectclassReject $ schemaUpdateAttributeAccept $ schemaUpdateAttributeReject) X-ORIGIN 'Netscape Directory Server' ) objectClasses: ( 2.16.840.1.113730.3.2.332 NAME 'nsChangelogConfig' DESC 'Configuration of the changelog5 object' SUP top MUST ( cn $ nsslapd-changelogdir ) MAY ( nsslapd-changelogmaxage $ nsslapd-changelogtrim-interval $ nsslapd-changelogmaxentries $ nsslapd-changelogsuffix $ nsslapd-changelogcompactdb-interval $ nsslapd-encryptionalgorithm $ nsSymmetricKey ) X-ORIGIN '389 Directory Server' ) objectClasses: ( 2.16.840.1.113730.3.2.337 NAME 'rewriterEntry' DESC '' SUP top MUST ( nsslapd-libPath ) MAY ( cn $ nsslapd-filterrewriter $ nsslapd-returnedAttrRewriter ) X-ORIGIN '389 Directory Server' ) +objectClasses: ( 2.16.840.1.113730.3.2.340 NAME 'pwdPBKDF2PluginConfig' DESC 'PBKDF2 Password Storage Plugin configuration' SUP top MAY ( nsslapd-pwdPBKDF2NumIterations ) X-ORIGIN '389 Directory Server' ) diff --git a/ldap/servers/slapd/config.c b/ldap/servers/slapd/config.c index eb8c9a4fee..3db7b7840c 100644 --- a/ldap/servers/slapd/config.c +++ b/ldap/servers/slapd/config.c @@ -43,6 +43,7 @@ static char *bootstrap_plugins[] = { "dn: cn=PBKDF2-SHA512,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2-SHA512\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_sha512_plugin_init\n" diff --git a/ldap/servers/slapd/fedse.c b/ldap/servers/slapd/fedse.c index 74f7e6cf99..85bcf063c8 100644 --- a/ldap/servers/slapd/fedse.c +++ b/ldap/servers/slapd/fedse.c @@ -231,6 +231,7 @@ static const char *internal_entries[] = "dn: cn=PBKDF2,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_plugin_init\n" @@ -244,6 +245,7 @@ static const char *internal_entries[] = "dn: cn=PBKDF2-SHA1,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2-SHA1\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_sha1_plugin_init\n" @@ -257,6 +259,7 @@ static const char *internal_entries[] = "dn: cn=PBKDF2-SHA256,cn=Password Storage Schemes,cn=plugins,cn=config\n" "objectclass: top\n" "objectclass: nsSlapdPlugin\n" + "objectClass: pwdPBKDF2PluginConfig\n" "cn: PBKDF2-SHA256\n" "nsslapd-pluginpath: libpwdchan-plugin\n" "nsslapd-plugininitfunc: pwdchan_pbkdf2_sha256_plugin_init\n" diff --git a/src/cockpit/389-console/src/lib/database/globalPwp.jsx b/src/cockpit/389-console/src/lib/database/globalPwp.jsx index a757361ec3..8681584677 100644 --- a/src/cockpit/389-console/src/lib/database/globalPwp.jsx +++ b/src/cockpit/389-console/src/lib/database/globalPwp.jsx @@ -89,6 +89,16 @@ const tpr_attrs = [ "passwordtprdelayvalidfrom", ]; +const password_storage_attrs = [ + "nsslapd-pwdpbkdf2numiterations" +]; + +const PBKDF2_SCHEMES = ['pbkdf2', 'pbkdf2-sha1', 'pbkdf2-sha256', 'pbkdf2-sha512']; + +const isPBKDF2Scheme = (scheme) => { + return PBKDF2_SCHEMES.includes(scheme.toLowerCase()); +}; + export class GlobalPwPolicy extends React.Component { constructor(props) { super(props); @@ -102,6 +112,7 @@ export class GlobalPwPolicy extends React.Component { // each field, so we can loop over them to efficently // check for changes, and updating/saving the config. saveGeneralDisabled: true, + savePasswordStorageDisabled: true, saveExpDisabled: true, saveLockoutDisabled: true, saveSyntaxDisabled: true, @@ -118,6 +129,8 @@ export class GlobalPwPolicy extends React.Component { this.handleGeneralChange = this.handleGeneralChange.bind(this); this.handleSaveGeneral = this.handleSaveGeneral.bind(this); + this.handlePasswordStorageChange = this.handlePasswordStorageChange.bind(this); + this.handleSavePasswordStorage = this.handleSavePasswordStorage.bind(this); this.handleExpChange = this.handleExpChange.bind(this); this.handleSaveExp = this.handleSaveExp.bind(this); this.handleLockoutChange = this.handleLockoutChange.bind(this); @@ -127,6 +140,7 @@ export class GlobalPwPolicy extends React.Component { this.handleTPRChange = this.handleTPRChange.bind(this); this.handleSaveTPR = this.handleSaveTPR.bind(this); this.handleLoadGlobal = this.handleLoadGlobal.bind(this); + this.handleLoadPasswordStorage = this.handleLoadPasswordStorage.bind(this); // Select Typeahead this.handleSelectToggle = this.handleSelectToggle.bind(this); this.handleSelectClear = this.handleSelectClear.bind(this); @@ -145,6 +159,68 @@ export class GlobalPwPolicy extends React.Component { this.setState({ activeKey: key }); } + handlePasswordStorageChange(e) { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; + const attr = e.target.id.toLowerCase(); + let disableSaveBtn = true; + + for (const password_storage_attr of password_storage_attrs) { + const storageAttr = password_storage_attr.toLowerCase(); + const oldValue = String(this.state['_' + storageAttr] || ''); + const newValue = String(value || ''); + + if (attr === storageAttr && oldValue !== newValue) { + disableSaveBtn = false; + break; + } + } + + this.setState({ + [attr]: value || '', + savePasswordStorageDisabled: disableSaveBtn, + }); + } + + handleSavePasswordStorage() { + if (!isPBKDF2Scheme(this.state.passwordstoragescheme)) { + return; + } + this.setState({ + saving: true + }); + + const cmd = [ + 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + 'plugin', 'pwstorage-scheme', this.state.passwordstoragescheme.toLowerCase(), + 'set-num-iterations', this.state[password_storage_attrs[0]] + ]; + + log_cmd("handleSavePasswordStorage", "Saving password storage settings", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + this.handleLoadGlobal(); + this.setState({ + saving: false + }); + this.props.addNotification( + "success", + _("Successfully updated number of iterations for password storage scheme") + ); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.handleLoadGlobal(); + this.setState({ + saving: false + }); + this.props.addNotification( + "error", + cockpit.format(_("Error updating number of iterations for password storage scheme - $0"), errMsg.desc) + ); + }); + } + handleGeneralChange(e) { const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; const attr = e.target.id; @@ -166,9 +242,18 @@ export class GlobalPwPolicy extends React.Component { } } - this.setState({ + // Create state update object + const stateUpdate = { [attr]: value, saveGeneralDisabled: disableSaveBtn, + }; + + this.setState(stateUpdate, () => { + // If passwordstoragescheme was changed and it's a PBKDF2 scheme, + // load the iterations value + if (attr === 'passwordstoragescheme' && isPBKDF2Scheme(value)) { + this.handleLoadPasswordStorage(true); + } }); } @@ -176,6 +261,15 @@ export class GlobalPwPolicy extends React.Component { this.setState({ saving: true }); + if (!this.state.savePasswordStorageDisabled) { + this.handleSavePasswordStorage(); + } + if (this.state.saveGeneralDisabled) { + this.setState({ + saving: false + }); + return; + } const cmd = [ 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", @@ -594,10 +688,74 @@ export class GlobalPwPolicy extends React.Component { }); } + handleLoadPasswordStorage(skipLoading = false) { + if (!skipLoading) { + this.setState({ + loading: true + }); + } + + if (!isPBKDF2Scheme(this.state.passwordstoragescheme)) { + this.setState({ + loading: false, + 'nsslapd-pwdpbkdf2numiterations': '', + '_nsslapd-pwdpbkdf2numiterations': '' + }); + return; + } + + const cmd = [ + 'dsconf', '-j', "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + 'plugin', 'pwstorage-scheme', this.state.passwordstoragescheme.toLowerCase(), + 'get-num-iterations' + ]; + + log_cmd("handleLoadPasswordStorage", "Load password storage settings", cmd); + cockpit + .spawn(cmd, { superuser: true, err: "message" }) + .done(content => { + const config = JSON.parse(content); + const attrs = config.attrs; + + const stateUpdates = { + 'nsslapd-pwdpbkdf2numiterations': '', + '_nsslapd-pwdpbkdf2numiterations': '' + }; + + if (!skipLoading) { + stateUpdates["loading"] = false + } + password_storage_attrs.forEach(attr => { + const attrLower = attr.toLowerCase(); + const attrValue = attrs[attr] || attrs[attrLower]; + + if (attrValue && attrValue[0]) { + stateUpdates[attrLower] = attrValue[0]; + stateUpdates['_' + attrLower] = attrValue[0]; + } + }); + + this.setState(stateUpdates); + }) + .fail(err => { + const errMsg = JSON.parse(err); + this.setState({ + loading: false, + 'nsslapd-pwdpbkdf2numiterations': '', + '_nsslapd-pwdpbkdf2numiterations': '' + }); + this.props.addNotification( + "error", + cockpit.format(_("Error loading password storage settings - $0"), errMsg.desc) + ); + }); + } + handleLoadGlobal() { this.setState({ loading: true }); + const cmd = [ "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", "config", "get" @@ -700,6 +858,7 @@ export class GlobalPwPolicy extends React.Component { loaded: true, loading: false, saveGeneralDisabled: true, + savePasswordStorageDisabled: true, saveUserDisabled: true, saveExpDisabled: true, saveLockoutDisabled: true, @@ -795,8 +954,10 @@ export class GlobalPwPolicy extends React.Component { _passwordtprmaxuse: attrs.passwordtprmaxuse[0], _passwordtprdelayexpireat: attrs.passwordtprdelayexpireat[0], _passwordtprdelayvalidfrom: attrs.passwordtprdelayvalidfrom[0], - }), this.props.enableTree() - ); + }), () => { + this.props.enableTree(); + this.handleLoadPasswordStorage(); + }); }) .fail(err => { const errMsg = JSON.parse(err); @@ -1385,6 +1546,25 @@ export class GlobalPwPolicy extends React.Component { + {isPBKDF2Scheme(this.state.passwordstoragescheme) && ( + + + {_("PBKDF2 Iterations")} + + + { + this.handlePasswordStorageChange(e); + }} + /> + + + )} @@ -1439,7 +1619,7 @@ export class GlobalPwPolicy extends React.Component {