From 8dd2a1f6eba2087ed722e8b5022995d5e199541c Mon Sep 17 00:00:00 2001 From: William Brown Date: Mon, 27 Nov 2017 14:47:44 +0100 Subject: [PATCH 09/10] 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: ??? --- ldap/schema/30ns-common.ldif | 3 +- 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/tests/nss_ssl_test.py | 68 +++++----- src/lib389/lib389/topologies.py | 4 +- 9 files changed, 346 insertions(+), 155 deletions(-) diff --git a/ldap/schema/30ns-common.ldif b/ldap/schema/30ns-common.ldif index b095909..2b6edc1 100644 --- a/ldap/schema/30ns-common.ldif +++ b/ldap/schema/30ns-common.ldif @@ -53,6 +53,7 @@ attributeTypes: ( nsExecRef-oid NAME 'nsExecRef' DESC 'Netscape defined attribut attributeTypes: ( nsLogSuppress-oid NAME 'nsLogSuppress' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape' ) attributeTypes: ( nsJarfilename-oid NAME 'nsJarfilename' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape' ) attributeTypes: ( nsClassname-oid NAME 'nsClassname' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape' ) +attributeTypes: ( 2.16.840.1.113730.3.1.2337 NAME 'nsCertSubjectDN' DESC 'An x509 DN from a certificate used to map during a TLS bind process' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN '389 Directory Server Project' ) objectClasses: ( nsAdminDomain-oid NAME 'nsAdminDomain' DESC 'Netscape defined objectclass' SUP organizationalUnit MAY ( nsAdminDomainName ) X-ORIGIN 'Netscape' ) objectClasses: ( nsHost-oid NAME 'nsHost' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( serverHostName $ description $ l $ nsHostLocation $ nsHardwarePlatform $ nsOsVersion ) X-ORIGIN 'Netscape' ) objectClasses: ( nsAdminGroup-oid NAME 'nsAdminGroup' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( nsAdminGroupName $ description $ nsConfigRoot $ nsAdminSIEDN ) X-ORIGIN 'Netscape' ) @@ -64,4 +65,4 @@ objectClasses: ( nsAdminObject-oid NAME 'nsAdminObject' DESC 'Netscape defined o objectClasses: ( nsConfig-oid NAME 'nsConfig' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( description $ nsServerPort $ nsServerAddress $ nsSuiteSpotUser $ nsErrorLog $ nsPidLog $ nsAccessLog $ nsDefaultAcceptLanguage $ nsServerSecurity ) X-ORIGIN 'Netscape' ) objectClasses: ( nsDirectoryInfo-oid NAME 'nsDirectoryInfo' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( nsBindDN $ nsBindPassword $ nsDirectoryURL $ nsDirectoryFailoverList $ nsDirectoryInfoRef ) X-ORIGIN 'Netscape' ) objectClasses: ( 2.16.840.1.113730.3.2.329 NAME 'nsMemberOf' DESC 'Allow memberOf assignment on groups for nesting and users' SUP top AUXILIARY MAY ( memberOf ) X-ORIGIN '389 Directory Server Project' ) - +objectClasses: ( 2.16.840.1.113730.3.2.331 NAME 'nsAccount' DESC 'A representation of a user in a directory server' SUP top AUXILIARY MAY ( userCertificate $ nsCertSubjectDN ) X-ORIGIN '389 Directory Server Project' ) 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 b78d8a4..dceb8b6 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 10b787a..c69cf6b 100644 --- a/src/lib389/lib389/idm/user.py +++ b/src/lib389/lib389/idm/user.py @@ -62,6 +62,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', @@ -77,6 +78,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/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/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