mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-09-07 10:21:53 +00:00
Stop hardcoding trust of old matrix.org key (#5374)
There are a few changes going on here: * We make checking the signature on a key server response optional: if no verify_keys are specified, we trust to TLS to validate the connection. * We change the default config so that it does not require responses to be signed by the old key. * We replace the old 'perspectives' config with 'trusted_key_servers', which is also formatted slightly differently. * We emit a warning to the logs every time we trust a key server response signed by the old key.
This commit is contained in:
parent
833c406b9b
commit
9fbb20a531
1
changelog.d/5374.feature
Normal file
1
changelog.d/5374.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Replace the `perspectives` configuration section with `trusted_key_servers`, and make validating the signatures on responses optional (since TLS will do this job for us).
|
@ -952,12 +952,43 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
|||||||
|
|
||||||
# The trusted servers to download signing keys from.
|
# The trusted servers to download signing keys from.
|
||||||
#
|
#
|
||||||
#perspectives:
|
# When we need to fetch a signing key, each server is tried in parallel.
|
||||||
# servers:
|
#
|
||||||
# "matrix.org":
|
# 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:
|
# verify_keys:
|
||||||
# "ed25519:auto":
|
# "ed25519:auto": "abcdefghijklmnopqrstuvwxyzabcdefghijklmopqr"
|
||||||
# key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
|
# - server_name: "my_other_trusted_server.example.com"
|
||||||
|
#
|
||||||
|
# The default configuration is:
|
||||||
|
#
|
||||||
|
#trusted_key_servers:
|
||||||
|
# - server_name: "matrix.org"
|
||||||
|
|
||||||
|
|
||||||
# Enable SAML2 for registration and login. Uses pysaml2.
|
# Enable SAML2 for registration and login. Uses pysaml2.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -17,6 +18,8 @@ import hashlib
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import attr
|
||||||
|
import jsonschema
|
||||||
from signedjson.key import (
|
from signedjson.key import (
|
||||||
NACL_ED25519,
|
NACL_ED25519,
|
||||||
decode_signing_key_base64,
|
decode_signing_key_base64,
|
||||||
@ -32,11 +35,27 @@ from synapse.util.stringutils import random_string, random_string_with_symbols
|
|||||||
|
|
||||||
from ._base import Config, ConfigError
|
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."""
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class KeyConfig(Config):
|
@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):
|
def read_config(self, config):
|
||||||
# the signing key can be specified inline or in a separate file
|
# the signing key can be specified inline or in a separate file
|
||||||
if "signing_key" in config:
|
if "signing_key" in config:
|
||||||
@ -49,16 +68,27 @@ class KeyConfig(Config):
|
|||||||
config.get("old_signing_keys", {})
|
config.get("old_signing_keys", {})
|
||||||
)
|
)
|
||||||
self.key_refresh_interval = self.parse_duration(
|
self.key_refresh_interval = self.parse_duration(
|
||||||
config.get("key_refresh_interval", "1d"),
|
config.get("key_refresh_interval", "1d")
|
||||||
)
|
)
|
||||||
self.perspectives = self.read_perspectives(
|
|
||||||
config.get("perspectives", {}).get("servers", {
|
# if neither trusted_key_servers nor perspectives are given, use the default.
|
||||||
"matrix.org": {"verify_keys": {
|
if "perspectives" not in config and "trusted_key_servers" not in config:
|
||||||
"ed25519:auto": {
|
key_servers = [{"server_name": "matrix.org"}]
|
||||||
"key": "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
|
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(
|
self.macaroon_secret_key = config.get(
|
||||||
@ -78,8 +108,9 @@ class KeyConfig(Config):
|
|||||||
# falsification of values
|
# falsification of values
|
||||||
self.form_secret = config.get("form_secret", None)
|
self.form_secret = config.get("form_secret", None)
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, generate_secrets=False,
|
def default_config(
|
||||||
**kwargs):
|
self, config_dir_path, server_name, generate_secrets=False, **kwargs
|
||||||
|
):
|
||||||
base_key_name = os.path.join(config_dir_path, server_name)
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
|
|
||||||
if generate_secrets:
|
if generate_secrets:
|
||||||
@ -91,7 +122,8 @@ class KeyConfig(Config):
|
|||||||
macaroon_secret_key = "# macaroon_secret_key: <PRIVATE STRING>"
|
macaroon_secret_key = "# macaroon_secret_key: <PRIVATE STRING>"
|
||||||
form_secret = "# form_secret: <PRIVATE STRING>"
|
form_secret = "# form_secret: <PRIVATE STRING>"
|
||||||
|
|
||||||
return """\
|
return (
|
||||||
|
"""\
|
||||||
# a secret which is used to sign access tokens. If none is specified,
|
# a secret which is used to sign access tokens. If none is specified,
|
||||||
# the registration_shared_secret is used, if one is given; otherwise,
|
# the registration_shared_secret is used, if one is given; otherwise,
|
||||||
# a secret key is derived from the signing key.
|
# a secret key is derived from the signing key.
|
||||||
@ -133,33 +165,53 @@ class KeyConfig(Config):
|
|||||||
|
|
||||||
# The trusted servers to download signing keys from.
|
# The trusted servers to download signing keys from.
|
||||||
#
|
#
|
||||||
#perspectives:
|
# When we need to fetch a signing key, each server is tried in parallel.
|
||||||
# servers:
|
#
|
||||||
# "matrix.org":
|
# 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:
|
# verify_keys:
|
||||||
# "ed25519:auto":
|
# "ed25519:auto": "abcdefghijklmnopqrstuvwxyzabcdefghijklmopqr"
|
||||||
# key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
|
# - server_name: "my_other_trusted_server.example.com"
|
||||||
""" % locals()
|
#
|
||||||
|
# The default configuration is:
|
||||||
def read_perspectives(self, perspectives_servers):
|
#
|
||||||
servers = {}
|
#trusted_key_servers:
|
||||||
for server_name, server_config in perspectives_servers.items():
|
# - server_name: "matrix.org"
|
||||||
for key_id, key_data in server_config["verify_keys"].items():
|
"""
|
||||||
if is_signing_algorithm_supported(key_id):
|
% locals()
|
||||||
key_base64 = key_data["key"]
|
)
|
||||||
key_bytes = decode_base64(key_base64)
|
|
||||||
verify_key = decode_verify_key_bytes(key_id, key_bytes)
|
|
||||||
servers.setdefault(server_name, {})[key_id] = verify_key
|
|
||||||
return servers
|
|
||||||
|
|
||||||
def read_signing_key(self, signing_key_path):
|
def read_signing_key(self, signing_key_path):
|
||||||
signing_keys = self.read_file(signing_key_path, "signing_key")
|
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||||
try:
|
try:
|
||||||
return read_signing_keys(signing_keys.splitlines(True))
|
return read_signing_keys(signing_keys.splitlines(True))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ConfigError(
|
raise ConfigError("Error reading signing_key: %s" % (str(e)))
|
||||||
"Error reading signing_key: %s" % (str(e))
|
|
||||||
)
|
|
||||||
|
|
||||||
def read_old_signing_keys(self, old_signing_keys):
|
def read_old_signing_keys(self, old_signing_keys):
|
||||||
keys = {}
|
keys = {}
|
||||||
@ -182,9 +234,7 @@ class KeyConfig(Config):
|
|||||||
if not self.path_exists(signing_key_path):
|
if not self.path_exists(signing_key_path):
|
||||||
with open(signing_key_path, "w") as signing_key_file:
|
with open(signing_key_path, "w") as signing_key_file:
|
||||||
key_id = "a_" + random_string(4)
|
key_id = "a_" + random_string(4)
|
||||||
write_signing_keys(
|
write_signing_keys(signing_key_file, (generate_signing_key(key_id),))
|
||||||
signing_key_file, (generate_signing_key(key_id),),
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
signing_keys = self.read_file(signing_key_path, "signing_key")
|
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||||
if len(signing_keys.split("\n")[0].split()) == 1:
|
if len(signing_keys.split("\n")[0].split()) == 1:
|
||||||
@ -194,6 +244,106 @@ class KeyConfig(Config):
|
|||||||
NACL_ED25519, key_id, signing_keys.split("\n")[0]
|
NACL_ED25519, key_id, signing_keys.split("\n")[0]
|
||||||
)
|
)
|
||||||
with open(signing_key_path, "w") as signing_key_file:
|
with open(signing_key_path, "w") as signing_key_file:
|
||||||
write_signing_keys(
|
write_signing_keys(signing_key_file, (key,))
|
||||||
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 verify_keys
|
||||||
|
and not server.get("accept_keys_insecurely")
|
||||||
|
and not federation_verify_certificates
|
||||||
|
):
|
||||||
|
raise ConfigError(INSECURE_NOTARY_ERROR)
|
||||||
|
|
||||||
|
yield result
|
||||||
|
@ -585,25 +585,27 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
super(PerspectivesKeyFetcher, self).__init__(hs)
|
super(PerspectivesKeyFetcher, self).__init__(hs)
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.client = hs.get_http_client()
|
self.client = hs.get_http_client()
|
||||||
self.perspective_servers = self.config.perspectives
|
self.key_servers = self.config.key_servers
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_keys(self, keys_to_fetch):
|
def get_keys(self, keys_to_fetch):
|
||||||
"""see KeyFetcher.get_keys"""
|
"""see KeyFetcher.get_keys"""
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_key(perspective_name, perspective_keys):
|
def get_key(key_server):
|
||||||
try:
|
try:
|
||||||
result = yield self.get_server_verify_key_v2_indirect(
|
result = yield self.get_server_verify_key_v2_indirect(
|
||||||
keys_to_fetch, perspective_name, perspective_keys
|
keys_to_fetch, key_server
|
||||||
)
|
)
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
except KeyLookupError as e:
|
except KeyLookupError as e:
|
||||||
logger.warning("Key lookup failed from %r: %s", perspective_name, e)
|
logger.warning(
|
||||||
|
"Key lookup failed from %r: %s", key_server.server_name, e
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Unable to get key from %r: %s %s",
|
"Unable to get key from %r: %s %s",
|
||||||
perspective_name,
|
key_server.server_name,
|
||||||
type(e).__name__,
|
type(e).__name__,
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
@ -613,8 +615,8 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
results = yield logcontext.make_deferred_yieldable(
|
results = yield logcontext.make_deferred_yieldable(
|
||||||
defer.gatherResults(
|
defer.gatherResults(
|
||||||
[
|
[
|
||||||
run_in_background(get_key, p_name, p_keys)
|
run_in_background(get_key, server)
|
||||||
for p_name, p_keys in self.perspective_servers.items()
|
for server in self.key_servers
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
@ -629,17 +631,15 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_server_verify_key_v2_indirect(
|
def get_server_verify_key_v2_indirect(
|
||||||
self, keys_to_fetch, perspective_name, perspective_keys
|
self, keys_to_fetch, key_server
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
keys_to_fetch (dict[str, dict[str, int]]):
|
keys_to_fetch (dict[str, dict[str, int]]):
|
||||||
the keys to be fetched. server_name -> key_id -> min_valid_ts
|
the keys to be fetched. server_name -> key_id -> min_valid_ts
|
||||||
|
|
||||||
perspective_name (str): name of the notary server to query for the keys
|
key_server (synapse.config.key.TrustedKeyServer): notary server to query for
|
||||||
|
the keys
|
||||||
perspective_keys (dict[str, VerifyKey]): map of key_id->key for the
|
|
||||||
notary server
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred[dict[str, dict[str, synapse.storage.keys.FetchKeyResult]]]: map
|
Deferred[dict[str, dict[str, synapse.storage.keys.FetchKeyResult]]]: map
|
||||||
@ -649,6 +649,7 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
KeyLookupError if there was an error processing the entire response from
|
KeyLookupError if there was an error processing the entire response from
|
||||||
the server
|
the server
|
||||||
"""
|
"""
|
||||||
|
perspective_name = key_server.server_name
|
||||||
logger.info(
|
logger.info(
|
||||||
"Requesting keys %s from notary server %s",
|
"Requesting keys %s from notary server %s",
|
||||||
keys_to_fetch.items(),
|
keys_to_fetch.items(),
|
||||||
@ -689,11 +690,13 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processed_response = yield self._process_perspectives_response(
|
self._validate_perspectives_response(
|
||||||
perspective_name,
|
key_server,
|
||||||
perspective_keys,
|
|
||||||
response,
|
response,
|
||||||
time_added_ms=time_now_ms,
|
)
|
||||||
|
|
||||||
|
processed_response = yield self.process_v2_response(
|
||||||
|
perspective_name, response, time_added_ms=time_now_ms
|
||||||
)
|
)
|
||||||
except KeyLookupError as e:
|
except KeyLookupError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@ -717,28 +720,24 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
|
|
||||||
defer.returnValue(keys)
|
defer.returnValue(keys)
|
||||||
|
|
||||||
def _process_perspectives_response(
|
def _validate_perspectives_response(
|
||||||
self, perspective_name, perspective_keys, response, time_added_ms
|
self, key_server, response,
|
||||||
):
|
):
|
||||||
"""Parse a 'Server Keys' structure from the result of a /key/query request
|
"""Optionally check the signature on the result of a /key/query request
|
||||||
|
|
||||||
Checks that the entry is correctly signed by the perspectives server, and then
|
|
||||||
passes over to process_v2_response
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
perspective_name (str): the name of the notary server that produced this
|
key_server (synapse.config.key.TrustedKeyServer): the notary server that
|
||||||
result
|
produced this result
|
||||||
|
|
||||||
perspective_keys (dict[str, VerifyKey]): map of key_id->key for the
|
|
||||||
notary server
|
|
||||||
|
|
||||||
response (dict): the json-decoded Server Keys response object
|
response (dict): the json-decoded Server Keys response object
|
||||||
|
|
||||||
time_added_ms (int): the timestamp to record in server_keys_json
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred[dict[str, FetchKeyResult]]: map from key_id to result object
|
|
||||||
"""
|
"""
|
||||||
|
perspective_name = key_server.server_name
|
||||||
|
perspective_keys = key_server.verify_keys
|
||||||
|
|
||||||
|
if perspective_keys is None:
|
||||||
|
# signature checking is disabled on this server
|
||||||
|
return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
u"signatures" not in response
|
u"signatures" not in response
|
||||||
or perspective_name not in response[u"signatures"]
|
or perspective_name not in response[u"signatures"]
|
||||||
@ -751,6 +750,13 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
verify_signed_json(response, perspective_name, perspective_keys[key_id])
|
verify_signed_json(response, perspective_name, perspective_keys[key_id])
|
||||||
verified = True
|
verified = True
|
||||||
|
|
||||||
|
if perspective_name == "matrix.org" and key_id == "ed25519:auto":
|
||||||
|
logger.warning(
|
||||||
|
"Trusting trusted_key_server responses signed by the "
|
||||||
|
"compromised matrix.org signing key 'ed25519:auto'. "
|
||||||
|
"This is a placebo."
|
||||||
|
)
|
||||||
|
|
||||||
if not verified:
|
if not verified:
|
||||||
raise KeyLookupError(
|
raise KeyLookupError(
|
||||||
"Response not signed with a known key: signed with: %r, known keys: %r"
|
"Response not signed with a known key: signed with: %r, known keys: %r"
|
||||||
@ -760,10 +766,6 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.process_v2_response(
|
|
||||||
perspective_name, response, time_added_ms=time_added_ms
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ServerKeyFetcher(BaseV2KeyFetcher):
|
class ServerKeyFetcher(BaseV2KeyFetcher):
|
||||||
"""KeyFetcher impl which fetches keys from the origin servers"""
|
"""KeyFetcher impl which fetches keys from the origin servers"""
|
||||||
|
@ -19,7 +19,7 @@ from mock import Mock
|
|||||||
import canonicaljson
|
import canonicaljson
|
||||||
import signedjson.key
|
import signedjson.key
|
||||||
import signedjson.sign
|
import signedjson.sign
|
||||||
from signedjson.key import get_verify_key
|
from signedjson.key import encode_verify_key_base64, get_verify_key
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ class MockPerspectiveServer(object):
|
|||||||
|
|
||||||
def get_verify_keys(self):
|
def get_verify_keys(self):
|
||||||
vk = signedjson.key.get_verify_key(self.key)
|
vk = signedjson.key.get_verify_key(self.key)
|
||||||
return {"%s:%s" % (vk.alg, vk.version): vk}
|
return {"%s:%s" % (vk.alg, vk.version): encode_verify_key_base64(vk)}
|
||||||
|
|
||||||
def get_signed_key(self, server_name, verify_key):
|
def get_signed_key(self, server_name, verify_key):
|
||||||
key_id = "%s:%s" % (verify_key.alg, verify_key.version)
|
key_id = "%s:%s" % (verify_key.alg, verify_key.version)
|
||||||
@ -48,9 +48,7 @@ class MockPerspectiveServer(object):
|
|||||||
"server_name": server_name,
|
"server_name": server_name,
|
||||||
"old_verify_keys": {},
|
"old_verify_keys": {},
|
||||||
"valid_until_ts": time.time() * 1000 + 3600,
|
"valid_until_ts": time.time() * 1000 + 3600,
|
||||||
"verify_keys": {
|
"verify_keys": {key_id: {"key": encode_verify_key_base64(verify_key)}},
|
||||||
key_id: {"key": signedjson.key.encode_verify_key_base64(verify_key)}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
self.sign_response(res)
|
self.sign_response(res)
|
||||||
return res
|
return res
|
||||||
@ -63,10 +61,18 @@ class KeyringTestCase(unittest.HomeserverTestCase):
|
|||||||
def make_homeserver(self, reactor, clock):
|
def make_homeserver(self, reactor, clock):
|
||||||
self.mock_perspective_server = MockPerspectiveServer()
|
self.mock_perspective_server = MockPerspectiveServer()
|
||||||
self.http_client = Mock()
|
self.http_client = Mock()
|
||||||
hs = self.setup_test_homeserver(handlers=None, http_client=self.http_client)
|
|
||||||
keys = self.mock_perspective_server.get_verify_keys()
|
config = self.default_config()
|
||||||
hs.config.perspectives = {self.mock_perspective_server.server_name: keys}
|
config["trusted_key_servers"] = [
|
||||||
return hs
|
{
|
||||||
|
"server_name": self.mock_perspective_server.server_name,
|
||||||
|
"verify_keys": self.mock_perspective_server.get_verify_keys(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.setup_test_homeserver(
|
||||||
|
handlers=None, http_client=self.http_client, config=config
|
||||||
|
)
|
||||||
|
|
||||||
def check_context(self, _, expected):
|
def check_context(self, _, expected):
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
@ -371,10 +377,18 @@ class PerspectivesKeyFetcherTestCase(unittest.HomeserverTestCase):
|
|||||||
def make_homeserver(self, reactor, clock):
|
def make_homeserver(self, reactor, clock):
|
||||||
self.mock_perspective_server = MockPerspectiveServer()
|
self.mock_perspective_server = MockPerspectiveServer()
|
||||||
self.http_client = Mock()
|
self.http_client = Mock()
|
||||||
hs = self.setup_test_homeserver(handlers=None, http_client=self.http_client)
|
|
||||||
keys = self.mock_perspective_server.get_verify_keys()
|
config = self.default_config()
|
||||||
hs.config.perspectives = {self.mock_perspective_server.server_name: keys}
|
config["trusted_key_servers"] = [
|
||||||
return hs
|
{
|
||||||
|
"server_name": self.mock_perspective_server.server_name,
|
||||||
|
"verify_keys": self.mock_perspective_server.get_verify_keys(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.setup_test_homeserver(
|
||||||
|
handlers=None, http_client=self.http_client, config=config
|
||||||
|
)
|
||||||
|
|
||||||
def test_get_keys_from_perspectives(self):
|
def test_get_keys_from_perspectives(self):
|
||||||
# arbitrarily advance the clock a bit
|
# arbitrarily advance the clock a bit
|
||||||
@ -439,8 +453,7 @@ class PerspectivesKeyFetcherTestCase(unittest.HomeserverTestCase):
|
|||||||
self.assertEqual(res["ts_valid_until_ms"], VALID_UNTIL_TS)
|
self.assertEqual(res["ts_valid_until_ms"], VALID_UNTIL_TS)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bytes(res["key_json"]),
|
bytes(res["key_json"]), canonicaljson.encode_canonical_json(response)
|
||||||
canonicaljson.encode_canonical_json(response),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_perspectives_responses(self):
|
def test_invalid_perspectives_responses(self):
|
||||||
|
@ -57,6 +57,7 @@ class MatrixFederationAgentTests(TestCase):
|
|||||||
# present will not be trusted. We should do better here, though.
|
# present will not be trusted. We should do better here, though.
|
||||||
config_dict = default_config("test", parse=False)
|
config_dict = default_config("test", parse=False)
|
||||||
config_dict["federation_verify_certificates"] = False
|
config_dict["federation_verify_certificates"] = False
|
||||||
|
config_dict["trusted_key_servers"] = []
|
||||||
config = HomeServerConfig()
|
config = HomeServerConfig()
|
||||||
config.parse_config_dict(config_dict)
|
config.parse_config_dict(config_dict)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user