add support for ssl protocol and ciphersuite

This commit is contained in:
Peter Bieringer 2024-11-13 22:19:44 +01:00
parent 1d07d72946
commit fb904320d2
4 changed files with 143 additions and 1 deletions

6
config
View file

@ -40,6 +40,12 @@
# TCP traffic between Radicale and a reverse proxy
#certificate_authority =
# SSL protocol, secure configuration: ALL -SSLv3 -TLSv1 -TLSv1.1
#protocol = (default)
# SSL ciphersuite, secure configuration: DHE:ECDHE:-NULL:-SHA (see also "man openssl-ciphers")
#ciphersuite = (default)
[encoding]

View file

@ -141,6 +141,14 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"aliases": ("-s", "--ssl",),
"opposite_aliases": ("-S", "--no-ssl",),
"type": bool}),
("protocol", {
"value": "",
"help": "SSL/TLS protocol (Apache SSLProtocol format)",
"type": str}),
("ciphersuite", {
"value": "",
"help": "SSL/TLS Cipher Suite (OpenSSL cipher list format)",
"type": str}),
("certificate", {
"value": "/etc/ssl/radicale.cert.pem",
"help": "set certificate file",

View file

@ -34,7 +34,7 @@ from typing import (Any, Callable, Dict, List, MutableMapping, Optional, Set,
Tuple, Union)
from urllib.parse import unquote
from radicale import Application, config
from radicale import Application, config, utils
from radicale.log import logger
COMPAT_EAI_ADDRFAMILY: int
@ -167,6 +167,8 @@ class ParallelHTTPSServer(ParallelHTTPServer):
certfile: str = self.configuration.get("server", "certificate")
keyfile: str = self.configuration.get("server", "key")
cafile: str = self.configuration.get("server", "certificate_authority")
protocol: str = self.configuration.get("server", "protocol")
ciphersuite: str = self.configuration.get("server", "ciphersuite")
# Test if the files can be read
for name, filename in [("certificate", certfile), ("key", keyfile),
("certificate_authority", cafile)]:
@ -184,6 +186,23 @@ class ParallelHTTPSServer(ParallelHTTPServer):
e)) from e
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile=certfile, keyfile=keyfile)
if protocol:
logger.info("SSL set explicit protocol: '%s'", protocol)
context.options = utils.ssl_context_options_by_protocol(protocol, context.options)
context.minimum_version = utils.ssl_context_minimum_version_by_options(context.options)
else:
logger.info("SSL default protocol active")
logger.info("SSL minimum acceptable protocol: %s", context.minimum_version)
logger.info("SSL accepted protocols: %s", ' '.join(utils.ssl_get_protocols(context)))
if ciphersuite:
logger.info("SSL set explicit ciphersuite: '%s'", ciphersuite)
context.set_ciphers(ciphersuite)
else:
logger.info("SSL default ciphersuite active")
cipherlist = []
for entry in context.get_ciphers():
cipherlist.append(entry["name"])
logger.info("SSL accepted ciphers: %s", ' '.join(cipherlist))
if cafile:
context.load_verify_locations(cafile=cafile)
context.verify_mode = ssl.CERT_REQUIRED

View file

