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