Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit be3b901

Browse files
authored
Update the TLS cipher string and provide configurability for TLS on outgoing federation (#5550)
1 parent 9646a59 commit be3b901

File tree

7 files changed

+190
-9
lines changed

7 files changed

+190
-9
lines changed

changelog.d/5550.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The minimum TLS version used for outgoing federation requests can now be set with `federation_client_minimum_tls_version`.

changelog.d/5550.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Synapse will now only allow TLS v1.2 connections when serving federation, if it terminates TLS. As Synapse's allowed ciphers were only able to be used in TLSv1.2 before, this does not change behaviour.

docs/sample_config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,15 @@ listeners:
317317
#
318318
#federation_verify_certificates: false
319319

320+
# The minimum TLS version that will be used for outbound federation requests.
321+
#
322+
# Defaults to `1`. Configurable to `1`, `1.1`, `1.2`, or `1.3`. Note
323+
# that setting this value higher than `1.2` will prevent federation to most
324+
# of the public Matrix network: only configure it to `1.3` if you have an
325+
# entirely private federation setup and you can ensure TLS 1.3 support.
326+
#
327+
#federation_client_minimum_tls_version: 1.2
328+
320329
# Skip federation certificate verification on the following whitelist
321330
# of domains.
322331
#

scripts/generate_config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python
1+
#!/usr/bin/env python3
22

33
import argparse
44
import shutil

synapse/config/tls.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from unpaddedbase64 import encode_base64
2525

26-
from OpenSSL import crypto
26+
from OpenSSL import SSL, crypto
2727
from twisted.internet._sslverify import Certificate, trustRootFromCertificates
2828

2929
from synapse.config._base import Config, ConfigError
@@ -81,6 +81,27 @@ def read_config(self, config, config_dir_path, **kwargs):
8181
"federation_verify_certificates", True
8282
)
8383

