1# Copyright 2015, 2016 OpenMarket Ltd
2# Copyright 2019 The Matrix.org Foundation C.I.C.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import hashlib
17import logging
18import os
19from typing import Any, Dict, Iterator, List, Optional
20
21import attr
22import jsonschema
23from signedjson.key import (
24    NACL_ED25519,
25    SigningKey,
26    VerifyKey,
27    decode_signing_key_base64,
28    decode_verify_key_bytes,
29    generate_signing_key,
30    is_signing_algorithm_supported,
31    read_signing_keys,
32    write_signing_keys,
33)
34from unpaddedbase64 import decode_base64
35
36from synapse.types import JsonDict
37from synapse.util.stringutils import random_string, random_string_with_symbols
38
39from ._base import Config, ConfigError
40
41INSECURE_NOTARY_ERROR = """\
42Your server is configured to accept key server responses without signature
43validation or TLS certificate validation. This is likely to be very insecure. If
44you are *sure* you want to do this, set 'accept_keys_insecurely' on the
45keyserver configuration."""
46
47RELYING_ON_MATRIX_KEY_ERROR = """\
48Your server is configured to accept key server responses without TLS certificate
49validation, and which are only signed by the old (possibly compromised)
50matrix.org signing key 'ed25519:auto'. This likely isn't what you want to do,
51and you should enable 'federation_verify_certificates' in your configuration.
52
53If you are *sure* you want to do this, set 'accept_keys_insecurely' on the
54trusted_key_server configuration."""
55
56TRUSTED_KEY_SERVER_NOT_CONFIGURED_WARN = """\
57Synapse requires that a list of trusted key servers are specified in order to
58provide signing keys for other servers in the federation.
59
60This homeserver does not have a trusted key server configured in
61homeserver.yaml and will fall back to the default of 'matrix.org'.
62
63Trusted key servers should be long-lived and stable which makes matrix.org a
64good choice for many admins, but some admins may wish to choose another. To
65suppress this warning, the admin should set 'trusted_key_servers' in
66homeserver.yaml to their desired key server and 'suppress_key_server_warning'
67to 'true'.
68
69In a future release the software-defined default will be removed entirely and
70the trusted key server will be defined exclusively by the value of
71'trusted_key_servers'.
72--------------------------------------------------------------------------------"""
73
74TRUSTED_KEY_SERVER_CONFIGURED_AS_M_ORG_WARN = """\
75This server is configured to use 'matrix.org' as its trusted key server via the
76'trusted_key_servers' config option. 'matrix.org' is a good choice for a key
77server since it is long-lived, stable and trusted. However, some admins may
78wish to use another server for this purpose.
79
80To suppress this warning and continue using 'matrix.org', admins should set
81'suppress_key_server_warning' to 'true' in homeserver.yaml.
82--------------------------------------------------------------------------------"""
83
84logger = logging.getLogger(__name__)
85
86
87@attr.s(slots=True, auto_attribs=True)
88class TrustedKeyServer:
89    # name of the server.
90    server_name: str
91
92    # map from key id to key object, or None to disable signature verification.
93    verify_keys: Optional[Dict[str, VerifyKey]] = None
94
95
96class KeyConfig(Config):
97    section = "key"
98
99    def read_config(self, config, config_dir_path, **kwargs):
100        # the signing key can be specified inline or in a separate file
101        if "signing_key" in config:
102            self.signing_key = read_signing_keys([config["signing_key"]])
103        else:
104            signing_key_path = config.get("signing_key_path")
105            if signing_key_path is None:
106                signing_key_path = os.path.join(
107                    config_dir_path, config["server_name"] + ".signing.key"
108                )
109
110            self.signing_key = self.read_signing_keys(signing_key_path, "signing_key")
111
112        self.old_signing_keys = self.read_old_signing_keys(
113            config.get("old_signing_keys")
114        )
115        self.key_refresh_interval = self.parse_duration(
116            config.get("key_refresh_interval", "1d")
117        )
118
119        suppress_key_server_warning = config.get("suppress_key_server_warning", False)
120        key_server_signing_keys_path = config.get("key_server_signing_keys_path")
121        if key_server_signing_keys_path:
122            self.key_server_signing_keys = self.read_signing_keys(
123                key_server_signing_keys_path, "key_server_signing_keys_path"
124            )
125        else:
126            self.key_server_signing_keys = list(self.signing_key)
127
128        # if neither trusted_key_servers nor perspectives are given, use the default.
129        if "perspectives" not in config and "trusted_key_servers" not in config:
130            logger.warning(TRUSTED_KEY_SERVER_NOT_CONFIGURED_WARN)
131            key_servers = [{"server_name": "matrix.org"}]
132        else:
133            key_servers = config.get("trusted_key_servers", [])
134
135            if not isinstance(key_servers, list):
136                raise ConfigError(
137                    "trusted_key_servers, if given, must be a list, not a %s"
138                    % (type(key_servers).__name__,)
139                )
140
141            # merge the 'perspectives' config into the 'trusted_key_servers' config.
142            key_servers.extend(_perspectives_to_key_servers(config))
143
144            if not suppress_key_server_warning and "matrix.org" in (
145                s["server_name"] for s in key_servers
146            ):
147                logger.warning(TRUSTED_KEY_SERVER_CONFIGURED_AS_M_ORG_WARN)
148
149        # list of TrustedKeyServer objects
150        self.key_servers = list(
151            _parse_key_servers(
152                key_servers, self.root.tls.federation_verify_certificates
153            )
154        )
155
156        self.macaroon_secret_key = config.get(
157            "macaroon_secret_key", self.root.registration.registration_shared_secret
158        )
159
160        if not self.macaroon_secret_key:
161            # Unfortunately, there are people out there that don't have this
162            # set. Lets just be "nice" and derive one from their secret key.
163            logger.warning("Config is missing macaroon_secret_key")
164            seed = bytes(self.signing_key[0])
165            self.macaroon_secret_key = hashlib.sha256(seed).digest()
166
167        # a secret which is used to calculate HMACs for form values, to stop
168        # falsification of values
169        self.form_secret = config.get("form_secret", None)
170
171    def generate_config_section(
172        self, config_dir_path, server_name, generate_secrets=False, **kwargs
173    ):
174        base_key_name = os.path.join(config_dir_path, server_name)
175
176        if generate_secrets:
177            macaroon_secret_key = 'macaroon_secret_key: "%s"' % (
178                random_string_with_symbols(50),
179            )
180            form_secret = 'form_secret: "%s"' % random_string_with_symbols(50)
181        else:
182            macaroon_secret_key = "#macaroon_secret_key: <PRIVATE STRING>"
183            form_secret = "#form_secret: <PRIVATE STRING>"
184
185        return (
186            """\
187        # a secret which is used to sign access tokens. If none is specified,
188        # the registration_shared_secret is used, if one is given; otherwise,
189        # a secret key is derived from the signing key.
190        #
191        %(macaroon_secret_key)s
192
193        # a secret which is used to calculate HMACs for form values, to stop
194        # falsification of values. Must be specified for the User Consent
195        # forms to work.
196        #
197        %(form_secret)s
198
199        ## Signing Keys ##
200
201        # Path to the signing key to sign messages with
202        #
203        signing_key_path: "%(base_key_name)s.signing.key"
204
205        # The keys that the server used to sign messages with but won't use
206        # to sign new messages.
207        #
208        old_signing_keys:
209          # For each key, `key` should be the base64-encoded public key, and
210          # `expired_ts`should be the time (in milliseconds since the unix epoch) that
211          # it was last used.
212          #
213          # It is possible to build an entry from an old signing.key file using the
214          # `export_signing_key` script which is provided with synapse.
215          #
216          # For example:
217          #
218          #"ed25519:id": { key: "base64string", expired_ts: 123456789123 }
219
220        # How long key response published by this server is valid for.
221        # Used to set the valid_until_ts in /key/v2 APIs.
222        # Determines how quickly servers will query to check which keys
223        # are still valid.
224        #
225        #key_refresh_interval: 1d
226
227        # The trusted servers to download signing keys from.
228        #
229        # When we need to fetch a signing key, each server is tried in parallel.
230        #
231        # Normally, the connection to the key server is validated via TLS certificates.
232        # Additional security can be provided by configuring a `verify key`, which
233        # will make synapse check that the response is signed by that key.
234        #
235        # This setting supercedes an older setting named `perspectives`. The old format
236        # is still supported for backwards-compatibility, but it is deprecated.
237        #
238        # 'trusted_key_servers' defaults to matrix.org, but using it will generate a
239        # warning on start-up. To suppress this warning, set
240        # 'suppress_key_server_warning' to true.
241        #
242        # Options for each entry in the list include:
243        #
244        #    server_name: the name of the server. required.
245        #
246        #    verify_keys: an optional map from key id to base64-encoded public key.
247        #       If specified, we will check that the response is signed by at least
248        #       one of the given keys.
249        #
250        #    accept_keys_insecurely: a boolean. Normally, if `verify_keys` is unset,
251        #       and federation_verify_certificates is not `true`, synapse will refuse
252        #       to start, because this would allow anyone who can spoof DNS responses
253        #       to masquerade as the trusted key server. If you know what you are doing
254        #       and are sure that your network environment provides a secure connection
255        #       to the key server, you can set this to `true` to override this
256        #       behaviour.
257        #
258        # An example configuration might look like:
259        #
260        #trusted_key_servers:
261        #  - server_name: "my_trusted_server.example.com"
262        #    verify_keys:
263        #      "ed25519:auto": "abcdefghijklmnopqrstuvwxyzabcdefghijklmopqr"
264        #  - server_name: "my_other_trusted_server.example.com"
265        #
266        trusted_key_servers:
267          - server_name: "matrix.org"
268
269        # Uncomment the following to disable the warning that is emitted when the
270        # trusted_key_servers include 'matrix.org'. See above.
271        #
272        #suppress_key_server_warning: true
273
274        # The signing keys to use when acting as a trusted key server. If not specified
275        # defaults to the server signing key.
276        #
277        # Can contain multiple keys, one per line.
278        #
279        #key_server_signing_keys_path: "key_server_signing_keys.key"
280        """
281            % locals()
282        )
283
284    def read_signing_keys(self, signing_key_path: str, name: str) -> List[SigningKey]:
285        """Read the signing keys in the given path.
286
287        Args:
288            signing_key_path
289            name: Associated config key name
290
291        Returns:
292            The signing keys read from the given path.
293        """
294
295        signing_keys = self.read_file(signing_key_path, name)
296        try:
297            return read_signing_keys(signing_keys.splitlines(True))
298        except Exception as e:
299            raise ConfigError("Error reading %s: %s" % (name, str(e)))
300
301    def read_old_signing_keys(
302        self, old_signing_keys: Optional[JsonDict]
303    ) -> Dict[str, VerifyKey]:
304        if old_signing_keys is None:
305            return {}
306        keys = {}
307        for key_id, key_data in old_signing_keys.items():
308            if is_signing_algorithm_supported(key_id):
309                key_base64 = key_data["key"]
310                key_bytes = decode_base64(key_base64)
311                verify_key = decode_verify_key_bytes(key_id, key_bytes)
312                verify_key.expired_ts = key_data["expired_ts"]
313                keys[key_id] = verify_key
314            else:
315                raise ConfigError(
316                    "Unsupported signing algorithm for old key: %r" % (key_id,)
317                )
318        return keys
319
320    def generate_files(self, config: Dict[str, Any], config_dir_path: str) -> None:
321        if "signing_key" in config:
322            return
323
324        signing_key_path = config.get("signing_key_path")
325        if signing_key_path is None:
326            signing_key_path = os.path.join(
327                config_dir_path, config["server_name"] + ".signing.key"
328            )
329
330        if not self.path_exists(signing_key_path):
331            print("Generating signing key file %s" % (signing_key_path,))
332            with open(signing_key_path, "w") as signing_key_file:
333                key_id = "a_" + random_string(4)
334                write_signing_keys(signing_key_file, (generate_signing_key(key_id),))
335        else:
336            signing_keys = self.read_file(signing_key_path, "signing_key")
337            if len(signing_keys.split("\n")[0].split()) == 1:
338                # handle keys in the old format.
339                key_id = "a_" + random_string(4)
340                key = decode_signing_key_base64(
341                    NACL_ED25519, key_id, signing_keys.split("\n")[0]
342                )
343                with open(signing_key_path, "w") as signing_key_file:
344                    write_signing_keys(signing_key_file, (key,))
345
346
347def _perspectives_to_key_servers(config: JsonDict) -> Iterator[JsonDict]:
348    """Convert old-style 'perspectives' configs into new-style 'trusted_key_servers'
349
350    Returns an iterable of entries to add to trusted_key_servers.
351    """
352
353    # 'perspectives' looks like:
354    #
355    # {
356    #     "servers": {
357    #         "matrix.org": {
358    #             "verify_keys": {
359    #                 "ed25519:auto": {
360    #                     "key": "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
361    #                 }
362    #             }
363    #         }
364    #     }
365    # }
366    #
367    # 'trusted_keys' looks like:
368    #
369    # [
370    #     {
371    #         "server_name": "matrix.org",
372    #         "verify_keys": {
373    #             "ed25519:auto": "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
374    #         }
375    #     }
376    # ]
377
378    perspectives_servers = config.get("perspectives", {}).get("servers", {})
379
380    for server_name, server_opts in perspectives_servers.items():
381        trusted_key_server_entry = {"server_name": server_name}
382        verify_keys = server_opts.get("verify_keys")
383        if verify_keys is not None:
384            trusted_key_server_entry["verify_keys"] = {
385                key_id: key_data["key"] for key_id, key_data in verify_keys.items()
386            }
387        yield trusted_key_server_entry
388
389
390TRUSTED_KEY_SERVERS_SCHEMA = {
391    "$schema": "http://json-schema.org/draft-04/schema#",
392    "description": "schema for the trusted_key_servers setting",
393    "type": "array",
394    "items": {
395        "type": "object",
396        "properties": {
397            "server_name": {"type": "string"},
398            "verify_keys": {
399                "type": "object",
400                # each key must be a base64 string
401                "additionalProperties": {"type": "string"},
402            },
403        },
404        "required": ["server_name"],
405    },
406}
407
408
409def _parse_key_servers(
410    key_servers: List[Any], federation_verify_certificates: bool
411) -> Iterator[TrustedKeyServer]:
412    try:
413        jsonschema.validate(key_servers, TRUSTED_KEY_SERVERS_SCHEMA)
414    except jsonschema.ValidationError as e:
415        raise ConfigError(
416            "Unable to parse 'trusted_key_servers': {}".format(
417                e.message  # noqa: B306, jsonschema.ValidationError.message is a valid attribute
418            )
419        )
420
421    for server in key_servers:
422        server_name = server["server_name"]
423        result = TrustedKeyServer(server_name=server_name)
424
425        verify_keys = server.get("verify_keys")
426        if verify_keys is not None:
427            result.verify_keys = {}
428            for key_id, key_base64 in verify_keys.items():
429                if not is_signing_algorithm_supported(key_id):
430                    raise ConfigError(
431                        "Unsupported signing algorithm on key %s for server %s in "
432                        "trusted_key_servers" % (key_id, server_name)
433                    )
434                try:
435                    key_bytes = decode_base64(key_base64)
436                    verify_key = decode_verify_key_bytes(key_id, key_bytes)
437                except Exception as e:
438                    raise ConfigError(
439                        "Unable to parse key %s for server %s in "
440                        "trusted_key_servers: %s" % (key_id, server_name, e)
441                    )
442
443                result.verify_keys[key_id] = verify_key
444
445        if not federation_verify_certificates and not server.get(
446            "accept_keys_insecurely"
447        ):
448            _assert_keyserver_has_verify_keys(result)
449
450        yield result
451
452
453def _assert_keyserver_has_verify_keys(trusted_key_server: TrustedKeyServer) -> None:
454    if not trusted_key_server.verify_keys:
455        raise ConfigError(INSECURE_NOTARY_ERROR)
456
457    # also check that they are not blindly checking the old matrix.org key
458    if trusted_key_server.server_name == "matrix.org" and any(
459        key_id == "ed25519:auto" for key_id in trusted_key_server.verify_keys
460    ):
461        raise ConfigError(RELYING_ON_MATRIX_KEY_ERROR)
462