Просмотр исходного кода

add support for ssl protocol and ciphersuite

Peter Bieringer 1 год назад
Родитель
Сommit
fb904320d2
4 измененных файлов с 143 добавлено и 1 удалено
  1. 6 0
      config
  2. 8 0
      radicale/config.py
  3. 20 1
      radicale/server.py
  4. 109 0
      radicale/utils.py

+ 6 - 0
config

@@ -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]
 

+ 8 - 0
radicale/config.py

@@ -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",

+ 20 - 1
radicale/server.py

@@ -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

+ 109 - 0
radicale/utils.py

@@ -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