diff options
| -rw-r--r-- | ipalib/config.py | 11 | ||||
| -rw-r--r-- | ipalib/constants.py | 1 | ||||
| -rw-r--r-- | ipalib/rpc.py | 70 | ||||
| -rw-r--r-- | ipalib/util.py | 144 |
4 files changed, 169 insertions, 57 deletions
diff --git a/ipalib/config.py b/ipalib/config.py index 388ffe81b..4891f53d7 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -504,6 +504,17 @@ class Env(object): if 'nss_dir' not in self: self.nss_dir = self._join('confdir', 'nssdb') + if 'tls_ca_cert' not in self: + self.tls_ca_cert = self._join('confdir', 'ca.crt') + + # having tls_ca_cert an absolute path could help us extending this + # in the future for different certificate providers simply by adding + # a prefix to the path + if not path.isabs(self.tls_ca_cert): + raise errors.EnvironmentError( + "tls_ca_cert has to be an absolute path to a CA certificate, " + "got '{}'".format(self.tls_ca_cert)) + # Set plugins_on_demand: if 'plugins_on_demand' not in self: self.plugins_on_demand = (self.context == 'cli') diff --git a/ipalib/constants.py b/ipalib/constants.py index e64324f2d..bc78422ea 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -226,6 +226,7 @@ DEFAULT_CONFIG = ( ('conf_default', object), # File containing context independent config ('plugins_on_demand', object), # Whether to finalize plugins on-demand (bool) ('nss_dir', object), # Path to nssdb, default {confdir}/nssdb + ('tls_ca_cert', object), # Path to CA cert file # Set in Env._finalize_core(): ('in_server', object), # Whether or not running in-server (bool) diff --git a/ipalib/rpc.py b/ipalib/rpc.py index b91e8e24d..f2cdad9bb 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -44,7 +44,7 @@ import gzip import gssapi from dns import resolver, rdatatype from dns.exception import DNSException -from nss.error import NSPRError +from ssl import SSLError import six from six.moves import urllib @@ -60,8 +60,7 @@ from ipapython import kernel_keyring from ipapython.cookie import Cookie from ipapython.dnsutil import DNSName from ipalib.text import _ -import ipapython.nsslib -from ipapython.nsslib import NSSConnection +from ipalib.util import create_https_connection from ipalib.krb_utils import KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, KRB5KRB_AP_ERR_TKT_EXPIRED, \ KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, \ KRB5_REALM_CANT_RESOLVE, KRB5_CC_NOTFOUND, get_principal @@ -542,48 +541,20 @@ class LanguageAwareTransport(MultiProtocolTransport): return (host, extra_headers, x509) + class SSLTransport(LanguageAwareTransport): """Handles an HTTPS transaction to an XML-RPC server.""" - - def get_connection_dbdir(self): - """ - If there is a connections open it may have already initialized - NSS database. Return the database location used by the connection. - """ - for value in context.__dict__.values(): - if not isinstance(value, Connection): - continue - if not isinstance( - getattr(value.conn, '_ServerProxy__transport', None), - SSLTransport): - continue - if hasattr(value.conn._ServerProxy__transport, 'dbdir'): - return value.conn._ServerProxy__transport.dbdir - return None - def make_connection(self, host): host, self._extra_headers, _x509 = self.get_host_info(host) if self._connection and host == self._connection[0]: return self._connection[1] - dbdir = context.nss_dir - connection_dbdir = self.get_connection_dbdir() - - if connection_dbdir: - # If an existing connection is already using the same NSS - # database there is no need to re-initialize. - no_init = dbdir == connection_dbdir - - else: - # If the NSS database is already being used there is no - # need to re-initialize. - no_init = dbdir == ipapython.nsslib.current_dbdir - - conn = NSSConnection(host, 443, dbdir=dbdir, no_init=no_init, - tls_version_min=api.env.tls_version_min, - tls_version_max=api.env.tls_version_max) - self.dbdir=dbdir + conn = create_https_connection( + host, 443, + api.env.tls_ca_cert, + tls_version_min=api.env.tls_version_min, + tls_version_max=api.env.tls_version_max) conn.connect() @@ -963,15 +934,15 @@ class RPCClient(Connectible): return session_url def create_connection(self, ccache=None, verbose=None, fallback=None, - delegate=None, nss_dir=None): + delegate=None, ca_certfile=None): if verbose is None: verbose = self.api.env.verbose if fallback is None: fallback = self.api.env.fallback if delegate is None: delegate = self.api.env.delegate - if nss_dir is None: - nss_dir = self.api.env.nss_dir + if ca_certfile is None: + ca_certfile = self.api.env.tls_ca_cert try: rpc_uri = self.env[self.env_rpc_uri_key] principal = get_principal(ccache_name=ccache) @@ -989,7 +960,7 @@ class RPCClient(Connectible): except (errors.CCacheError, ValueError): # No session key, do full Kerberos auth pass - context.nss_dir = nss_dir + context.ca_certfile = ca_certfile urls = self.get_url_list(rpc_uri) serverproxy = None for url in urls: @@ -1099,7 +1070,7 @@ class RPCClient(Connectible): error=e.faultString, server=server, ) - except NSPRError as e: + except SSLError as e: raise NetworkError(uri=server, error=str(e)) except ProtocolError as e: # By catching a 401 here we can detect the case where we have @@ -1116,22 +1087,9 @@ class RPCClient(Connectible): # This shouldn't happen if we have a session but it isn't fatal. pass - # Create a new serverproxy with the non-session URI. If there - # is an existing connection we need to save the NSS dbdir so - # we can skip an unnecessary NSS_Initialize() and avoid - # NSS_Shutdown issues. + # Create a new serverproxy with the non-session URI serverproxy = self.create_connection(os.environ.get('KRB5CCNAME'), self.env.verbose, self.env.fallback, self.env.delegate) - - dbdir = None - current_conn = getattr(context, self.id, None) - if current_conn is not None: - dbdir = getattr(current_conn.conn._ServerProxy__transport, 'dbdir', None) - if dbdir is not None: - self.debug('Using dbdir %s' % dbdir) setattr(context, self.id, Connection(serverproxy, self.disconnect)) - if dbdir is not None: - current_conn = getattr(context, self.id, None) - current_conn.conn._ServerProxy__transport.dbdir = dbdir return self.forward(name, *args, **kw) raise NetworkError(uri=server, error=e.errmsg) except socket.error as e: diff --git a/ipalib/util.py b/ipalib/util.py index 1509607db..2beabf1c7 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -33,6 +33,7 @@ import decimal import dns import encodings import sys +import ssl from weakref import WeakKeyDictionary import netaddr @@ -42,8 +43,17 @@ from dns.resolver import NXDOMAIN from netaddr.core import AddrFormatError import six +try: + from httplib import HTTPSConnection +except ImportError: + # Python 3 + from http.client import HTTPSConnection + from ipalib import errors, messages -from ipalib.constants import DOMAIN_LEVEL_0 +from ipalib.constants import ( + DOMAIN_LEVEL_0, + TLS_VERSIONS, TLS_VERSION_MINIMAL +) from ipalib.text import _ from ipapython.ssh import SSHPublicKey from ipapython.dn import DN, RDN @@ -51,6 +61,7 @@ from ipapython.dnsutil import DNSName from ipapython.dnsutil import resolve_ip_addresses from ipapython.ipa_log_manager import root_logger + if six.PY3: unicode = str @@ -187,6 +198,137 @@ def normalize_zone(zone): return zone +def get_proper_tls_version_span(tls_version_min, tls_version_max): + """ + This function checks whether the given TLS versions are known in + FreeIPA and that these versions fulfill the requirements for minimal + TLS version (see + `ipalib.constants: TLS_VERSIONS, TLS_VERSION_MINIMAL`). + + :param tls_version_min: + the lower value in the TLS min-max span, raised to the lowest + allowed value if too low + :param tls_version_max: + the higher value in the TLS min-max span, raised to tls_version_min + if lower than TLS_VERSION_MINIMAL + :raises: ValueError + """ + min_allowed_idx = TLS_VERSIONS.index(TLS_VERSION_MINIMAL) + + try: + min_version_idx = TLS_VERSIONS.index(tls_version_min) + except ValueError: + raise ValueError("tls_version_min ('{val}') is not a known " + "TLS version.".format(val=tls_version_min)) + + try: + max_version_idx = TLS_VERSIONS.index(tls_version_max) + except ValueError: + raise ValueError("tls_version_max ('{val}') is not a known " + "TLS version.".format(val=tls_version_max)) + + if min_version_idx > max_version_idx: + raise ValueError("tls_version_min is higher than " + "tls_version_max.") + + if min_version_idx < min_allowed_idx: + min_version_idx = min_allowed_idx + root_logger.warning("tls_version_min set too low ('{old}')," + "using '{new}' instead" + .format(old=tls_version_min, + new=TLS_VERSIONS[min_version_idx])) + + if max_version_idx < min_allowed_idx: + max_version_idx = min_version_idx + root_logger.warning("tls_version_max set too low ('{old}')," + "using '{new}' instead" + .format(old=tls_version_max, + new=TLS_VERSIONS[max_version_idx])) + return TLS_VERSIONS[min_version_idx:max_version_idx+1] + + +def create_https_connection( + host, port=HTTPSConnection.default_port, + cafile=None, + client_certfile=None, client_keyfile=None, + keyfile_passwd=None, + tls_version_min="tls1.1", + tls_version_max="tls1.2", + **kwargs +): + """ + Create a customized HTTPSConnection object. + + :param host: The host to connect to + :param port: The port to connect to, defaults to + HTTPSConnection.default_port + :param cafile: A PEM-format file containning the trusted + CA certificates + :param client_certfile: + A PEM-format client certificate file that will be used to + identificate the user to the server. + :param client_keyfile: + A file with the client private key. If this argument is not + supplied, the key will be sought in client_certfile. + :param keyfile_passwd: + A path to the file which stores the password that is used to + encrypt client_keyfile. Leave default value if the keyfile + is not encrypted. + :returns An established HTTPS connection to host:port + """ + # pylint: disable=no-member + tls_cutoff_map = { + "ssl2": ssl.OP_NO_SSLv2, + "ssl3": ssl.OP_NO_SSLv3, + "tls1.0": ssl.OP_NO_TLSv1, + "tls1.1": ssl.OP_NO_TLSv1_1, + "tls1.2": ssl.OP_NO_TLSv1_2, + } + # pylint: enable=no-member + + if cafile is None: + raise RuntimeError("cafile argument is required to perform server " + "certificate verification") + + # remove the slice of negating protocol options according to options + tls_span = get_proper_tls_version_span(tls_version_min, tls_version_max) + + # official Python documentation states that the best option to get + # TLSv1 and later is to setup SSLContext with PROTOCOL_SSLv23 + # and then negate the insecure SSLv2 and SSLv3 + # pylint: disable=no-member + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.options |= ( + ssl.OP_ALL | ssl.OP_NO_COMPRESSION | ssl.OP_SINGLE_DH_USE | + ssl.OP_SINGLE_ECDH_USE + ) + + # pylint: enable=no-member + # set up the correct TLS version flags for the SSL context + for version in TLS_VERSIONS: + if version in tls_span: + # make sure the required TLS versions are available if Python + # decides to modify the default TLS flags + ctx.options &= ~tls_cutoff_map[version] + else: + # disable all TLS versions not in tls_span + ctx.options |= tls_cutoff_map[version] + + ctx.verify_mode = ssl.CERT_REQUIRED + ctx.check_hostname = True + ctx.load_verify_locations(cafile) + + if client_certfile is not None: + if keyfile_passwd is not None: + with open(keyfile_passwd) as pwd_f: + passwd = pwd_f.read() + else: + passwd = None + ctx.load_cert_chain(client_certfile, client_keyfile, passwd) + + return HTTPSConnection(host, port, context=ctx, **kwargs) + + def validate_dns_label(dns_label, allow_underscore=False, allow_slash=False): base_chars = 'a-z0-9' extra_chars = '' |
