summary refs log blame commit diff
path: root/synapse/config/key.py
blob: 94a0f47ea48286d65c1897d0d571cece70127202 (plain) (tree)
1
2
3
                       
                                     
                                                 











                                                                          

              
 
                 
                            





                                   
 
                                        
 
                                                                              
 
                                      
 




                                                                                







                                                                                
 
                                    
 


                                 
 




                                                                                 
                                  


                                                                         
                                                                           
 
                                                           
                                              
                                                        
                                                    
         

















                                                                                     
         
 





                                                                            
                                                                
                                                                    
 
                                                                           


                                                                            

                                                                            
                                                                  
 



                                                                              
             
                                                                           
 
                

                                                                             
         
                               
 
                                                 
         
                                   
 
                                                                            
                                                                         
         
                       
 

                                                       
         


                                                                           
                          








                                                                       
         
                                 
                                                            
         






































                                                                                       


                                                                      
                                                                   
                              
                                                                         
 












                                                                               
 
                                     
                                                     
                                                  
                                                                 
                                                
                                                                                     
             
                                                                          
                                                             
                                                
                                                                     
                 
                                                                     

















































































                                                                                    
                     









                                                                            
                                                                 
          
                                                     
                    









                                                                             
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import hashlib
import logging
import os

import attr
import jsonschema
from signedjson.key import (
    NACL_ED25519,
    decode_signing_key_base64,
    decode_verify_key_bytes,
    generate_signing_key,
    is_signing_algorithm_supported,
    read_signing_keys,
    write_signing_keys,
)
from unpaddedbase64 import decode_base64

from synapse.util.stringutils import random_string, random_string_with_symbols

from ._base import Config, ConfigError

INSECURE_NOTARY_ERROR = """\
Your server is configured to accept key server responses without signature
validation or TLS certificate validation. This is likely to be very insecure. If
you are *sure* you want to do this, set 'accept_keys_insecurely' on the
keyserver configuration."""

RELYING_ON_MATRIX_KEY_ERROR = """\
Your server is configured to accept key server responses without TLS certificate
validation, and which are only signed by the old (possibly compromised)
matrix.org signing key 'ed25519:auto'. This likely isn't what you want to do,
and you should enable 'federation_verify_certificates' in your configuration.

If you are *sure* you want to do this, set 'accept_keys_insecurely' on the
trusted_key_server configuration."""


logger = logging.getLogger(__name__)


@attr.s
class TrustedKeyServer(object):
    # string: name of the server.
    server_name = attr.ib()

    # dict[str,VerifyKey]|None: map from key id to key object, or None to disable
    # signature verification.
    verify_keys = attr.ib(default=None)


