summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/config.py11
-rw-r--r--ipalib/constants.py1
-rw-r--r--ipalib/rpc.py70
-rw-r--r--ipalib/util.py144
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 = ''