84+
# Minimum TLS version to use for outbound federation traffic
85+
self.federation_client_minimum_tls_version = str(
86+
config.get("federation_client_minimum_tls_version", 1)
87+
)
88+
89+
if self.federation_client_minimum_tls_version not in ["1", "1.1", "1.2", "1.3"]:
90+
raise ConfigError(
91+
"federation_client_minimum_tls_version must be one of: 1, 1.1, 1.2, 1.3"
92+
)
93+
94+
# Prevent people shooting themselves in the foot here by setting it to
95+
# the biggest number blindly
96+
if self.federation_client_minimum_tls_version == "1.3":
97+
if getattr(SSL, "OP_NO_TLSv1_3", None) is None:
98+
raise ConfigError(
99+
(
100+
"federation_client_minimum_tls_version cannot be 1.3, "
101+
"your OpenSSL does not support it"
102+
)
103+
)
104+
84105
# Whitelist of domains to not verify certificates for
85106
fed_whitelist_entries = config.get(
86107
"federation_certificate_verification_whitelist", []
@@ -261,6 +282,15 @@ def generate_config_section(
261282
#
262283
#federation_verify_certificates: false
263284
285+
# The minimum TLS version that will be used for outbound federation requests.
286+
#
287+
# Defaults to `1`. Configurable to `1`, `1.1`, `1.2`, or `1.3`. Note
288+
# that setting this value higher than `1.2` will prevent federation to most
289+
# of the public Matrix network: only configure it to `1.3` if you have an
290+
# entirely private federation setup and you can ensure TLS 1.3 support.
291+
#
292+
#federation_client_minimum_tls_version: 1.2
293+
264294
# Skip federation certificate verification on the following whitelist
265295
# of domains.
266296
#

synapse/crypto/context_factory.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,25 @@
2424
from twisted.internet._sslverify import _defaultCurveName
2525
from twisted.internet.abstract import isIPAddress, isIPv6Address
2626
from twisted.internet.interfaces import IOpenSSLClientConnectionCreator
27-
from twisted.internet.ssl import CertificateOptions, ContextFactory, platformTrust
27+
from twisted.internet.ssl import (
28+
CertificateOptions,
29+
ContextFactory,
30+
TLSVersion,
31+
platformTrust,
32+
)
2833
from twisted.python.failure import Failure
2934

3035
logger = logging.getLogger(__name__)
3136

3237

38+
_TLS_VERSION_MAP = {
39+
"1": TLSVersion.TLSv1_0,
40+
"1.1": TLSVersion.TLSv1_1,
41+
"1.2": TLSVersion.TLSv1_2,
42+
"1.3": TLSVersion.TLSv1_3,
43+
}
44+
45+
3346
class ServerContextFactory(ContextFactory):
3447
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
3548
connections."""
@@ -43,16 +56,18 @@ def configure_context(context, config):
4356
try:
4457
_ecCurve = crypto.get_elliptic_curve(_defaultCurveName)
4558
context.set_tmp_ecdh(_ecCurve)
46-
4759
except Exception:
4860
logger.exception("Failed to enable elliptic curve for TLS")
49-
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
61+
62+
context.set_options(
63+
SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1
64+
)
5065
context.use_certificate_chain_file(config.tls_certificate_file)
5166
context.use_privatekey(config.tls_private_key)
5267

5368
# https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
5469
context.set_cipher_list(
55-
"ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1"
70+
"ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM"
5671
)
5772

5873
def getContext(self):
@@ -79,10 +94,22 @@ def __init__(self, config):
7994
# Use CA root certs provided by OpenSSL
8095
trust_root = platformTrust()
8196

82-
self._verify_ssl_context = CertificateOptions(trustRoot=trust_root).getContext()
97+
# "insecurelyLowerMinimumTo" is the argument that will go lower than
98+
# Twisted's default, which is why it is marked as "insecure" (since
99+
# Twisted's defaults are reasonably secure). But, since Twisted is
100+
# moving to TLS 1.2 by default, we want to respect the config option if
101+
# it is set to 1.0 (which the alternate option, raiseMinimumTo, will not
102+
# let us do).
103+
minTLS = _TLS_VERSION_MAP[config.federation_client_minimum_tls_version]
104+
105+
self._verify_ssl = CertificateOptions(
106+
trustRoot=trust_root, insecurelyLowerMinimumTo=minTLS
107+
)
108+
self._verify_ssl_context = self._verify_ssl.getContext()
83109
self._verify_ssl_context.set_info_callback(self._context_info_cb)
84110

85-
self._no_verify_ssl_context = CertificateOptions().getContext()
111+
self._no_verify_ssl = CertificateOptions(insecurelyLowerMinimumTo=minTLS)
112+
self._no_verify_ssl_context = self._no_verify_ssl.getContext()
86113
self._no_verify_ssl_context.set_info_callback(self._context_info_cb)
87114

88115
def get_options(self, host):

tests/config/test_tls.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2019 New Vector Ltd
3+
# Copyright 2019 Matrix.org Foundation C.I.C.
34
#
45
# Licensed under the Apache License, Version 2.0 (the "License");
56
# you may not use this file except in compliance with the License.
@@ -15,7 +16,10 @@
1516

1617
import os
1718

18-
from synapse.config.tls import TlsConfig
19+
from OpenSSL import SSL
20+
21+
from synapse.config.tls import ConfigError, TlsConfig
22+
from synapse.crypto.context_factory import ClientTLSOptionsFactory
1923

2024
from tests.unittest import TestCase
2125

@@ -78,3 +82,112 @@ def test_warn_self_signed(self):
7882
"or use Synapse's ACME support to provision one."
7983
),
8084
)
85+
86+
def test_tls_client_minimum_default(self):
87+
"""
88+
The default client TLS version is 1.0.
89+
"""
90+
config = {}
91+
t = TestConfig()
92+
t.read_config(config, config_dir_path="", data_dir_path="")
93+
94+
self.assertEqual(t.federation_client_minimum_tls_version, "1")
95+
96+
def test_tls_client_minimum_set(self):
97+
"""
98+
The default client TLS version can be set to 1.0, 1.1, and 1.2.
99+
"""
100+
config = {"federation_client_minimum_tls_version": 1}
101+
t = TestConfig()
102+
t.read_config(config, config_dir_path="", data_dir_path="")
103+
self.assertEqual(t.federation_client_minimum_tls_version, "1")
104+
105+
config = {"federation_client_minimum_tls_version": 1.1}
106+
t = TestConfig()
107+
t.read_config(config, config_dir_path="", data_dir_path="")
108+
self.assertEqual(t.federation_client_minimum_tls_version, "1.1")
109+
110+
config = {"federation_client_minimum_tls_version": 1.2}
111+
t = TestConfig()
112+
t.read_config(config, config_dir_path="", data_dir_path="")
113+
self.assertEqual(t.federation_client_minimum_tls_version, "1.2")
114+
115+
# Also test a string version
116+
config = {"federation_client_minimum_tls_version": "1"}
117+
t = TestConfig()
118+
t.read_config(config, config_dir_path="", data_dir_path="")
119+
self.assertEqual(t.federation_client_minimum_tls_version, "1")
120+
121+
config = {"federation_client_minimum_tls_version": "1.2"}
122+
t = TestConfig()
123+
t.read_config(config, config_dir_path="", data_dir_path="")
124+
self.assertEqual(t.federation_client_minimum_tls_version, "1.2")
125+
126+
def test_tls_client_minimum_1_point_3_missing(self):
127+
"""
128+
If TLS 1.3 support is missing and it's configured, it will raise a
129+
ConfigError.
130+
"""
131+
# thanks i hate it
132+
if hasattr(SSL, "OP_NO_TLSv1_3"):
133+
OP_NO_TLSv1_3 = SSL.OP_NO_TLSv1_3
134+
delattr(SSL, "OP_NO_TLSv1_3")
135+
self.addCleanup(setattr, SSL, "SSL.OP_NO_TLSv1_3", OP_NO_TLSv1_3)
136+
assert not hasattr(SSL, "OP_NO_TLSv1_3")
137+
138+
config = {"federation_client_minimum_tls_version": 1.3}
139+
t = TestConfig()
140+
with self.assertRaises(ConfigError) as e:
141+
t.read_config(config, config_dir_path="", data_dir_path="")
142+
self.assertEqual(
143+
e.exception.args[0],
144+
(
145+
"federation_client_minimum_tls_version cannot be 1.3, "
146+
"your OpenSSL does not support it"
147+
),
148+
)
149+
150+
def test_tls_client_minimum_1_point_3_exists(self):
151+
"""
152+
If TLS 1.3 support exists and it's configured, it will be settable.
153+
"""
154+
# thanks i hate it, still
155+
if not hasattr(SSL, "OP_NO_TLSv1_3"):
156+
SSL.OP_NO_TLSv1_3 = 0x00
157+
self.addCleanup(lambda: delattr(SSL, "OP_NO_TLSv1_3"))
158+
assert hasattr(SSL, "OP_NO_TLSv1_3")
159+
160+
config = {"federation_client_minimum_tls_version": 1.3}
161+
t = TestConfig()
162+
t.read_config(config, config_dir_path="", data_dir_path="")
163+
self.assertEqual(t.federation_client_minimum_tls_version, "1.3")
164+
165+
def test_tls_client_minimum_set_passed_through_1_2(self):
166+
"""
167+
The configured TLS version is correctly configured by the ContextFactory.
168+
"""
169+
config = {"federation_client_minimum_tls_version": 1.2}
170+
t = TestConfig()
171+
t.read_config(config, config_dir_path="", data_dir_path="")
172+
173+
cf = ClientTLSOptionsFactory(t)
174+
175+
# The context has had NO_TLSv1_1 and NO_TLSv1_0 set, but not NO_TLSv1_2
176+
self.assertNotEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1, 0)
177+
self.assertNotEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_1, 0)
178+
self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_2, 0)
179+
180+
def test_tls_client_minimum_set_passed_through_1_0(self):
181+
"""
182+
The configured TLS version is correctly configured by the ContextFactory.
183+
"""
184+
config = {"federation_client_minimum_tls_version": 1}
185+
t = TestConfig()
186+
t.read_config(config, config_dir_path="", data_dir_path="")
187+
188+
cf = ClientTLSOptionsFactory(t)
189+
190+
# The context has not had any of the NO_TLS set.
191+
self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1, 0)
192+
self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_1, 0)
193+
self.assertEqual(cf._verify_ssl._options & SSL.OP_NO_TLSv1_2, 0)

0 commit comments

Comments
 (0)