class KeyConfig(Config):
    def read_config(self, config):
        # the signing key can be specified inline or in a separate file
        if "signing_key" in config:
            self.signing_key = read_signing_keys([config["signing_key"]])
        else:
            self.signing_key_path = config["signing_key_path"]
            self.signing_key = self.read_signing_key(self.signing_key_path)

        self.old_signing_keys = self.read_old_signing_keys(
            config.get("old_signing_keys", {})
        )
        self.key_refresh_interval = self.parse_duration(
            config.get("key_refresh_interval", "1d")
        )

        # if neither trusted_key_servers nor perspectives are given, use the default.
        if "perspectives" not in config and "trusted_key_servers" not in config:
            key_servers = [{"server_name": "matrix.org"}]
        else:
            key_servers = config.get("trusted_key_servers", [])

            if not isinstance(key_servers, list):
                raise ConfigError(
                    "trusted_key_servers, if given, must be a list, not a %s"
                    % (type(key_servers).__name__,)
                )

            # merge the 'perspectives' config into the 'trusted_key_servers' config.
            key_servers.extend(_perspectives_to_key_servers(config))

        # list of TrustedKeyServer objects
        self.key_servers = list(
            _parse_key_servers(key_servers, self.federation_verify_certificates)
        )

        self.macaroon_secret_key = config.get(
            "macaroon_secret_key", self.registration_shared_secret
        )

        if not self.macaroon_secret_key:
            # Unfortunately, there are people out there that don't have this
            # set. Lets just be "nice" and derive one from their secret key.
            logger.warn("Config is missing macaroon_secret_key")
            seed = bytes(self.signing_key[0])
            self.macaroon_secret_key = hashlib.sha256(seed).digest()

        self.expire_access_token = config.get("expire_access_token", False)

        # a secret which is used to calculate HMACs for form values, to stop
        # falsification of values
        self.form_secret = config.get("form_secret", None)

    def default_config(
        self, config_dir_path, server_name, generate_secrets=False, **kwargs
    ):
        base_key_name = os.path.join(config_dir_path, server_name)

        if generate_secrets:
            macaroon_secret_key = 'macaroon_secret_key: "%s"' % (
                random_string_with_symbols(50),
            )
            form_secret = 'form_secret: "%s"' % random_string_with_symbols(50)
        else:
            macaroon_secret_key = "# macaroon_secret_key: <PRIVATE STRING>"
            form_secret = "# form_secret: <PRIVATE STRING>"

        return (
            """\
        # a secret which is used to sign access tokens. If none is specified,
        # the registration_shared_secret is used, if one is given; otherwise,
        # a secret key is derived from the signing key.
        #
        %(macaroon_secret_key)s

        # Used to enable access token expiration.
        #
        #expire_access_token: False

        # a secret which is used to calculate HMACs for form values, to stop
        # falsification of values. Must be specified for the User Consent
        # forms to work.
        #
        %(form_secret)s

        ## Signing Keys ##

        # Path to the signing key to sign messages with
        #
        signing_key_path: "%(base_key_name)s.signing.key"

        # The keys that the server used to sign messages with but won't use
        # to sign new messages. E.g. it has lost its private key
        #
        #old_signing_keys:
        #  "ed25519:auto":
        #    # Base64 encoded public key
        #    key: "The public part of your old signing key."
        #    # Millisecond POSIX timestamp when the key expired.
        #    expired_ts: 123456789123

        # How long key response published by this server is valid for.
        # Used to set the valid_until_ts in /key/v2 APIs.
        # Determines how quickly servers will query to check which keys
        # are still valid.
        #
        #key_refresh_interval: 1d

        # The trusted servers to download signing keys from.
        #
        # When we need to fetch a signing key, each server is tried in parallel.
        #
        # Normally, the connection to the key server is validated via TLS certificates.
        # Additional security can be provided by configuring a `verify key`, which
        # will make synapse check that the response is signed by that key.
        #
        # This setting supercedes an older setting named `perspectives`. The old format
        # is still supported for backwards-compatibility, but it is deprecated.
        #
        # Options for each entry in the list include:
        #
        #    server_name: the name of the server. required.
        #
        #    verify_keys: an optional map from key id to base64-encoded public key.
        #       If specified, we will check that the response is signed by at least
        #       one of the given keys.
        #
        #    accept_keys_insecurely: a boolean. Normally, if `verify_keys` is unset,
        #       and federation_verify_certificates is not `true`, synapse will refuse
        #       to start, because this would allow anyone who can spoof DNS responses
        #       to masquerade as the trusted key server. If you know what you are doing
        #       and are sure that your network environment provides a secure connection
        #       to the key server, you can set this to `true` to override this
        #       behaviour.
        #
        # An example configuration might look like:
        #
        #trusted_key_servers:
        #  - server_name: "my_trusted_server.example.com"
        #    verify_keys:
        #      "ed25519:auto": "abcdefghijklmnopqrstuvwxyzabcdefghijklmopqr"
        #  - server_name: "my_other_trusted_server.example.com"
        #
        # The default configuration is:
        #
        #trusted_key_servers:
        #  - server_name: "matrix.org"
        """
            % locals()
        )

    def read_signing_key(self, signing_key_path):
        signing_keys = self.read_file(signing_key_path, "signing_key")
        try:
            return read_signing_keys(signing_keys.splitlines(True))
        except Exception as e:
            raise ConfigError("Error reading signing_key: %s" % (str(e)))

    def read_old_signing_keys(self, old_signing_keys):
        keys = {}
        for key_id, key_data in old_signing_keys.items():
            if is_signing_algorithm_supported(key_id):
                key_base64 = key_data["key"]
                key_bytes = decode_base64(key_base64)
                verify_key = decode_verify_key_bytes(key_id, key_bytes)
                verify_key.expired_ts = key_data["expired_ts"]
                keys[key_id] = verify_key
            else:
                raise ConfigError(
                    "Unsupported signing algorithm for old key: %r" % (key_id,)
                )
        return keys

    def generate_files(self, config):
        signing_key_path = config["signing_key_path"]

        if not self.path_exists(signing_key_path):
            with open(signing_key_path, "w") as signing_key_file:
                key_id = "a_" + random_string(4)
                write_signing_keys(signing_key_file, (generate_signing_key(key_id),))
        else:
            signing_keys = self.read_file(signing_key_path, "signing_key")
            if len(signing_keys.split("\n")[0].split()) == 1:
                # handle keys in the old format.
                key_id = "a_" + random_string(4)
                key = decode_signing_key_base64(
                    NACL_ED25519, key_id, signing_keys.split("\n")[0]
                )
                with open(signing_key_path, "w") as signing_key_file:
                    write_signing_keys(signing_key_file, (key,))


