From 871172fa453bdd64e60ffc1228bd2dc739e36131 Mon Sep 17 00:00:00 2001 From: William Brown Date: Tue, 14 Nov 2017 16:53:53 +1000 Subject: [PATCH 8/9] Ticket 49218 - Certmap - support TLS tests Bug Description: This adds support for pluggable certificate mapping libraries. To achieve this, this replaces the existing baked in certificate mapping code. Fix Description: Improve our tls tests to cover more cases, support external signing cas, user certs, and addition of TLS by default to tests. https://pagure.io/389-ds-base/issue/49218 https://pagure.io/lib389/issue/95 https://pagure.io/lib389/issue/84 Author: wibrown Review by: ??? --- rpm/389-ds-base.spec.in | 8 +- src/lib389/lib389/__init__.py | 96 +++++++---- src/lib389/lib389/_mapped_object.py | 34 ++-- src/lib389/lib389/idm/user.py | 10 ++ src/lib389/lib389/instance/setup.py | 46 ++++-- src/lib389/lib389/nss_ssl.py | 232 ++++++++++++++++++++------- src/lib389/lib389/plugins_v4.py | 62 +++++++ src/lib389/lib389/tests/nss_ssl_test.py | 68 ++++---- src/lib389/lib389/tests/tls_external_test.py | 143 +++++++++++++---- src/lib389/lib389/topologies.py | 4 +- 10 files changed, 515 insertions(+), 188 deletions(-) create mode 100644 src/lib389/lib389/plugins_v4.py diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in index d16ab5a..0a51b6b 100644 --- a/rpm/389-ds-base.spec.in +++ b/rpm/389-ds-base.spec.in @@ -232,9 +232,10 @@ Summary: A library for accessing, testing, and configuring the 389 Directory Se BuildArch: noarch Group: Development/Libraries Requires: krb5-workstation -Requires: krb5-server Requires: openssl Requires: iproute +# This is for /usr/sbin/cacertdir_rehash +Requires: authconfig Requires: python%{python3_pkgversion} Requires: python%{python3_pkgversion}-pytest Requires: python%{python3_pkgversion}-pyldap @@ -251,8 +252,9 @@ This module contains tools and libraries for accessing, testing, Summary: The lib389 Continuous Integration Tests Group: Development/Libraries BuildArch: noarch -Requires: python%{python3_pkgversion} -Requires: python%{python3_pkgversion}-lib389 +Requires: krb5-server +Requires: python%{python3_pkgversion} +Requires: python%{python3_pkgversion}-lib389 %description -n python%{python3_pkgversion}-%{srcname}-tests The lib389 CI tests that can be run against the Directory Server. diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py index 8a49df4..f292891 100644 --- a/src/lib389/lib389/__init__.py +++ b/src/lib389/lib389/__init__.py @@ -83,6 +83,7 @@ from lib389.utils import ( ensure_bytes, ensure_str) from lib389.paths import Paths +from lib389.nss_ssl import NssSsl # mixin # from lib389.tools import DirSrvTools @@ -292,7 +293,6 @@ class DirSrv(SimpleLDAPObject, object): def __add_brookers__(self): from lib389.config import Config from lib389.aci import Aci - from lib389.nss_ssl import NssSsl from lib389.config import RSA from lib389.config import Encryption from lib389.dirsrv_log import DirsrvAccessLog, DirsrvErrorLog @@ -334,7 +334,6 @@ class DirSrv(SimpleLDAPObject, object): self.mappingtrees = MappingTrees(self) self.replicas = Replicas(self) self.aci = Aci(self) - self.nss_ssl = NssSsl(self) self.rsa = RSA(self) self.encryption = Encryption(self) self.ds_access_log = DirsrvAccessLog(self) @@ -521,7 +520,7 @@ class DirSrv(SimpleLDAPObject, object): (self.sslport or self.port))) - def openConnection(self, *args, **kwargs): + def clone(self, args_instance={}): """ Open a new connection to our LDAP server *IMPORTANT* @@ -537,7 +536,6 @@ class DirSrv(SimpleLDAPObject, object): args_instance[SER_SERVERID_PROP] = self.serverid args_standalone = args_instance.copy() server.allocate(args_standalone) - server.open(*args, **kwargs) return server @@ -817,6 +815,10 @@ class DirSrv(SimpleLDAPObject, object): log.error("Can't find file: %r, removing extension" % prog) prog = prog[:-3] + # We need to tuck the SSLport away because we need to connect + hidden_ssl_port = self.sslport + self.sslport = None + # Create and extract a service keytab args = {SER_HOST: self.host, SER_PORT: self.port, @@ -851,7 +853,33 @@ class DirSrv(SimpleLDAPObject, object): (self.prefix, self.serverid)) self.restart() - # Restart the instance + # Now we need to connect and setup tls + self.open() + + # If it doesn't exist, create a cadb. + ssca_path = os.path.join(self.get_sysconf_dir(), 'dirsrv/ssca/') + ssca = NssSsl(dbpath=ssca_path) + if not ssca._db_exists(): + ssca.reinit() + ssca.create_rsa_ca() + + # Create certificate database. + tlsdb = NssSsl(dbpath=self.get_cert_dir()) + # Remember, DS breaks the db, so force init it. + tlsdb.reinit() + csr = tlsdb.create_rsa_key_and_csr() + (ca, crt) = ssca.rsa_ca_sign_csr(csr) + tlsdb.import_rsa_crt(ca, crt) + + # Setup the config + self.rsa.create() + self.config.set('nsslapd-secureport', '%s' % hidden_ssl_port) + self.config.set('nsslapd-security', 'on') + # Finally, unstash the sslport. + self.sslport = hidden_ssl_port + # Restart the instance + self.restart(post_open=False) + def _createPythonDirsrv(self, version): """ @@ -881,6 +909,8 @@ class DirSrv(SimpleLDAPObject, object): slapd_options.verify() slapd = slapd_options.collect() + print(slapd) + # In order to work by "default" for tests, we need to create a backend. userroot = { 'cn': 'userRoot', @@ -1022,60 +1052,57 @@ class DirSrv(SimpleLDAPObject, object): @raise LDAPError ''' - ################## - # WARNING: While you have a python ldap connection open some settings like - # ldap.set_option MAY NOT WORK AS YOU EXPECT. - # There are cases (especially CACERT/USERCERTS) where when one connection - # is open set_option SILENTLY fails!!!! - # - # You MAY need to set post_open=False in your DirSrv start/restart instance! - ################## + # Force our state offline to prevent paths from trying to search + # cn=config while we startup. + self.state = DIRSRV_STATE_OFFLINE uri = self.toLDAPURL() + log.debug('Connecting to %s' % uri) + + if self.verbose: + self.log.info('open(): Connecting to uri %s' % uri) + if hasattr(ldap, 'PYLDAP_VERSION') and MAJOR >= 3: + super(DirSrv, self).__init__(uri, bytes_mode=False, trace_level=TRACE_LEVEL) + else: + super(DirSrv, self).__init__(uri, trace_level=TRACE_LEVEL) if certdir is None and self.isLocal: certdir = self.get_cert_dir() log.debug("Using dirsrv ca certificate %s" % certdir) + if certdir is not None: + """ + We have a certificate directory, so lets start up TLS negotiations + """ + # Note this sets LDAP.OPT not SELF. Because once self has opened + # it can NOT change opts AT ALL. + self.set_option(ldap.OPT_X_TLS_CACERTDIR, ensure_str(certdir)) + log.debug("Using external ca certificate %s" % certdir) + if userkey is not None: # Note this sets LDAP.OPT not SELF. Because once self has opened # it can NOT change opts AT ALL. - ldap.set_option(ldap.OPT_X_TLS_KEYFILE, ensure_str(userkey)) + self.set_option(ldap.OPT_X_TLS_KEYFILE, ensure_str(userkey)) log.debug("Using user private key %s" % userkey) if usercert is not None: # Note this sets LDAP.OPT not SELF. Because once self has opened # it can NOT change opts AT ALL. - ldap.set_option(ldap.OPT_X_TLS_CERTFILE, ensure_str(usercert)) + self.set_option(ldap.OPT_X_TLS_CERTFILE, ensure_str(usercert)) log.debug("Using user certificate %s" % usercert) - if certdir is not None: - """ - We have a certificate directory, so lets start up TLS negotiations - """ - # Note this sets LDAP.OPT not SELF. Because once self has opened - # it can NOT change opts AT ALL. - ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, ensure_str(certdir)) - log.debug("Using external ca certificate %s" % certdir) - if certdir or starttls: try: # Note this sets LDAP.OPT not SELF. Because once self has opened # it can NOT change opts on reused (ie restart) - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, reqcert) + self.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, reqcert) log.debug("Using certificate policy %s" % reqcert) log.debug("ldap.OPT_X_TLS_REQUIRE_CERT = %s" % reqcert) except ldap.LDAPError as e: log.fatal('TLS negotiation failed: %s' % str(e)) raise e - ## NOW INIT THIS. This MUST be after all the ldap.OPT set above, - # so that we inherit the settings correctly!!!! - if self.verbose: - self.log.info('open(): Connecting to uri %s' % uri) - if hasattr(ldap, 'PYLDAP_VERSION') and MAJOR >= 3: - super(DirSrv, self).__init__(uri, bytes_mode=False, trace_level=TRACE_LEVEL) - else: - super(DirSrv, self).__init__(uri, trace_level=TRACE_LEVEL) + # Tell python ldap to make a new TLS context with this information. + self.set_option(ldap.OPT_X_TLS_NEWCTX, 0) if starttls and not uri.startswith('ldaps'): self.start_tls_s() @@ -1621,6 +1648,9 @@ class DirSrv(SimpleLDAPObject, object): def get_sysconf_dir(self): return self.ds_paths.sysconf_dir + def get_ssca_dir(self): + return os.path.join(self.ds_paths.sysconf_dir, 'dirsrv/ssca') + def get_initconfig_dir(self): return self.ds_paths.initconfig_dir diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py index 4e5bc72..cfd1b2b 100644 --- a/src/lib389/lib389/_mapped_object.py +++ b/src/lib389/lib389/_mapped_object.py @@ -13,7 +13,7 @@ import logging from functools import partial from lib389._entry import Entry -from lib389._constants import DIRSRV_STATE_ONLINE +from lib389._constants import DIRSRV_STATE_ONLINE, SER_ROOT_DN, SER_ROOT_PW from lib389.utils import ( ensure_bytes, ensure_str, ensure_int, ensure_list_bytes, ensure_list_str, ensure_list_int @@ -537,9 +537,9 @@ class DSLdapObject(DSLogging): :returns: Connection with a binding as the entry """ - conn = self._instance.openConnection(*args, **kwargs) - conn.simple_bind_s(self.dn, password) - return conn + inst_clone = self._instance.clone({SER_ROOT_DN: self.dn, SER_ROOT_PW: password}) + inst_clone.open(*args, **kwargs) + return inst_clone # Modifies the DN of an entry to the new fqdn provided def rename(self, new_rdn, newsuperior=None): @@ -631,7 +631,9 @@ class DSLdapObject(DSLogging): if basedn is None: raise ldap.UNWILLING_TO_PERFORM('Invalid request to create. basedn cannot be None') - if properties.get(self._rdn_attribute, None) is not None: + if rdn is not None: + tdn = ensure_str('%s,%s' % (rdn, basedn)) + elif properties.get(self._rdn_attribute, None) is not None: # Favour the value in the properties dictionary v = properties.get(self._rdn_attribute) rdn = ensure_str(v[0]) @@ -846,17 +848,17 @@ class DSLdapObjects(DSLogging): raise ldap.UNWILLING_TO_PERFORM("properties must be a dictionary") # Get the rdn out of the properties if it's unset??? - if rdn is None and self._rdn_attribute in properties: - # First see if we can get it from the properties. - trdn = properties.get(self._rdn_attribute) - if type(trdn) == str: - rdn = trdn - elif type(trdn) == list and len(trdn) != 1: - raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from properties. Too many choices" % (self._rdn_attribute)) - elif type(trdn) == list: - rdn = trdn[0] - else: - raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from properties, Invalid type" % type(trdn)) + # if rdn is None and self._rdn_attribute in properties: + # # First see if we can get it from the properties. + # trdn = properties.get(self._rdn_attribute) + # if type(trdn) == str: + # rdn = "%s=%s" % (self._rdn_attribute, trdn) + # elif type(trdn) == list and len(trdn) != 1: + # raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from properties. Too many choices" % (self._rdn_attribute)) + # elif type(trdn) == list: + # rdn = "%s=%s" % (self._rdn_attribute, trdn[0]) + # else: + # raise ldap.UNWILLING_TO_PERFORM("Cannot determine rdn %s from properties, Invalid type" % type(trdn)) return (rdn, properties) diff --git a/src/lib389/lib389/idm/user.py b/src/lib389/lib389/idm/user.py index 5c3f55f..e4228c9 100644 --- a/src/lib389/lib389/idm/user.py +++ b/src/lib389/lib389/idm/user.py @@ -52,6 +52,7 @@ class UserAccount(Account): self._create_objectclasses.append('inetUser') else: self._create_objectclasses.append('nsMemberOf') + self._create_objectclasses.append('nsAccount') user_compare_exclude = [ 'nsUniqueId', 'modifyTimestamp', @@ -67,6 +68,15 @@ class UserAccount(Account): return super(UserAccount, self)._validate(rdn, properties, basedn) + def enroll_certificate(self, der_path): + if ds_is_older('1.3.7'): + raise Exception("This version of DS does not support nsAccount") + # Given a cert path, add this to the object as a userCertificate + crt = None + with open(der_path, 'rb') as f: + crt = f.read() + self.add('usercertificate;binary', crt) + # Add a set password function.... # Can't I actually just set, and it will hash? diff --git a/src/lib389/lib389/instance/setup.py b/src/lib389/lib389/instance/setup.py index 55b7b6c..a56d610 100644 --- a/src/lib389/lib389/instance/setup.py +++ b/src/lib389/lib389/instance/setup.py @@ -21,6 +21,8 @@ from lib389._constants import * from lib389.properties import * from lib389.passwd import password_hash, password_generate +from lib389.nss_ssl import NssSsl + from lib389.configurations import get_config from lib389.instance.options import General2Base, Slapd2Base @@ -267,10 +269,9 @@ class SetupDs(object): assert(slapd['port'] is not None) assert(socket_check_open('::1', slapd['port']) is False) - ## This causes some problems in tests :( - # assert(slapd['secure_port'] is not None) - if slapd['secure_port'] is not None: - assert(socket_check_open('::1', slapd['secure_port']) is False) + # We enable secure port by default. + assert(slapd['secure_port'] is not None) + assert(socket_check_open('::1', slapd['secure_port']) is False) if self.verbose: self.log.info("PASSED: network avaliability checking") @@ -357,7 +358,13 @@ class SetupDs(object): srcfile = os.path.join(slapd['sysconf_dir'], 'dirsrv/config/slapd-collations.conf') dstfile = os.path.join(slapd['config_dir'], 'slapd-collations.conf') shutil.copy2(srcfile, dstfile) - os.chown(slapd['schema_dir'], slapd['user_uid'], slapd['group_gid']) + os.chown(dstfile, slapd['user_uid'], slapd['group_gid']) + + # Copy in the certmap configuration + srcfile = os.path.join(slapd['sysconf_dir'], 'dirsrv/config/certmap.conf') + dstfile = os.path.join(slapd['config_dir'], 'certmap.conf') + shutil.copy2(srcfile, dstfile) + os.chown(dstfile, slapd['user_uid'], slapd['group_gid']) # If we are on the correct platform settings, systemd if general['systemd'] and not self.containerised: @@ -423,9 +430,21 @@ class SetupDs(object): ds_instance.allocate(args) # Does this work? assert(ds_instance.exists()) - # Create the nssdb - assert(ds_instance.nss_ssl.reinit()) - # Do we want to selfsign a CA and cert? + + # If it doesn't exist, create a cadb. + ssca_path = os.path.join(slapd['sysconf_dir'], 'dirsrv/ssca/') + ssca = NssSsl(dbpath=ssca_path) + if not ssca._db_exists(): + ssca.reinit() + ssca.create_rsa_ca() + + # Create certificate database. + tlsdb = NssSsl(dbpath=slapd['cert_dir']) + if not tlsdb._db_exists(): + tlsdb.reinit() + csr = tlsdb.create_rsa_key_and_csr() + (ca, crt) = ssca.rsa_ca_sign_csr(csr) + tlsdb.import_rsa_crt(ca, crt) ## LAST CHANCE, FIX PERMISSIONS. # Selinux fixups? @@ -443,6 +462,12 @@ class SetupDs(object): base_config_inst = base_config(ds_instance) base_config_inst.apply_config(install=True) + # Setup TLS with the instance. + ### FUTURE: This should be part of template.dse.ldif + ds_instance.rsa.create() + ds_instance.config.set('nsslapd-secureport', '%s' % slapd['secure_port']) + ds_instance.config.set('nsslapd-security', 'on') + # Create the backends as listed # Load example data if needed. for backend in backends: @@ -455,7 +480,10 @@ class SetupDs(object): ds_instance.config.set('nsslapd-rootpw', ensure_str(slapd['root_password'])) - # In a container build we need to stop DirSrv at the end if self.containerised: + # In a container build we need to stop DirSrv at the end ds_instance.stop() + else: + # Restart for changes to take effect - this could be removed later + ds_instance.restart(post_open=False) diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py index 90e78f9..b799edf 100644 --- a/src/lib389/lib389/nss_ssl.py +++ b/src/lib389/lib389/nss_ssl.py @@ -15,6 +15,9 @@ import random import string import re import socket +import time +import shutil +import logging # from nss import nss from subprocess import check_call, check_output from lib389.passwd import password_generate @@ -27,25 +30,28 @@ CERT_NAME = 'Server-Cert' USER_PREFIX = 'user-' PIN_TXT = 'pin.txt' PWD_TXT = 'pwdfile.txt' -ISSUER = 'CN=ca.lib389.example.com,O=testing,L=lib389,ST=Queensland,C=AU' -SELF_ISSUER = 'CN={HOSTNAME},O=testing,L=lib389,ST=Queensland,C=AU' +CERT_SUFFIX = 'O=testing,L=389ds,ST=Queensland,C=AU' +ISSUER = 'CN=ssca.389ds.example.com,%s' % CERT_SUFFIX +SELF_ISSUER = 'CN={HOSTNAME},%s' % CERT_SUFFIX VALID = 2 +# My logger +log = logging.getLogger(__name__) class NssSsl(object): - def __init__(self, dirsrv, dbpassword=None): + def __init__(self, dirsrv=None, dbpassword=None, dbpath=None): self.dirsrv = dirsrv - self.log = self.dirsrv.log + self._certdb = dbpath + if self._certdb is None: + self._certdb = self.dirsrv.get_cert_dir() + self.log = log + if self.dirsrv is not None: + self.log = self.dirsrv.log if dbpassword is None: self.dbpassword = password_generate() else: self.dbpassword = dbpassword - @property - def _certdb(self): - # return "sql:%s" % self.dirsrv.get_cert_dir() - return self.dirsrv.get_cert_dir() - def _generate_noise(self, fpath): noise = password_generate(256) with open(fpath, 'w') as f: @@ -60,38 +66,44 @@ class NssSsl(object): for f in ('key3.db', 'cert8.db', 'key4.db', 'cert9.db', 'secmod.db', 'pkcs11.txt'): try: # Perhaps we should be backing these up instead ... - os.remove("%s/%s" % (self.dirsrv.get_cert_dir(), f )) + os.remove("%s/%s" % (self._certdb, f )) except: pass + try: + os.makedirs(self._certdb) + except FileExistsError: + pass + # In the future we may add the needed option to avoid writing the pin # files. # Write the pin.txt, and the pwdfile.txt - if not os.path.exists('%s/%s' % (self.dirsrv.get_cert_dir(), PIN_TXT)): - with open('%s/%s' % (self.dirsrv.get_cert_dir(), PIN_TXT), 'w') as f: + if not os.path.exists('%s/%s' % (self._certdb, PIN_TXT)): + with open('%s/%s' % (self._certdb, PIN_TXT), 'w') as f: f.write('Internal (Software) Token:%s' % self.dbpassword) - if not os.path.exists('%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT)): - with open('%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), 'w') as f: + if not os.path.exists('%s/%s' % (self._certdb, PWD_TXT)): + with open('%s/%s' % (self._certdb, PWD_TXT), 'w') as f: f.write('%s' % self.dbpassword) # Init the db. # 48886; This needs to be sql format ... - cmd = ['/usr/bin/certutil', '-N', '-d', self._certdb, '-f', '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT)] - self.dirsrv.log.debug("nss cmd: %s" % cmd) - result = check_output(cmd) - self.dirsrv.log.debug("nss output: %s" % result) + cmd = ['/usr/bin/certutil', '-N', '-d', self._certdb, '-f', '%s/%s' % (self._certdb, PWD_TXT)] + self._generate_noise('%s/noise.txt' % self._certdb) + self.log.debug("nss cmd: %s" % cmd) + result = ensure_str(check_output(cmd)) + self.log.debug("nss output: %s" % result) return True def _db_exists(self): """ Check that a nss db exists at the certpath """ - key3 = os.path.exists("%s/key3.db" % (self.dirsrv.get_cert_dir())) - cert8 = os.path.exists("%s/cert8.db" % (self.dirsrv.get_cert_dir())) - key4 = os.path.exists("%s/key4.db" % (self.dirsrv.get_cert_dir())) - cert9 = os.path.exists("%s/cert9.db" % (self.dirsrv.get_cert_dir())) - secmod = os.path.exists("%s/secmod.db" % (self.dirsrv.get_cert_dir())) - pkcs11 = os.path.exists("%s/pkcs11.txt" % (self.dirsrv.get_cert_dir())) + key3 = os.path.exists("%s/key3.db" % (self._certdb)) + cert8 = os.path.exists("%s/cert8.db" % (self._certdb)) + key4 = os.path.exists("%s/key4.db" % (self._certdb)) + cert9 = os.path.exists("%s/cert9.db" % (self._certdb)) + secmod = os.path.exists("%s/secmod.db" % (self._certdb)) + pkcs11 = os.path.exists("%s/pkcs11.txt" % (self._certdb)) if ((key3 and cert8 and secmod) or (key4 and cert9 and pkcs11)): return True @@ -102,8 +114,10 @@ class NssSsl(object): Create a self signed CA. """ + # Wait a second to avoid an NSS bug with serial ids based on time. + time.sleep(1) # Create noise. - self._generate_noise('%s/noise.txt' % self.dirsrv.get_cert_dir()) + self._generate_noise('%s/noise.txt' % self._certdb) # Now run the command. Can we do this with NSS native? cmd = [ '/usr/bin/certutil', @@ -122,12 +136,12 @@ class NssSsl(object): '-d', self._certdb, '-z', - '%s/noise.txt' % self.dirsrv.get_cert_dir(), + '%s/noise.txt' % self._certdb, '-f', - '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), + '%s/%s' % (self._certdb, PWD_TXT), ] - result = check_output(cmd) - self.dirsrv.log.debug("nss output: %s" % result) + result = ensure_str(check_output(cmd)) + self.log.debug("nss output: %s" % result) # Now extract the CAcert to a well know place. # This allows us to point the cacert dir here and it "just works" cmd = [ @@ -140,10 +154,9 @@ class NssSsl(object): '-a', ] certdetails = check_output(cmd) - with open('%s/ca.crt' % self.dirsrv.get_cert_dir(), 'w') as f: + with open('%s/ca.crt' % self._certdb, 'w') as f: f.write(ensure_str(certdetails)) - if os.path.isfile('/usr/sbin/cacertdir_rehash'): - check_output(['/usr/sbin/cacertdir_rehash', self.dirsrv.get_cert_dir()]) + check_output(['/usr/sbin/cacertdir_rehash', self._certdb]) return True def _rsa_cert_list(self): @@ -153,9 +166,9 @@ class NssSsl(object): '-d', self._certdb, '-f', - '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), + '%s/%s' % (self._certdb, PWD_TXT), ] - result = check_output(cmd) + result = ensure_str(check_output(cmd)) # We can skip the first few lines. They are junk # IE ['', @@ -180,9 +193,9 @@ class NssSsl(object): '-d', self._certdb, '-f', - '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), + '%s/%s' % (self._certdb, PWD_TXT), ] - result = check_output(cmd) + result = ensure_str(check_output(cmd)) lines = result.split('\n')[1:-1] key_list = [] @@ -255,18 +268,20 @@ class NssSsl(object): if len(alt_names) == 0: alt_names.append(socket.gethostname()) - if self.dirsrv.host not in alt_names: + if self.dirsrv and self.dirsrv.host not in alt_names: alt_names.append(self.dirsrv.host) + # Wait a second to avoid an NSS bug with serial ids based on time. + time.sleep(1) # Create noise. - self._generate_noise('%s/noise.txt' % self.dirsrv.get_cert_dir()) + self._generate_noise('%s/noise.txt' % self._certdb) cmd = [ '/usr/bin/certutil', '-S', '-n', CERT_NAME, '-s', - SELF_ISSUER.format(HOSTNAME=self.dirsrv.host), + SELF_ISSUER.format(HOSTNAME=alt_names[0]), # We MUST issue with SANs else ldap wont verify the name. '-8', ','.join(alt_names), '-c', @@ -280,21 +295,113 @@ class NssSsl(object): '-d', self._certdb, '-z', - '%s/noise.txt' % self.dirsrv.get_cert_dir(), + '%s/noise.txt' % self._certdb, '-f', - '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), + '%s/%s' % (self._certdb, PWD_TXT), ] - result = check_output(cmd) - self.dirsrv.log.debug("nss output: %s" % result) + result = ensure_str(check_output(cmd)) + self.log.debug("nss output: %s" % result) return True + def create_rsa_key_and_csr(self, alt_names=[]): + csr_path = os.path.join(self._certdb, '%s.csr' % CERT_NAME) + + if len(alt_names) == 0: + alt_names.append(socket.gethostname()) + if self.dirsrv and self.dirsrv.host not in alt_names: + alt_names.append(self.dirsrv.host) + + # Wait a second to avoid an NSS bug with serial ids based on time. + time.sleep(1) + # Create noise. + self._generate_noise('%s/noise.txt' % self._certdb) + + check_call([ + '/usr/bin/certutil', + '-R', + '-s', + SELF_ISSUER.format(HOSTNAME=alt_names[0]), + # We MUST issue with SANs else ldap wont verify the name. + '-8', ','.join(alt_names), + '-g', + '%s' % KEYBITS, + '-v', + '%s' % VALID, + '-d', + self._certdb, + '-z', + '%s/noise.txt' % self._certdb, + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + '-a', + '-o', csr_path, + ]) + return csr_path + + def rsa_ca_sign_csr(self, csr_path): + crt_path = 'crt'.join(csr_path.rsplit('csr', 1)) + ca_path = '%s/ca.crt' % self._certdb + + check_call([ + '/usr/bin/certutil', + '-C', + '-d', + self._certdb, + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + '-a', + '-i', csr_path, + '-o', crt_path, + '-c', CA_NAME, + ]) + + return (ca_path, crt_path) + + def import_rsa_crt(self, ca, crt): + shutil.copyfile(ca, '%s/ca.crt' % self._certdb) + check_output(['/usr/sbin/cacertdir_rehash', self._certdb]) + check_call([ + '/usr/bin/certutil', + '-A', + '-n', CA_NAME, + '-t', "CT,,", + '-a', + '-i', '%s/ca.crt' % self._certdb, + '-d', self._certdb, + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + ]) + check_call([ + '/usr/bin/certutil', + '-A', + '-n', CERT_NAME, + '-t', ",,", + '-a', + '-i', crt, + '-d', self._certdb, + '-f', + '%s/%s' % (self._certdb, PWD_TXT), + ]) + check_call([ + '/usr/bin/certutil', + '-V', + '-d', self._certdb, + '-n', CERT_NAME, + '-u', 'V' + ]) + def create_rsa_user(self, name): """ Create a key and cert for a user to authenticate to the directory. Name is the uid of the account, and will become the CN of the cert. """ + if self._rsa_user_exists(name): + return True + + # Wait a second to avoid an NSS bug with serial ids based on time. + time.sleep(1) cmd = [ '/usr/bin/certutil', '-S', @@ -319,20 +426,20 @@ class NssSsl(object): '-d', self._certdb, '-z', - '%s/noise.txt' % self.dirsrv.get_cert_dir(), + '%s/noise.txt' % self._certdb, '-f', - '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), + '%s/%s' % (self._certdb, PWD_TXT), ] - result = check_output(cmd) - self.dirsrv.log.debug("nss output: %s" % result) + result = ensure_str(check_output(cmd)) + self.log.debug("nss output: %s" % result) # Now extract this into PEM files that we can use. # pk12util -o user-william.p12 -d . -k pwdfile.txt -n user-william -W '' check_call([ 'pk12util', '-d', self._certdb, - '-o', '%s/%s%s.p12' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name), - '-k', '%s/%s' % (self.dirsrv.get_cert_dir(), PWD_TXT), + '-o', '%s/%s%s.p12' % (self._certdb, USER_PREFIX, name), + '-k', '%s/%s' % (self._certdb, PWD_TXT), '-n', '%s%s' % (USER_PREFIX, name), '-W', '""' ]) @@ -341,9 +448,9 @@ class NssSsl(object): check_call([ 'openssl', 'pkcs12', - '-in', '%s/%s%s.p12' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name), + '-in', '%s/%s%s.p12' % (self._certdb, USER_PREFIX, name), '-passin', 'pass:""', - '-out', '%s/%s%s.key' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name), + '-out', '%s/%s%s.key' % (self._certdb, USER_PREFIX, name), '-nocerts', '-nodes' ]) @@ -351,21 +458,32 @@ class NssSsl(object): check_call([ 'openssl', 'pkcs12', - '-in', '%s/%s%s.p12' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name), + '-in', '%s/%s%s.p12' % (self._certdb, USER_PREFIX, name), '-passin', 'pass:""', - '-out', '%s/%s%s.crt' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name), + '-out', '%s/%s%s.crt' % (self._certdb, USER_PREFIX, name), '-nokeys', '-clcerts', '-nodes' ]) + # Convert the cert for userCertificate attr + check_call([ + 'openssl', + 'x509', + '-inform', 'PEM', + '-outform', 'DER', + '-in', '%s/%s%s.crt' % (self._certdb, USER_PREFIX, name), + '-out', '%s/%s%s.der' % (self._certdb, USER_PREFIX, name), + ]) + return True def get_rsa_user(self, name): """ Return a dict of information for ca, key and cert paths for the user id """ - ca_path = '%s/ca.crt' % self.dirsrv.get_cert_dir() - key_path = '%s/%s%s.key' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name) - crt_path = '%s/%s%s.crt' % (self.dirsrv.get_cert_dir(), USER_PREFIX, name) - return {'ca': ca_path, 'key': key_path, 'crt': crt_path} + ca_path = '%s/ca.crt' % self._certdb + key_path = '%s/%s%s.key' % (self._certdb, USER_PREFIX, name) + crt_path = '%s/%s%s.crt' % (self._certdb, USER_PREFIX, name) + crt_der_path = '%s/%s%s.der' % (self._certdb, USER_PREFIX, name) + return {'ca': ca_path, 'key': key_path, 'crt': crt_path, 'crt_der_path': crt_der_path} diff --git a/src/lib389/lib389/plugins_v4.py b/src/lib389/lib389/plugins_v4.py new file mode 100644 index 0000000..f42182d --- /dev/null +++ b/src/lib389/lib389/plugins_v4.py @@ -0,0 +1,62 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import copy + +from lib389._mapped_object import DSLdapObjects, DSLdapObject + +class DylibPlugin4(DSLdapObject): + _plugin_properties = { + 'nsslapd-pluginEnabled' : 'off', + 'nsslapd-pluginInitfunc' : 'NONE', + 'nsslapd-pluginPath': 'NONE', + } + + def __init__(self, instance, dn=None, batch=False): + super(DylibPlugin4, self).__init__(instance, dn, batch) + self._rdn_attribute = 'cn' + self._must_attributes = [ + 'cn', + 'nsslapd-pluginPath', + 'nsslapd-pluginInitfunc', + 'nsslapd-pluginEnabled', + ] + self._create_objectclasses = ['top', 'nsDylibPlugin4'] + # We'll mark this protected, and people can just disable the plugins. + self._protected = True + + def enable(self): + self.set('nsslapd-pluginEnabled', 'on') + + def disable(self): + self.set('nsslapd-pluginEnabled', 'off') + + def status(self): + return self.get_attr_val_utf8('nsslapd-pluginEnabled') == 'on' + + def create(self, rdn=None, properties=None, basedn=None): + # When we create plugins, we don't want people to have to consider all + # the little details. Plus, the server during creation needs to be able + # to create these from nothing. + # As a result, all the named plugins carry a default properties + # dictionary that can be used. + + # Copy the plugin internal properties. + internal_properties = copy.deepcopy(self._plugin_properties) + if properties is not None: + internal_properties.update(properties) + return super(DylibPlugin4, self).create(rdn, internal_properties, basedn) + +class CertmapPlugin(DylibPlugin4): + def __init__(self, instance, dn="cn=certmap plugin,cn=plugins,cn=config", batch=False): + super(CertmapPlugin, self).__init__(instance, dn, batch) + + + + + diff --git a/src/lib389/lib389/tests/nss_ssl_test.py b/src/lib389/lib389/tests/nss_ssl_test.py index e1867b6..0a781e2 100644 --- a/src/lib389/lib389/tests/nss_ssl_test.py +++ b/src/lib389/lib389/tests/nss_ssl_test.py @@ -14,6 +14,8 @@ import logging from lib389.topologies import topology_st as topo +from lib389.nss_ssl import NssSsl + DEBUGGING = os.getenv('DEBUGGING', False) if DEBUGGING: @@ -23,52 +25,48 @@ else: log = logging.getLogger(__name__) -def test_nss(topo): +def test_external_ca(): + # If it doesn't exist, create a cadb. + ssca = NssSsl(dbpath='/tmp/lib389-ssca') + ssca.reinit() + ssca.create_rsa_ca() + + # Create certificate database. + tlsdb = NssSsl(dbpath='/tmp/lib389-tlsdb') + tlsdb.reinit() + + csr = tlsdb.create_rsa_key_and_csr() + (ca, crt) = ssca.rsa_ca_sign_csr(csr) + tlsdb.import_rsa_crt(ca, crt) + +def test_nss_server(topo): """ Build a nss db, create a ca, and check that it is correct. """ + inst_db = NssSsl(dirsrv=topo.standalone) + + # Check our instance installed a valid and correct DB + assert(inst_db._db_exists() is True) + assert(inst_db._rsa_key_and_cert_exists() is True) + +def test_nss_ssca_users(topo): + """ + Validate that we can submit user certs to the ds ca for signing. + """ + ssca = NssSsl(dbpath=topo.standalone.get_ssca_dir()) - standalone = topo.standalone - - # This is a trick. The nss db that ships with DS is broken fundamentally. - # THIS ASSUMES old nss format. SQLite will bite us! - for f in ('key3.db', 'cert8.db', 'key4.db', 'cert9.db', 'secmod.db', 'pkcs11.txt'): - try: - os.remove("%s/%s" % (standalone.confdir, f)) - except: - pass - - - # Check if the db exists. Should be false. - assert(standalone.nss_ssl._db_exists() is False) - # Create it. Should work. - assert(standalone.nss_ssl.reinit() is True) - # Check if the db exists. Should be true - assert(standalone.nss_ssl._db_exists() is True) - - # Check if ca exists. Should be false. - assert(standalone.nss_ssl._rsa_ca_exists() is False) - # Create it. Should work. - assert(standalone.nss_ssl.create_rsa_ca() is True) - # Check if ca exists. Should be true - assert(standalone.nss_ssl._rsa_ca_exists() is True) - - # Check if we have a server cert / key. Should be false. - assert(standalone.nss_ssl._rsa_key_and_cert_exists() is False) - # Create it. Should work. - assert(standalone.nss_ssl.create_rsa_key_and_cert() is True) - # Check if server cert and key exist. Should be true. - assert(standalone.nss_ssl._rsa_key_and_cert_exists() is True) + assert(ssca._rsa_ca_exists() is True) # Check making users certs. They should never conflict for user in ('william', 'noriko', 'mark'): - assert(standalone.nss_ssl._rsa_user_exists(user) is False) # Create the user cert - assert(standalone.nss_ssl.create_rsa_user(user) is True) + assert(ssca.create_rsa_user(user) is True) # Assert it exists now - assert(standalone.nss_ssl._rsa_user_exists(user) is True) + assert(ssca._rsa_user_exists(user) is True) + if __name__ == "__main__": CURRENT_FILE = os.path.realpath(__file__) pytest.main("-s -vv %s" % CURRENT_FILE) + diff --git a/src/lib389/lib389/tests/tls_external_test.py b/src/lib389/lib389/tests/tls_external_test.py index 68aaa9d..58b30c9 100644 --- a/src/lib389/lib389/tests/tls_external_test.py +++ b/src/lib389/lib389/tests/tls_external_test.py @@ -8,53 +8,102 @@ # import ldap +import pytest +from lib389 import DirSrv +from lib389.utils import ds_is_older from lib389.topologies import topology_st from lib389.utils import logging +from lib389.nss_ssl import NssSsl, CERT_SUFFIX from lib389.idm.user import UserAccounts -from lib389._constants import DEFAULT_SUFFIX, SECUREPORT_STANDALONE1 +from lib389.plugins_v4 import CertmapPlugin +from lib389.backend import Backends, BACKEND_SAMPLE_ENTRIES +from lib389._constants import DEFAULT_SUFFIX, SECUREPORT_STANDALONE1, INSTALL_LATEST_CONFIG from lib389.config import CertmapLegacy log = logging.getLogger(__name__) +def _check_bind(inst, dn, tls_locs): + + inst_clone = inst.clone() + inst_clone.open(saslmethod='EXTERNAL', + connOnly=True, + certdir=inst.get_ssca_dir(), + userkey=tls_locs['key'], + usercert=tls_locs['crt']) + assert(inst_clone.whoami_s().lower() == "dn: %s" % dn.lower()) + inst_clone.close() + log.debug("PASS: bound as %s" % dn) + +@pytest.mark.skipif(ds_is_older('1.4.0'), reason="Not implemented in 1.4.0") +def test_certmap_plugin_upgrade(topology_st): + standalone = topology_st.standalone + + certmap = CertmapPlugin(standalone) + # Now, reach in and allow deletion! + certmap._protected = False + certmap.delete() + + # Restart the server + standalone.restart() + + # Now assert that the item exists again. + certmap_reload = CertmapPlugin(standalone) + assert certmap_reload.status() == True + +@pytest.mark.skipif(ds_is_older('1.4.0'), reason="Not implemented in 1.4.0") def test_tls_external(topology_st): standalone = topology_st.standalone - # SETUP TLS - standalone.stop() - assert(standalone.nss_ssl.reinit() is True) - assert(standalone.nss_ssl.create_rsa_ca() is True) - assert(standalone.nss_ssl.create_rsa_key_and_cert() is True) + ssca = NssSsl(dbpath=topology_st.standalone.get_ssca_dir()) + assert(ssca._rsa_ca_exists() is True) + + inst_db = NssSsl(dirsrv=topology_st.standalone) + assert(inst_db._db_exists() is True) + assert(inst_db._rsa_key_and_cert_exists() is True) + # Create a user - assert(standalone.nss_ssl.create_rsa_user('testuser') is True) + assert(ssca.create_rsa_user('testuser_a') is True) # Now get the details of where the key and crt are. - tls_locs = standalone.nss_ssl.get_rsa_user('testuser') - # {'ca': ca_path, 'key': key_path, 'crt': crt_path} + testuser_a_tls_locs = ssca.get_rsa_user('testuser_a') + + # This user is added to a different backend to test "without" dncomps + # IE we try to bind to the DN in the cert. + backends = Backends(standalone) + backends.create(properties={ + 'cn': 'certRoot', + 'nsslapd-suffix': CERT_SUFFIX, + BACKEND_SAMPLE_ENTRIES: INSTALL_LATEST_CONFIG + }) - # Start again - standalone.start() + # Manual testing: sudo LDAPTLS_KEY=/opt/dirsrv/etc/dirsrv/ssca/user-testuser_a.key LDAPTLS_CERT=/opt/dirsrv/etc/dirsrv/ssca/user-testuser_a.crt LDAPTLS_CACERT=/opt/dirsrv/etc/dirsrv/ssca/ca.crt ldapwhoami -H ldaps://localhost:63601/ - users = UserAccounts(standalone, DEFAULT_SUFFIX) - user = users.create(properties={ - 'uid': 'testuser', - 'cn' : 'testuser', + c_users = UserAccounts(standalone, CERT_SUFFIX, rdn=None) + testuser_a = c_users.create(rdn='cn=testuser_a', properties={ + 'uid': 'testuser_a', + 'cn' : 'testuser_a', + 'o': 'testing', 'sn' : 'user', - 'uidNumber' : '1000', - 'gidNumber' : '2000', - 'homeDirectory' : '/home/testuser' + 'uidNumber' : '1001', + 'gidNumber' : '2001', + 'homeDirectory' : '/home/testuser_a', + 'nsCertSubjectDn': 'CN=testuser_a,%s' % CERT_SUFFIX, }) + testuser_a.enroll_certificate(testuser_a_tls_locs['crt_der_path']) + + ## REMEMBER the bind logic is in this order: + # If dncomps == None: bind to subject DN in cert + # If dncomps == None, cmapattr set, search object with cmap attr. nsCertSubjectDN + # if dncomps == '', use filter comps ot search an entyr below basedn + # if dncomps == 'attr', use these to construct a DN - standalone.rsa.create() - # Set the secure port and nsslapd-security - standalone.config.set('nsslapd-secureport', '%s' % SECUREPORT_STANDALONE1 ) - standalone.config.set('nsslapd-security', 'on') - standalone.sslport = SECUREPORT_STANDALONE1 # Now turn on the certmap. cm = CertmapLegacy(standalone) certmaps = cm.list() - certmaps['default']['DNComps'] = '' + # This means to use filter comps instead of dn comps + certmaps['default']['DNComps'] = None certmaps['default']['FilterComps'] = ['cn'] certmaps['default']['VerifyCert'] = 'off' cm.set(certmaps) @@ -66,17 +115,43 @@ def test_tls_external(topology_st): standalone.restart(post_open=False) # Now attempt a bind with TLS external - conn = standalone.openConnection(saslmethod='EXTERNAL', connOnly=True, certdir=standalone.get_cert_dir(), userkey=tls_locs['key'], usercert=tls_locs['crt']) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) + + # Change the certmap and try and verify the certificate + certmaps['default']['CmapLdapAttr'] = 'nsCertSubjectDN' + cm.set(certmaps) + standalone.restart(post_open=False) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) + + # Change the basedn and check it works. + certmaps['default']['basedn'] = 'o=testing,l=lib389,st=Queensland,c=AU' + cm.set(certmaps) + standalone.restart(post_open=False) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) + + # Change the certmap and try and verify the certificate + certmaps['default']['VerifyCert'] = 'on' + cm.set(certmaps) + standalone.restart(post_open=False) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) - assert(conn.whoami_s() == "dn: uid=testuser,ou=People,dc=example,dc=com") + # Check filter comps + certmaps['default']['DNComps'] = '' + certmaps['default']['CmapLdapAttr'] = None + cm.set(certmaps) + standalone.restart(post_open=False) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) - # Backup version of the code: - # ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - # ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_locs['ca']) - # ldap.set_option(ldap.OPT_X_TLS_KEYFILE, tls_locs['key']) - # ldap.set_option(ldap.OPT_X_TLS_CERTFILE, tls_locs['crt']) - # conn = ldap.initialize(standalone.toLDAPURL()) + # Change the filter comps to check o and cn + certmaps['default']['FilterComps'] = ['cn', 'o'] + cm.set(certmaps) + standalone.restart(post_open=False) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) - # sasl_auth = ldap.sasl.external() - # conn.sasl_interactive_bind_s("", sasl_auth) + # Finally, check the dncomps + # The order of these matter as they are extract and appended in *this* order. + certmaps['default']['DNComps'] = ['cn', 'o', 'l', 'st', 'c'] + cm.set(certmaps) + standalone.restart(post_open=False) + _check_bind(standalone, 'cn=testuser_a,%s' % CERT_SUFFIX, testuser_a_tls_locs) diff --git a/src/lib389/lib389/topologies.py b/src/lib389/lib389/topologies.py index 69c71ee..9a23f73 100644 --- a/src/lib389/lib389/topologies.py +++ b/src/lib389/lib389/topologies.py @@ -16,7 +16,7 @@ from lib389 import DirSrv from lib389.utils import generate_ds_params from lib389.replica import Replicas from lib389._constants import (args_instance, SER_HOST, SER_PORT, SER_SERVERID_PROP, SER_CREATION_SUFFIX, - ReplicaRole, DEFAULT_SUFFIX, REPLICA_ID) + SER_SECURE_PORT, ReplicaRole, DEFAULT_SUFFIX, REPLICA_ID) DEBUGGING = os.getenv('DEBUGGING', default=False) if DEBUGGING: @@ -61,6 +61,7 @@ def create_topology(topo_dict): # the instance creation here. args_instance[SER_HOST] = instance_data[SER_HOST] args_instance[SER_PORT] = instance_data[SER_PORT] + args_instance[SER_SECURE_PORT] = instance_data[SER_SECURE_PORT] args_instance[SER_SERVERID_PROP] = instance_data[SER_SERVERID_PROP] args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX args_copied = args_instance.copy() @@ -328,6 +329,7 @@ def topology_m1h1c1(request): instance = DirSrv(verbose=False) args_instance[SER_HOST] = instance_data[SER_HOST] args_instance[SER_PORT] = instance_data[SER_PORT] + args_instance[SER_SECURE_PORT] = instance_data[SER_SECURE_PORT] args_instance[SER_SERVERID_PROP] = instance_data[SER_SERVERID_PROP] args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX args_copied = args_instance.copy() -- 1.8.3.1