From 2307e77efb6a75091b9152f81a52c83b8282d61a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Hon=C4=9Bk?= <mhonek@redhat.com>
Date: Thu, 31 Jan 2019 10:44:55 +0100
Subject: [PATCH 05/12] 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 <mhonek@redhat.com>
Review by: firstyear, mreynolds (Thanks!)
---
src/lib389/cli/dsconf | 2 +
src/lib389/lib389/cli_conf/security.py | 244 +++++++++++++++++++++++++
src/lib389/lib389/config.py | 97 +++++++++-
src/lib389/lib389/nss_ssl.py | 7 +-
4 files changed, 343 insertions(+), 7 deletions(-)
create mode 100644 src/lib389/lib389/cli_conf/security.py
diff --git a/src/lib389/cli/dsconf b/src/lib389/cli/dsconf
index f81516290..c0c0b4dfe 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 000000000..6d8c1ae0f
--- /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 '<undefined>')
+
+
+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 b462585df..c2a34fa07 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 7a8f2a5bd..a54095cd4 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
--
2.21.0