mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-01-26 16:16:31 -06:00
Remove NSSConnection from the Python RPC module
NSSConnection was causing a lot of trouble in the past and there is a lot of logic around it just to make it not fail. What's more, when using NSS to create an SSL connection in FIPS mode, NSS always requires database password which makes the `ipa` command totally unusable. NSSConnection is therefore replaced with Python's httplib.HTTPSConnection which is OpenSSL based. The HTTPSConnection is set up to handle authentication with client certificate for connections to Dogtag server as RA agent. It allows to handle client cert/private key in separate files and also encrypted private key files. https://fedorahosted.org/freeipa/ticket/5695 Reviewed-By: Jan Cholasta <jcholast@redhat.com>
This commit is contained in:
parent
98e3b14a04
commit
dfd560a190
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
144
ipalib/util.py
144
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 = ''
|
||||
|
Loading…
Reference in New Issue
Block a user