def _perspectives_to_key_servers(config):
    """Convert old-style 'perspectives' configs into new-style 'trusted_key_servers'

    Returns an iterable of entries to add to trusted_key_servers.
    """

    # 'perspectives' looks like:
    #
    # {
    #     "servers": {
    #         "matrix.org": {
    #             "verify_keys": {
    #                 "ed25519:auto": {
    #                     "key": "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
    #                 }
    #             }
    #         }
    #     }
    # }
    #
    # 'trusted_keys' looks like:
    #
    # [
    #     {
    #         "server_name": "matrix.org",
    #         "verify_keys": {
    #             "ed25519:auto": "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
    #         }
    #     }
    # ]

    perspectives_servers = config.get("perspectives", {}).get("servers", {})

    for server_name, server_opts in perspectives_servers.items():
        trusted_key_server_entry = {"server_name": server_name}
        verify_keys = server_opts.get("verify_keys")
        if verify_keys is not None:
            trusted_key_server_entry["verify_keys"] = {
                key_id: key_data["key"] for key_id, key_data in verify_keys.items()
            }
        yield trusted_key_server_entry


TRUSTED_KEY_SERVERS_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "description": "schema for the trusted_key_servers setting",
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "server_name": {"type": "string"},
            "verify_keys": {
                "type": "object",
                # each key must be a base64 string
                "additionalProperties": {"type": "string"},
            },
        },
        "required": ["server_name"],
    },
}


def _parse_key_servers(key_servers, federation_verify_certificates):
    try:
        jsonschema.validate(key_servers, TRUSTED_KEY_SERVERS_SCHEMA)
    except jsonschema.ValidationError as e:
        raise ConfigError("Unable to parse 'trusted_key_servers': " + e.message)

    for server in key_servers:
        server_name = server["server_name"]
        result = TrustedKeyServer(server_name=server_name)

        verify_keys = server.get("verify_keys")
        if verify_keys is not None:
            result.verify_keys = {}
            for key_id, key_base64 in verify_keys.items():
                if not is_signing_algorithm_supported(key_id):
                    raise ConfigError(
                        "Unsupported signing algorithm on key %s for server %s in "
                        "trusted_key_servers" % (key_id, server_name)
                    )
                try:
                    key_bytes = decode_base64(key_base64)
                    verify_key = decode_verify_key_bytes(key_id, key_bytes)
                except Exception as e:
                    raise ConfigError(
                        "Unable to parse key %s for server %s in "
                        "trusted_key_servers: %s" % (key_id, server_name, e)
                    )

                result.verify_keys[key_id] = verify_key

        if not federation_verify_certificates and not server.get(
            "accept_keys_insecurely"
        ):
            _assert_keyserver_has_verify_keys(result)

        yield result


def _assert_keyserver_has_verify_keys(trusted_key_server):
    if not trusted_key_server.verify_keys:
        raise ConfigError(INSECURE_NOTARY_ERROR)

    # also check that they are not blindly checking the old matrix.org key
    if trusted_key_server.server_name == "matrix.org" and any(
        key_id == "ed25519:auto" for key_id in trusted_key_server.verify_keys
    ):
        raise ConfigError(RELYING_ON_MATRIX_KEY_ERROR)