Repository: matrix-org/synapse

Issue:
Synapse should not require TLS certificate files
Testing with 0.99rc4, it seems like:

- regardless of the `no_tls` setting's value (`False` or `True`), Synapse would still try to load `tls_certificate_path` or `tls_private_key_path` (I'm not sure which one now, but it does try to read it and fails with an exception if it's missing or not a valid file)

- the comment in `homeserver.yaml` about `no_tls` seems backward ("Setting no_tls to False will do so (and avoid the need to give synapse a TLS private key).") I guess this should say `True`, rather than `False`

----------

I wish to use Synapse with a reverse-proxy and this issue is a blocker for that. I'd rather not provide any certificate files to Synapse at all.

Now, looking at #2840, unless something changed, it seems like Synapse may really need to access the certificate file.

This leads to all sorts of pain, especially when automated renewal is involved.
Not only do we need to renew the certificate and reload the reverse-proxy (no downtime there), but we also need to **restart** Synapse at the same time (reloading doesn't seem possible as explained in #1180).


Discussion:
this is a thing we should fix before 1.0
Is it just a problem of wishing to read some file, or does it actually use it for something?

If this is to be fixed for 1.0 (and not 0.99), how should we do the 0.99 transition? By supplying any certificate file (self-signed even) just to prevent parsing from breaking.. or by supplying a proper working certificate (the actual one used for reverse-proxying)?
> Is it just a problem of wishing to read some file, or does it actually use it for something?

It exposes it in a field in the s2s api which is no longer used.

> If this is to be fixed for 1.0 (and not 0.99), how should we do the 0.99 transition? 

Bear in mind that there is no need to change anything to use 0.99.

> By supplying any certificate file (self-signed even) just to prevent parsing from breaking.. or by supplying a proper working certificate (the actual one used for reverse-proxying)?

any certificate should do
I'm trying to prepare [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy) for 0.99 (and 1.0).

Existing installations (that run from before v0.99) do have a self-signed certificate which can be left in place, so there's no need to do anything.

On the other hand, new installations would not have a self-signed certificate generated in `/data`, because 0.99's `generate` command doesn't do that anymore. So I do need to do something sensible in that case -- either make the playbook generate a self-signed certificate just so we can make 0.99 happy.. or pass along the real certificate into the container.

Thank you for your answers!

I now know that both solutions are feasible.
They do have pros and cons, so I'll figure out which one to choose, so we can migrate people over to Synapse 0.99.
I think that this still needs to be exposed for pre-0.99 Synapses, which will verify the TLS fingerprint? But for 1.0, it can def. be removed.
meh. pre-0.99 Synapses will only check it when fetching the server keys, which is normally delegated via `matrix.org`, which is on 0.99, so doesn't care. We might as well make it optional for now.
I was about to generate a temporary self-signed certificate to make Synapse v0.99 happy, but it looks like once #4566 lands in 0.99rc5 (or 0.99 final), I'll be able to keep things simpler and use `tls_certificate_path: /conf/dummy.tls.crt` without doing anything extra.

Thanks!
This seems to be the issue i was having when trying to set up a fresh server. the server would not begin to listen on the ports (unable to see python when running netstat) checking further it seems it was not able to find the tls and would error out. I tried changing the true to a false but that didn't seem to help me. Luckily i had a backup of my hyper-v instance i could restore.

Then when upgrading the previous instance I see that it tried to overwrite my homeserver.yaml with all the new tls changes, etc. At this time i kept mine, but i'm assuming this will be a requirement to be merged with my local homeserver.yaml in the future (ie v1) when it no longer breaks the server?

Resolving patch:
diff --git a/synapse/app/_base.py b/synapse/app/_base.py
--- a/synapse/app/_base.py
+++ b/synapse/app/_base.py
@@ -213,12 +213,13 @@ def refresh_certificate(hs):
     Refresh the TLS certificates that Synapse is using by re-reading them from
     disk and updating the TLS context factories to use them.
     """
-    hs.config.read_certificate_from_disk()
 
     if not hs.config.has_tls_listener():
-        # nothing else to do here
+        # attempt to reload the certs for the good of the tls_fingerprints
+        hs.config.read_certificate_from_disk(require_cert_and_key=False)
         return
 
+    hs.config.read_certificate_from_disk(require_cert_and_key=True)
     hs.tls_server_context_factory = context_factory.ServerContextFactory(hs.config)
 
     if hs._listening_services:
diff --git a/synapse/config/tls.py b/synapse/config/tls.py
--- a/synapse/config/tls.py
+++ b/synapse/config/tls.py
@@ -23,7 +23,7 @@
 
 from OpenSSL import crypto
 
-from synapse.config._base import Config
+from synapse.config._base import Config, ConfigError
 
 logger = logging.getLogger(__name__)
 
@@ -45,6 +45,19 @@ def read_config(self, config):
 
         self.tls_certificate_file = self.abspath(config.get("tls_certificate_path"))
         self.tls_private_key_file = self.abspath(config.get("tls_private_key_path"))
+
+        if self.has_tls_listener():
+            if not self.tls_certificate_file:
+                raise ConfigError(
+                    "tls_certificate_path must be specified if TLS-enabled listeners are "
+                    "configured."
+                )
+            if not self.tls_private_key_file:
+                raise ConfigError(
+                    "tls_certificate_path must be specified if TLS-enabled listeners are "
+                    "configured."
+                )
+
         self._original_tls_fingerprints = config.get("tls_fingerprints", [])
 
         if self._original_tls_fingerprints is None:
@@ -105,26 +118,40 @@ def is_disk_cert_valid(self, allow_self_signed=True):
         days_remaining = (expires_on - now).days
         return days_remaining
 
-    def read_certificate_from_disk(self):
-        """
-        Read the certificates from disk.
+    def read_certificate_from_disk(self, require_cert_and_key):
         """
-        self.tls_certificate = self.read_tls_certificate()
+        Read the certificates and private key from disk.
 
-        if self.has_tls_listener():
+        Args:
+            require_cert_and_key (bool): set to True to throw an error if the certificate
+                and key file are not given
+        """
+        if require_cert_and_key:
             self.tls_private_key = self.read_tls_private_key()
+            self.tls_certificate = self.read_tls_certificate()
+        elif self.tls_certificate_file:
+            # we only need the certificate for the tls_fingerprints. Reload it if we
+            # can, but it's not a fatal error if we can't.
+            try:
+                self.tls_certificate = self.read_tls_certificate()
+            except Exception as e:
+                logger.info(
+                    "Unable to read TLS certificate (%s). Ignoring as no "
+                    "tls listeners enabled.", e,
+                )
 
         self.tls_fingerprints = list(self._original_tls_fingerprints)
 
-        # Check that our own certificate is included in the list of fingerprints
-        # and include it if it is not.
-        x509_certificate_bytes = crypto.dump_certificate(
-            crypto.FILETYPE_ASN1, self.tls_certificate
-        )
-        sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest())
-        sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints)
-        if sha256_fingerprint not in sha256_fingerprints:
-            self.tls_fingerprints.append({u"sha256": sha256_fingerprint})
+        if self.tls_certificate:
+            # Check that our own certificate is included in the list of fingerprints
+            # and include it if it is not.
+            x509_certificate_bytes = crypto.dump_certificate(
+                crypto.FILETYPE_ASN1, self.tls_certificate
+            )
+            sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest())
+            sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints)
+            if sha256_fingerprint not in sha256_fingerprints:
+                self.tls_fingerprints.append({u"sha256": sha256_fingerprint})
 
     def default_config(self, config_dir_path, server_name, **kwargs):
         base_key_name = os.path.join(config_dir_path, server_name)


Test patch:
diff --git a/tests/config/test_tls.py b/tests/config/test_tls.py
--- a/tests/config/test_tls.py
+++ b/tests/config/test_tls.py
@@ -65,7 +65,7 @@ def test_warn_self_signed(self):
 
         t = TestConfig()
         t.read_config(config)
-        t.read_certificate_from_disk()
+        t.read_certificate_from_disk(require_cert_and_key=False)
 
         warnings = self.flushWarnings()
         self.assertEqual(len(warnings), 1)