@ -2,6 +2,7 @@
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <unrud@outlook.com>
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -18,6 +19,7 @@
from importlib import import_module, metadata
from typing import Callable, Sequence, Type, TypeVar, Union
import ssl
from radicale import config
from radicale.log import logger
@ -47,3 +49,110 @@ def load_plugin(internal_types: Sequence[str], module_name: str,
def package_version(name):
return metadata.version(name)
def ssl_context_options_by_protocol(protocol: str, ssl_context_options):
logger.debug("SSL protocol string: '%s' and current SSL context options: '0x%x'", protocol, ssl_context_options)
# disable any protocol by default
logger.debug("SSL context options, disable ALL by default")
ssl_context_options |= ssl.OP_NO_SSLv2
ssl_context_options |= ssl.OP_NO_SSLv3
ssl_context_options |= ssl.OP_NO_TLSv1
ssl_context_options |= ssl.OP_NO_TLSv1_1
ssl_context_options |= ssl.OP_NO_TLSv1_2
ssl_context_options |= ssl.OP_NO_TLSv1_3
logger.debug("SSL cleared SSL context options: '0x%x'", ssl_context_options)
for entry in protocol.split():
entry = entry.strip('+') # remove trailing '+'
if entry == "ALL":
logger.debug("SSL context options, enable ALL (some maybe not supported by underlying OpenSSL, SSLv2 not enabled at all)")
ssl_context_options &= ~ssl.OP_NO_SSLv3
ssl_context_options &= ~ssl.OP_NO_TLSv1
ssl_context_options &= ~ssl.OP_NO_TLSv1_1
ssl_context_options &= ~ssl.OP_NO_TLSv1_2
ssl_context_options &= ~ssl.OP_NO_TLSv1_3
elif entry == "SSLv2":
logger.notice("SSL context options, ignore SSLv2 (totally insecure)")
elif entry == "SSLv3":
ssl_context_options &= ~ssl.OP_NO_SSLv3
logger.debug("SSL context options, enable SSLv3 (maybe not supported by underlying OpenSSL)")
elif entry == "TLSv1":
ssl_context_options &= ~ssl.OP_NO_TLSv1
logger.debug("SSL context options, enable TLSv1 (maybe not supported by underlying OpenSSL)")
elif entry == "TLSv1.1":
logger.debug("SSL context options, enable TLSv1.1 (maybe not supported by underlying OpenSSL)")
ssl_context_options &= ~ssl.OP_NO_TLSv1_1
elif entry == "TLSv1.2":
logger.debug("SSL context options, enable TLSv1.2")
ssl_context_options &= ~ssl.OP_NO_TLSv1_2
elif entry == "TLSv1.3":
logger.debug("SSL context options, enable TLSv1.3")
ssl_context_options &= ~ssl.OP_NO_TLSv1_3
elif entry == "-ALL":
logger.debug("SSL context options, disable ALL")
ssl_context_options |= ssl.OP_NO_SSLv2
ssl_context_options |= ssl.OP_NO_SSLv3
ssl_context_options |= ssl.OP_NO_TLSv1
ssl_context_options |= ssl.OP_NO_TLSv1_1
ssl_context_options |= ssl.OP_NO_TLSv1_2
ssl_context_options |= ssl.OP_NO_TLSv1_3
elif entry == "-SSLv2":
ssl_context_options |= ssl.OP_NO_SSLv2
logger.debug("SSL context options, disable SSLv2")
elif entry == "-SSLv3":
ssl_context_options |= ssl.OP_NO_SSLv3
logger.debug("SSL context options, disable SSLv3")
elif entry == "-TLSv1":
logger.debug("SSL context options, disable TLSv1")
ssl_context_options |= ssl.OP_NO_TLSv1
elif entry == "-TLSv1.1":
logger.debug("SSL context options, disable TLSv1.1")
ssl_context_options |= ssl.OP_NO_TLSv1_1
elif entry == "-TLSv1.2":
logger.debug("SSL context options, disable TLSv1.2")
ssl_context_options |= ssl.OP_NO_TLSv1_2
elif entry == "-TLSv1.3":
logger.debug("SSL context options, disable TLSv1.3")
ssl_context_options |= ssl.OP_NO_TLSv1_3
else:
logger.error("SSL protocol string: '%s' contain unsupported entry: '%s'", protocol, entry)
logger.debug("SSL resulting context options: '0x%x'", ssl_context_options)
return ssl_context_options
def ssl_context_minimum_version_by_options(ssl_context_options):
logger.debug("SSL calculate minimum version by context options: '0x%x'", ssl_context_options)
ssl_context_minimum_version = 0 # default
if ((ssl_context_options & ssl.OP_NO_SSLv3) and (ssl_context_minimum_version == 0)):
ssl_context_minimum_version = ssl.TLSVersion.TLSv1
if ((ssl_context_options & ssl.OP_NO_TLSv1) and (ssl_context_minimum_version == ssl.TLSVersion.TLSv1)):
ssl_context_minimum_version = ssl.TLSVersion.TLSv1_1
if ((ssl_context_options & ssl.OP_NO_TLSv1_1) and (ssl_context_minimum_version == ssl.TLSVersion.TLSv1_1)):
ssl_context_minimum_version = ssl.TLSVersion.TLSv1_2
if ((ssl_context_options & ssl.OP_NO_TLSv1_2) and (ssl_context_minimum_version == ssl.TLSVersion.TLSv1_2)):
ssl_context_minimum_version = ssl.TLSVersion.TLSv1_3
if (ssl_context_minimum_version == 0):
ssl_context_minimum_version = ssl.TLSVersion.SSLv3 # default
logger.debug("SSL context options: '0x%x' results in minimum version: %s", ssl_context_options, ssl_context_minimum_version)
return ssl_context_minimum_version
def ssl_get_protocols(context):
protocols = []
if not (context.options & ssl.OP_NO_SSLv3):
if (context.minimum_version < ssl.TLSVersion.TLSv1):
protocols.append("SSLv3")
if not (context.options & ssl.OP_NO_TLSv1):
if (context.minimum_version < ssl.TLSVersion.TLSv1_1):
protocols.append("TLSv1")
if not (context.options & ssl.OP_NO_TLSv1_1):
if (context.minimum_version < ssl.TLSVersion.TLSv1_2):
protocols.append("TLSv1.1")
if not (context.options & ssl.OP_NO_TLSv1_2):
if (context.minimum_version < ssl.TLSVersion.TLSv1_3):
protocols.append("TLSv1.2")
if not (context.options & ssl.OP_NO_TLSv1_3):
protocols.append("TLSv1.3")
return protocols