Don't hard-code client's TLS versions and ciphers

Client connections no longer override TLS version range and ciphers by
default. Instead clients use the default settings from the system's
crypto policy.

Minimum TLS version is now TLS 1.2. The default crypto policy on
RHEL 8 sets TLS 1.2 as minimum version, while Fedora 31 sets TLS 1.0 as
minimum version. The minimum version is configured with OpenSSL 1.1.1
APIs. Python 3.6 lacks the setters to override the system policy.

The effective minimum version is always TLS 1.2, because FreeIPA
reconfigures Apache HTTPd on Fedora.

Fixes: https://pagure.io/freeipa/issue/8125
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Thomas Woerner <twoerner@redhat.com>
This commit is contained in:
Christian Heimes 2019-11-22 10:42:11 +01:00
parent 0198eca795
commit 639bb71940
6 changed files with 108 additions and 26 deletions

View File

@ -633,19 +633,29 @@ class Env:
# set the best known TLS version if min/max versions are not set
if 'tls_version_min' not in self:
self.tls_version_min = TLS_VERSION_DEFAULT_MIN
elif self.tls_version_min not in TLS_VERSIONS:
if (
self.tls_version_min is not None and
self.tls_version_min not in TLS_VERSIONS
):
raise errors.EnvironmentError(
"Unknown TLS version '{ver}' set in tls_version_min."
.format(ver=self.tls_version_min))
if 'tls_version_max' not in self:
self.tls_version_max = TLS_VERSION_DEFAULT_MAX
elif self.tls_version_max not in TLS_VERSIONS:
if (
self.tls_version_max is not None and
self.tls_version_max not in TLS_VERSIONS
):
raise errors.EnvironmentError(
"Unknown TLS version '{ver}' set in tls_version_max."
.format(ver=self.tls_version_max))
if self.tls_version_max < self.tls_version_min:
if (
self.tls_version_min is not None and
self.tls_version_max is not None and
self.tls_version_max < self.tls_version_min
):
raise errors.EnvironmentError(
"tls_version_min is set to a higher TLS version than "
"tls_version_max.")

View File

