Blame SOURCES/0009-Ticket-50217-Implement-dsconf-security-section.patch

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