From e4ec3e0e6aac21afc0398102c5a0030f368f0add Mon Sep 17 00:00:00 2001 From: Matúš Honěk Date: Jun 25 2019 14:46:05 +0000 Subject: Ticket 50217 - Implement dsconf security section Bug Description: dsconf lacks options to configure security options Fix Description: Implementing options to configure security related attributes and handle ciphers configuration. Fixes: https://pagure.io/389-ds-base/issue/50217 Author: Matus Honek Review by: firstyear, mreynolds (Thanks!) --- diff --git a/src/lib389/cli/dsconf b/src/lib389/cli/dsconf index f815162..c0c0b4d 100755 --- a/src/lib389/cli/dsconf +++ b/src/lib389/cli/dsconf @@ -32,6 +32,7 @@ from lib389.cli_conf import backup as cli_backup from lib389.cli_conf import replication as cli_replication from lib389.cli_conf import chaining as cli_chaining from lib389.cli_conf import conflicts as cli_repl_conflicts +from lib389.cli_conf import security as cli_security from lib389.cli_base import disconnect_instance, connect_instance from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat from lib389.cli_base import setup_script_logger @@ -87,6 +88,7 @@ cli_plugin.create_parser(subparsers) cli_pwpolicy.create_parser(subparsers) cli_replication.create_parser(subparsers) cli_sasl.create_parser(subparsers) +cli_security.create_parser(subparsers) cli_schema.create_parser(subparsers) cli_repl_conflicts.create_parser(subparsers) diff --git a/src/lib389/lib389/cli_conf/security.py b/src/lib389/lib389/cli_conf/security.py new file mode 100644 index 0000000..6d8c1ae --- /dev/null +++ b/src/lib389/lib389/cli_conf/security.py @@ -0,0 +1,244 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2019 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +from collections import OrderedDict, namedtuple +import json + +from lib389.config import Config, Encryption, RSA +from lib389.nss_ssl import NssSsl + + +Props = namedtuple('Props', ['cls', 'attr', 'help', 'values']) + +onoff = ('on', 'off') +protocol_versions = ('SSLv3', 'TLS1.0', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3', '') +SECURITY_ATTRS_MAP = OrderedDict([ + ('security', Props(Config, 'nsslapd-security', + 'Enable or disable security', + onoff)), + ('listen-host', Props(Config, 'nsslapd-securelistenhost', + 'Host/address to listen on for LDAPS', + str)), + ('secure-port', Props(Config, 'nsslapd-securePort', + 'Port for LDAPS to listen on', + range(1, 65536))), + ('tls-client-auth', Props(Config, 'nsSSLClientAuth', + 'Client authentication requirement', + ('off', 'allowed', 'required'))), + ('require-secure-authentication', Props(Config, 'nsslapd-require-secure-binds', + 'Require binds over LDAPS, StartTLS, or SASL', + onoff)), + ('check-hostname', Props(Config, 'nsslapd-ssl-check-hostname', + 'Check Subject of remote certificate against the hostname', + onoff)), + ('verify-cert-chain-on-startup', Props(Config, 'nsslapd-validate-cert', + 'Validate server certificate during startup', + ('warn', *onoff))), + ('session-timeout', Props(Encryption, 'nsSSLSessionTimeout', + 'Secure session timeout', + int)), + ('tls-protocol-min', Props(Encryption, 'sslVersionMin', + 'Secure protocol minimal allowed version', + protocol_versions)), + ('tls-protocol-max', Props(Encryption, 'sslVersionMax', + 'Secure protocol maximal allowed version', + protocol_versions)), + ('allow-insecure-ciphers', Props(Encryption, 'allowWeakCipher', + 'Allow weak ciphers for legacy use', + onoff)), + ('allow-weak-dh-param', Props(Encryption, 'allowWeakDHParam', + 'Allow short DH params for legacy use', + onoff)), +]) + +RSA_ATTRS_MAP = OrderedDict([ + ('tls-allow-rsa-certificates', Props(RSA, 'nsSSLActivation', + 'Activate use of RSA certificates', + onoff)), + ('nss-cert-name', Props(RSA, 'nsSSLPersonalitySSL', + 'Server certificate name in NSS DB', + str)), + ('nss-token', Props(RSA, 'nsSSLToken', + 'Security token name (module of NSS DB)', + str)) +]) + + +def _security_generic_get(inst, basedn, logs, args, attrs_map): + result = {} + for attr, props in attrs_map.items(): + val = props.cls(inst).get_attr_val_utf8(props.attr) + result[props.attr] = val + if args.json: + print(json.dumps({'type': 'list', 'items': result})) + else: + print('\n'.join([f'{attr}: {value or ""}' for attr, value in result.items()])) + + +def _security_generic_set(inst, basedn, logs, args, attrs_map): + for attr, props in attrs_map.items(): + arg = getattr(args, attr.replace('-', '_')) + if arg is None: + continue + dsobj = props.cls(inst) + dsobj.replace(props.attr, arg) + + +def _security_generic_get_parser(parent, attrs_map, help): + p = parent.add_parser('get', help=help) + p.set_defaults(func=lambda *args: _security_generic_get(*args, attrs_map)) + return p + + +def _security_generic_set_parser(parent, attrs_map, help, description): + p = parent.add_parser('set', help=help, description=description) + p.set_defaults(func=lambda *args: _security_generic_set(*args, attrs_map)) + for opt, params in attrs_map.items(): + p.add_argument(f'--{opt}', help=f'{params[2]} ({params[1]})') + return p + + +def _security_ciphers_change(mode, ciphers, inst, log): + log = log.getChild('_security_ciphers_change') + if ('default' in ciphers) or ('all' in ciphers): + log.error(('Use ciphers\' names only. Keywords "default" and "all" are ignored. ' + 'Please, instead specify them manually using \'set\' command.')) + return + enc = Encryption(inst) + if enc.change_ciphers(mode, ciphers) is False: + log.error('Setting new ciphers failed.') + + +def _security_generic_toggle(inst, basedn, log, args, cls, attr, value, thing): + cls(inst).set(attr, value) + + +def _security_generic_toggle_parsers(parent, cls, attr, help_pattern): + def add_parser(action, value): + p = parent.add_parser(action.lower(), help=help_pattern.format(action)) + p.set_defaults(func=lambda *args: _security_generic_toggle(*args, cls, attr, value, action)) + return p + + return list(map(add_parser, ('Enable', 'Disable'), ('on', 'off'))) + + +def security_enable(inst, basedn, log, args): + dbpath = inst.get_cert_dir() + tlsdb = NssSsl(dbpath=dbpath) + if not tlsdb._db_exists(even_partial=True): # we want to be very careful + log.info(f'Secure database does not exist. Creating a new one in {dbpath}.') + tlsdb.reinit() + + Config(inst).set('nsslapd-security', 'on') + + +def security_disable(inst, basedn, log, args): + Config(inst).set('nsslapd-security', 'off') + + +def security_ciphers_enable(inst, basedn, log, args): + _security_ciphers_change('+', args.cipher, inst, log) + + +def security_ciphers_disable(inst, basedn, log, args): + _security_ciphers_change('-', args.cipher, inst, log) + + +def security_ciphers_set(inst, basedn, log, args): + enc = Encryption(inst) + enc.ciphers = args.cipher_string.split(',') + + +def security_ciphers_get(inst, basedn, log, args): + enc = Encryption(inst) + if args.json: + print({'type': 'list', 'items': enc.ciphers}) + else: + val = ','.join(enc.ciphers) + print(val if val != '' else '') + + +def security_ciphers_list(inst, basedn, log, args): + enc = Encryption(inst) + + if args.enabled: + lst = enc.enabled_ciphers + elif args.supported: + lst = enc.supported_ciphers + elif args.disabled: + lst = set(enc.supported_ciphers) - set(enc.enabled_ciphers) + else: + lst = enc.ciphers + + if args.json: + print(json.dumps({'type': 'list', 'items': lst})) + else: + if lst == []: + log.getChild('security').warn('List of ciphers is empty') + else: + print(*lst, sep='\n') + + +def create_parser(subparsers): + security = subparsers.add_parser('security', help='Query and manipulate security options') + security_sub = security.add_subparsers(help='security') + security_set = _security_generic_set_parser(security_sub, SECURITY_ATTRS_MAP, 'Set general security options', + ('Use this command for setting security related options located in cn=config and cn=encryption,cn=config.' + '\n\nTo enable/disable security you can use enable and disable commands instead.')) + security_get = _security_generic_get_parser(security_sub, SECURITY_ATTRS_MAP, 'Get general security options') + security_enable_p = security_sub.add_parser('enable', help='Enable security', description=( + 'If missing, create security database, then turn on security functionality. Please note this is usually not' + ' enought for TLS connections to work - proper setup of CA and server certificate is necessary.')) + security_enable_p.set_defaults(func=security_enable) + security_disable_p = security_sub.add_parser('disable', help='Disable security', description=( + 'Turn off security functionality. The rest of the configuration will be left untouched.')) + security_disable_p.set_defaults(func=security_disable) + + rsa = security_sub.add_parser('rsa', help='Query and mainpulate RSA security options') + rsa_sub = rsa.add_subparsers(help='rsa') + rsa_set = _security_generic_set_parser(rsa_sub, RSA_ATTRS_MAP, 'Set RSA security options', + ('Use this command for setting RSA (private key) related options located in cn=RSA,cn=encryption,cn=config.' + '\n\nTo enable/disable RSA you can use enable and disable commands instead.')) + rsa_get = _security_generic_get_parser(rsa_sub, RSA_ATTRS_MAP, 'Get RSA security options') + rsa_toggles = _security_generic_toggle_parsers(rsa_sub, RSA, 'nsSSLActivation', '{} RSA') + + ciphers = security_sub.add_parser('ciphers', help='Manage secure ciphers') + ciphers_sub = ciphers.add_subparsers(help='ciphers') + + ciphers_enable = ciphers_sub.add_parser('enable', help='Enable ciphers', description=( + 'Use this command to enable specific ciphers.')) + ciphers_enable.set_defaults(func=security_ciphers_enable) + ciphers_enable.add_argument('cipher', nargs='+') + + ciphers_disable = ciphers_sub.add_parser('disable', help='Disable ciphers', description=( + 'Use this command to disable specific ciphers.')) + ciphers_disable.set_defaults(func=security_ciphers_disable) + ciphers_disable.add_argument('cipher', nargs='+') + + ciphers_get = ciphers_sub.add_parser('get', help='Get ciphers attribute', description=( + 'Use this command to get contents of nsSSL3Ciphers attribute.')) + ciphers_get.set_defaults(func=security_ciphers_get) + + ciphers_set = ciphers_sub.add_parser('set', help='Set ciphers attribute', description=( + 'Use this command to directly set nsSSL3Ciphers attribute. It is a comma separated list ' + 'of cipher names (prefixed with + or -), optionaly including +all or -all. The attribute ' + 'may optionally be prefixed by keyword default. Please refer to documentation of ' + 'the attribute for a more detailed description.')) + ciphers_set.set_defaults(func=security_ciphers_set) + ciphers_set.add_argument('cipher_string', metavar='cipher-string') + + ciphers_list = ciphers_sub.add_parser('list', help='List ciphers', description=( + 'List secure ciphers. Without arguments, list ciphers as configured in nsSSL3Ciphers attribute.')) + ciphers_list.set_defaults(func=security_ciphers_list) + ciphers_list_group = ciphers_list.add_mutually_exclusive_group() + ciphers_list_group.add_argument('--enabled', action='store_true', + help='Only enabled ciphers') + ciphers_list_group.add_argument('--supported', action='store_true', + help='Only supported ciphers') + ciphers_list_group.add_argument('--disabled', action='store_true', + help='Only supported ciphers without enabled ciphers') diff --git a/src/lib389/lib389/config.py b/src/lib389/lib389/config.py index b462585..c2a34fa 100644 --- a/src/lib389/lib389/config.py +++ b/src/lib389/lib389/config.py @@ -1,5 +1,5 @@ # --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2015 Red Hat, Inc. +# Copyright (C) 2019 Red Hat, Inc. # All rights reserved. # # License: GPL (version 3 or any later version). @@ -202,14 +202,16 @@ class Config(DSLdapObject): return DSCLE0002 return None + class Encryption(DSLdapObject): """ Manage "cn=encryption,cn=config" tree, including: - ssl ciphers - ssl / tls levels """ - def __init__(self, conn): + def __init__(self, conn, dn=None): """@param conn - a DirSrv instance """ + assert dn is None # compatibility with Config class super(Encryption, self).__init__(instance=conn) self._dn = 'cn=encryption,%s' % DN_CONFIG self._create_objectclasses = ['top', 'nsEncryptionConfig'] @@ -225,11 +227,97 @@ class Encryption(DSLdapObject): super(Encryption, self).create(properties=properties) def _lint_check_tls_version(self): - tls_min = self.get_attr_val('sslVersionMin'); + tls_min = self.get_attr_val('sslVersionMin') if tls_min < ensure_bytes('TLS1.1'): return DSELE0001 return None + @property + def ciphers(self): + """List of requested ciphers. + + Each is represented by a string, either of: + - "+all" or "-all" + - TLS cipher RFC name, prefixed with either "+" or "-" + + Optionally, first element may be a string "default". + + :returns: list of str + """ + val = self.get_attr_val_utf8('nsSSL3Ciphers') + return val.split(',') if val else [] + + @ciphers.setter + def ciphers(self, ciphers): + """List of requested ciphers. + + :param ciphers: Ciphers to enable + :type ciphers: list of str + """ + self.set('nsSSL3Ciphers', ','.join(ciphers)) + self._log.info('Remeber to restart the server to apply the new cipher set.') + self._log.info('Some ciphers may be disabled anyway due to allowWeakCipher attribute.') + + def _get_listed_ciphers(self, attr): + """Remove features of ciphers that come after first :: occurence.""" + return [c[:c.index('::')] for c in self.get_attr_vals_utf8(attr)] + + @property + def enabled_ciphers(self): + """List currently enabled ciphers. + + :returns: list of str + """ + return self._get_listed_ciphers('nsSSLEnabledCiphers') + + @property + def supported_ciphers(self): + """List currently supported ciphers. + + :returns: list of str + """ + return self._get_listed_ciphers('nsSSLSupportedCiphers') + + def _check_ciphers_supported(self, ciphers): + good = True + for c in ciphers: + if c not in self.supported_ciphers: + self._log.warn(f'Cipher {c} is not supported.') + good = False + return good + + def change_ciphers(self, mode, ciphers): + """Enable or disable ciphers of the nsSSL3Ciphers attribute. + + :param mode: '+'/'-' string to enable/disable the ciphers + :type mode: str + :param ciphers: List of ciphers to enable/disable + :type ciphers: list of string + + :returns: False if some cipher is not supported + """ + if ('default' in ciphers) or 'all' in ciphers: + raise NotImplementedError('Processing "default" and "all" is not implemented.') + if not self._check_ciphers_supported(ciphers): + return False + + if mode == '+': + to_change = [c for c in ciphers if c not in self.enabled_ciphers] + elif mode == '-': + to_change = [c for c in ciphers if c in self.enabled_ciphers] + else: + raise ValueError('Incorrect mode. Use - or + sign.') + if len(to_change) != len(ciphers): + self._log.info( + ('Applying changes only for the following ciphers, the rest is up to date. ' + 'If this does not seem to be correct, please make sure the effective ' + 'set of enabled ciphers is up to date with configured ciphers ' + '- a server restart is needed for these to be applied.\n' + f'... {to_change}')) + cleaned = [c for c in self.ciphers if c[1:] not in to_change] + self.ciphers = cleaned + list(map(lambda c: mode + c, to_change)) + + class RSA(DSLdapObject): """ Manage the "cn=RSA,cn=encryption,cn=config" object @@ -237,8 +325,9 @@ class RSA(DSLdapObject): - Database path - ssl token name """ - def __init__(self, conn): + def __init__(self, conn, dn=None): """@param conn - a DirSrv instance """ + assert dn is None # compatibility with Config class super(RSA, self).__init__(instance=conn) self._dn = 'cn=RSA,cn=encryption,%s' % DN_CONFIG self._create_objectclasses = ['top', 'nsEncryptionModule'] diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py index 7a8f2a5..a54095c 100644 --- a/src/lib389/lib389/nss_ssl.py +++ b/src/lib389/lib389/nss_ssl.py @@ -162,11 +162,12 @@ only. self.log.debug("nss output: %s", result) return True - def _db_exists(self): + def _db_exists(self, even_partial=False): """Check that a nss db exists at the certpath""" - if all(map(os.path.exists, self.db_files["dbm_backend"])) or \ - all(map(os.path.exists, self.db_files["sql_backend"])): + fn = any if even_partial else all + if fn(map(os.path.exists, self.db_files["dbm_backend"])) or \ + fn(map(os.path.exists, self.db_files["sql_backend"])): return True return False