@ -38,8 +38,9 @@ except Exception:
# TLS related constants
# * SSL2 and SSL3 are broken.
# * TLS1.0 and TLS1.1 are no longer state of the art.
# * TLS1.3 support is not yet stable, e.g. issues with PHA.
# Therefore only TLS 1.2 is enabled by default.
# * TLS1.2 and 1.3 are secure and working properly
# * Crypto policies restrict TLS range to 1.2 and 1.3. Python 3.6 cannot
# override the crypto policy.
TLS_VERSIONS = [
"ssl2",
@ -49,9 +50,10 @@ TLS_VERSIONS = [
"tls1.2",
"tls1.3",
]
TLS_VERSION_MINIMAL = "tls1.0"
TLS_VERSION_DEFAULT_MIN = "tls1.2"
TLS_VERSION_DEFAULT_MAX = "tls1.3"
TLS_VERSION_MINIMAL = "tls1.2"
TLS_VERSION_MAXIMAL = "tls1.3"
TLS_VERSION_DEFAULT_MIN = None
TLS_VERSION_DEFAULT_MAX = None
# regular expression NameSpace member names must match:
NAME_REGEX = r'^[a-z][_a-z0-9]*[a-z0-9]$|^[a-z]$'

View File

@ -57,8 +57,8 @@ except ImportError:
from ipalib import errors, messages
from ipalib.constants import (
DOMAIN_LEVEL_0,
TLS_VERSIONS, TLS_VERSION_MINIMAL, TLS_VERSION_DEFAULT_MIN,
TLS_VERSION_DEFAULT_MAX,
TLS_VERSIONS, TLS_VERSION_MINIMAL, TLS_VERSION_MAXIMAL,
TLS_VERSION_DEFAULT_MIN, TLS_VERSION_DEFAULT_MAX,
)
from ipalib.text import _
from ipaplatform.constants import constants
@ -248,6 +248,13 @@ def get_proper_tls_version_span(tls_version_min, tls_version_max):
if lower than TLS_VERSION_MINIMAL
:raises: ValueError
"""
if tls_version_min is None and tls_version_max is None:
# no defaults, use system's default TLS version range
return None
if tls_version_min is None:
tls_version_min = TLS_VERSION_MINIMAL
if tls_version_max is None:
tls_version_max = TLS_VERSION_MAXIMAL
min_allowed_idx = TLS_VERSIONS.index(TLS_VERSION_MINIMAL)
try:
@ -326,9 +333,6 @@ def create_https_connection(
raise RuntimeError("cafile \'{file}\' doesn't exist or is unreadable".
format(file=cafile))
# 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
@ -343,16 +347,20 @@ def create_https_connection(
# configure ciphers, uses system crypto policies on RH platforms.
ctx.set_ciphers(constants.TLS_HIGH_CIPHERS)
# remove the slice of negating protocol options according to options
tls_span = get_proper_tls_version_span(tls_version_min, tls_version_max)
# 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]
if tls_span is not None:
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]
# Enable TLS 1.3 post-handshake auth
if getattr(ctx, "post_handshake_auth", None) is not None:

View File

@ -14,10 +14,9 @@ from ipaplatform.base.constants import BaseConstantsNamespace
class RedHatConstantsNamespace(BaseConstantsNamespace):
# System-wide crypto policy, but without TripleDES, pre-shared key,
# secure remote password, and DSA cert authentication.
# Use system-wide crypto policy
# see https://fedoraproject.org/wiki/Changes/CryptoPolicy
TLS_HIGH_CIPHERS = "PROFILE=SYSTEM:!3DES:!PSK:!SRP:!aDSS"
TLS_HIGH_CIPHERS = None
constants = RedHatConstantsNamespace()

View File

@ -683,7 +683,7 @@ class TestIPACommand(IntegrationTest):
assert 'Last name: %s' % (modlast) in cmd.stdout_text
def test_enabled_tls_protocols(self):
"""Check that only TLS 1.2 is enabled in Apache.
"""Check Apache has same TLS versions enabled as crypto policy
This is the regression test for issue
https://pagure.io/freeipa/issue/7995.
@ -698,9 +698,24 @@ class TestIPACommand(IntegrationTest):
)
return res.returncode == 0
# get minimum version from current crypto-policy
openssl_cnf = self.master.get_file_contents(
"/etc/crypto-policies/back-ends/opensslcnf.config",
encoding="utf-8"
)
mo = re.search(r"MinProtocol\s*=\s*(TLSv[0-9.]+)", openssl_cnf)
assert mo
min_tls = mo.group(1)
# Fedora DEFAULT has TLS 1.0 enabled, NEXT has TLS 1.2
# even FUTURE crypto policy has TLS 1.2 as minimum version
assert min_tls in {"TLSv1", "TLSv1.2"}
# On Fedora FreeIPA still disables TLS 1.0 and 1.1 in ssl.conf.
assert not is_tls_version_enabled('tls1')
assert not is_tls_version_enabled('tls1_1')
assert is_tls_version_enabled('tls1_2')
assert is_tls_version_enabled('tls1_3')
def test_samba_config_file(self):
"""Check that ipa-adtrust-install generates sane smb.conf

View File

@ -5,11 +5,15 @@
"""
import os
import ssl
from unittest import mock
import pytest
from ipalib.util import get_pager
from ipalib.util import (
get_pager, create_https_connection, get_proper_tls_version_span
)
from ipaplatform.constants import constants
@pytest.mark.parametrize('pager,expected_result', [
@ -24,3 +28,47 @@ def test_get_pager(pager, expected_result):
with mock.patch.dict(os.environ, {'PAGER': pager}):
pager = get_pager()
assert(pager == expected_result or pager.endswith(expected_result))
BASE_CTX = ssl.SSLContext(ssl.PROTOCOL_TLS)
if constants.TLS_HIGH_CIPHERS is not None:
BASE_CTX.set_ciphers(constants.TLS_HIGH_CIPHERS)
else:
BASE_CTX.set_ciphers("PROFILE=SYSTEM")
# options: IPA still supports Python 3.6 without min/max version setters
BASE_OPT = BASE_CTX.options
BASE_OPT |= (
ssl.OP_ALL | ssl.OP_NO_COMPRESSION | ssl.OP_SINGLE_DH_USE |
ssl.OP_SINGLE_ECDH_USE
)
TLS_OPT = (
ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 |
ssl.OP_NO_TLSv1_1
)
OP_NO_TLSv1_3 = getattr(ssl, "OP_NO_TLSv1_3", 0) # make pylint happy
@pytest.mark.parametrize('minver,maxver,opt,expected', [
(None, None, BASE_OPT, None),
(None, "tls1.3", BASE_OPT | TLS_OPT, ["tls1.2", "tls1.3"]),
("tls1.2", "tls1.3", BASE_OPT | TLS_OPT, ["tls1.2", "tls1.3"]),
("tls1.2", None, BASE_OPT | TLS_OPT, ["tls1.2", "tls1.3"]),
("tls1.2", "tls1.2", BASE_OPT | TLS_OPT | OP_NO_TLSv1_3, ["tls1.2"]),
(None, "tls1.2", BASE_OPT | TLS_OPT | OP_NO_TLSv1_3, ["tls1.2"]),
("tls1.3", "tls1.3", BASE_OPT | TLS_OPT | ssl.OP_NO_TLSv1_2, ["tls1.3"]),
("tls1.3", None, BASE_OPT | TLS_OPT | ssl.OP_NO_TLSv1_2, ["tls1.3"]),
])
def test_tls_version_span(minver, maxver, opt, expected):
assert get_proper_tls_version_span(minver, maxver) == expected
# file must exist and contain certs
cafile = ssl.get_default_verify_paths().cafile
conn = create_https_connection(
"invalid.test",
cafile=cafile,
tls_version_min=minver,
tls_version_max=maxver
)
ctx = getattr(conn, "_context")
assert ctx.options == BASE_OPT | opt
assert ctx.get_ciphers() == BASE_CTX.get_ciphers()