Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Key distribution v2 #129

Merged
merged 26 commits into from
Apr 29, 2015
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d488463
Add a version 2 of the key server api
Apr 14, 2015
32e14d8
Return a sha256 fingerprint rather than the entire tls certificate
Apr 14, 2015
8d76113
Fail quicker for 4xx responses in the key client, optional hit a diff…
Apr 15, 2015
a429515
Add methods for storing and retrieving the raw key json
Apr 15, 2015
2f9157b
Implement v2 key lookup
Apr 20, 2015
db8d4e8
Merge branch 'develop' into key_distribution
Apr 20, 2015
3ba522b
Merge branch 'develop' into key_distribution
Apr 21, 2015
f30d47c
Implement remote key lookup api
Apr 22, 2015
4bbf715
Update to match the specification for key/v2
Apr 23, 2015
149ed9f
Better help for the old-signing-key option
Apr 24, 2015
c8c710e
Move the key related config parser into a separate file
Apr 24, 2015
eede182
Merge branch 'develop' into key_distribution
Apr 24, 2015
31e262e
Copyright notice
Apr 24, 2015
b1e68ad
Add a config file for perspective servers
Apr 24, 2015
b2c2dc8
Merge branch 'develop' into key_distribution
Apr 24, 2015
c253b14
Merge branch 'develop' into key_distribution
Apr 24, 2015
2887021
Add config for setting the perspective servers
Apr 24, 2015
f8b8652
Merge branch 'develop' into key_distribution
Apr 27, 2015
b96c133
Add server_keys.sql to the current delta rather than creating a new d…
Apr 28, 2015
55e1bc8
And don't bump the schema version unnecessarily
Apr 28, 2015
46d200a
Implement minimum_valid_until_ts in the remote key resource
Apr 29, 2015
74874ff
Update the query format used by keyring to match current key v2 spec
Apr 29, 2015
4ad8b45
Merge branch 'develop' into key_distribution
Apr 29, 2015
a9549fd
Use bytea rather than BLOB
Apr 29, 2015
1319905
Use a defer.gatherResults to collect results from the perspective ser…
Apr 29, 2015
e26a3d8
bump database schema version
Apr 29, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions synapse/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
WEB_CLIENT_PREFIX = "/_matrix/client"
CONTENT_REPO_PREFIX = "/_matrix/content"
SERVER_KEY_PREFIX = "/_matrix/key/v1"
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
MEDIA_PREFIX = "/_matrix/media/v1"
APP_SERVICE_PREFIX = "/_matrix/appservice/v1"
8 changes: 7 additions & 1 deletion synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@
from synapse.rest.media.v0.content_repository import ContentRepoResource
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
from synapse.rest.key.v1.server_key_resource import LocalKey
from synapse.rest.key.v2 import KeyApiV2Resource
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
from synapse.api.urls import (
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
SERVER_KEY_PREFIX, MEDIA_PREFIX, CLIENT_V2_ALPHA_PREFIX, STATIC_PREFIX
SERVER_KEY_PREFIX, MEDIA_PREFIX, CLIENT_V2_ALPHA_PREFIX, STATIC_PREFIX,
SERVER_KEY_V2_PREFIX,
)
from synapse.config.homeserver import HomeServerConfig
from synapse.crypto import context_factory
Expand Down Expand Up @@ -97,6 +99,9 @@ def build_resource_for_media_repository(self):
def build_resource_for_server_key(self):
return LocalKey(self)

def build_resource_for_server_key_v2(self):
return KeyApiV2Resource(self)

def build_resource_for_metrics(self):
if self.get_config().enable_metrics:
return MetricsResource(self)
Expand Down Expand Up @@ -134,6 +139,7 @@ def create_resource_tree(self, redirect_root_to_web_client):
(FEDERATION_PREFIX, self.get_resource_for_federation()),
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo()),
(SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
(SERVER_KEY_V2_PREFIX, self.get_resource_for_server_key_v2()),
(MEDIA_PREFIX, self.get_resource_for_media_repository()),
(STATIC_PREFIX, self.get_resource_for_static_content()),
]
Expand Down
11 changes: 11 additions & 0 deletions synapse/config/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ def read_file(cls, file_path, config_name):
with open(file_path) as file_stream:
return file_stream.read()

@classmethod
def read_yaml_file(cls, file_path, config_name):
cls.check_file(file_path, config_name)
with open(file_path) as file_stream:
try:
return yaml.load(file_stream)
except:
raise ConfigError(
"Error parsing yaml in file %r" % (file_path,)
)

@staticmethod
def default_path(name):
return os.path.abspath(os.path.join(os.path.curdir, name))
Expand Down
3 changes: 2 additions & 1 deletion synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
from .registration import RegistrationConfig
from .metrics import MetricsConfig
from .appservice import AppServiceConfig
from .key import KeyConfig


class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
VoipConfig, RegistrationConfig,
MetricsConfig, AppServiceConfig,):
MetricsConfig, AppServiceConfig, KeyConfig,):
pass


Expand Down
147 changes: 147 additions & 0 deletions synapse/config/key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# Copyright 2015 OpenMarket Ltd
#
# 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 os
from ._base import Config, ConfigError
import syutil.crypto.signing_key
from syutil.crypto.signing_key import (
is_signing_algorithm_supported, decode_verify_key_bytes
)
from syutil.base64util import decode_base64


class KeyConfig(Config):

def __init__(self, args):
super(KeyConfig, self).__init__(args)
self.signing_key = self.read_signing_key(args.signing_key_path)
self.old_signing_keys = self.read_old_signing_keys(
args.old_signing_key_path
)
self.key_refresh_interval = args.key_refresh_interval
self.perspectives = self.read_perspectives(
args.perspectives_config_path
)

@classmethod
def add_arguments(cls, parser):
super(KeyConfig, cls).add_arguments(parser)
key_group = parser.add_argument_group("keys")
key_group.add_argument("--signing-key-path",
help="The signing key to sign messages with")
key_group.add_argument("--old-signing-key-path",
help="The keys that the server used to sign"
" sign messages with but won't use"
" to sign new messages. E.g. it has"
" lost its private key")
key_group.add_argument("--key-refresh-interval",
default=24 * 60 * 60 * 1000, # 1 Day
help="How long a key response is valid for."
" Used to set the exipiry in /key/v2/."
" Controls how frequently servers will"
" query what keys are still valid")
key_group.add_argument("--perspectives-config-path",
help="The trusted servers to download signing"
" keys from")

def read_perspectives(self, perspectives_config_path):
config = self.read_yaml_file(
perspectives_config_path, "perspectives_config_path"
)
servers = {}
for server_name, server_config in config["servers"].items():
for key_id, key_data in server_config["verify_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)
servers.setdefault(server_name, {})[key_id] = verify_key
return servers

def read_signing_key(self, signing_key_path):
signing_keys = self.read_file(signing_key_path, "signing_key")
try:
return syutil.crypto.signing_key.read_signing_keys(
signing_keys.splitlines(True)
)
except Exception:
raise ConfigError(
"Error reading signing_key."
" Try running again with --generate-config"
)

def read_old_signing_keys(self, old_signing_key_path):
old_signing_keys = self.read_file(
old_signing_key_path, "old_signing_key"
)
try:
return syutil.crypto.signing_key.read_old_signing_keys(
old_signing_keys.splitlines(True)
)
except Exception:
raise ConfigError(
"Error reading old signing keys."
)

@classmethod
def generate_config(cls, args, config_dir_path):
super(KeyConfig, cls).generate_config(args, config_dir_path)
base_key_name = os.path.join(config_dir_path, args.server_name)

args.pid_file = os.path.abspath(args.pid_file)

if not args.signing_key_path:
args.signing_key_path = base_key_name + ".signing.key"

if not os.path.exists(args.signing_key_path):
with open(args.signing_key_path, "w") as signing_key_file:
syutil.crypto.signing_key.write_signing_keys(
signing_key_file,
(syutil.crypto.signing_key.generate_signing_key("auto"),),
)
else:
signing_keys = cls.read_file(args.signing_key_path, "signing_key")
if len(signing_keys.split("\n")[0].split()) == 1:
# handle keys in the old format.
key = syutil.crypto.signing_key.decode_signing_key_base64(
syutil.crypto.signing_key.NACL_ED25519,
"auto",
signing_keys.split("\n")[0]
)
with open(args.signing_key_path, "w") as signing_key_file:
syutil.crypto.signing_key.write_signing_keys(
signing_key_file,
(key,),
)

if not args.old_signing_key_path:
args.old_signing_key_path = base_key_name + ".old.signing.keys"

if not os.path.exists(args.old_signing_key_path):
with open(args.old_signing_key_path, "w"):
pass

if not args.perspectives_config_path:
args.perspectives_config_path = base_key_name + ".perspectives"

if not os.path.exists(args.perspectives_config_path):
with open(args.perspectives_config_path, "w") as perspectives_file:
perspectives_file.write(
'servers:\n'
' matrix.org:\n'
' verify_keys:\n'
' "ed25519:auto":\n'
' key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"\n'
)
50 changes: 1 addition & 49 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
from ._base import Config, ConfigError
import syutil.crypto.signing_key
from ._base import Config


class ServerConfig(Config):
def __init__(self, args):
super(ServerConfig, self).__init__(args)
self.server_name = args.server_name
self.signing_key = self.read_signing_key(args.signing_key_path)
self.bind_port = args.bind_port
self.bind_host = args.bind_host
self.unsecure_port = args.unsecure_port
Expand Down Expand Up @@ -53,8 +50,6 @@ def add_arguments(cls, parser):
"This is used by remote servers to connect to this server, "
"e.g. matrix.org, localhost:8080, etc."
)
server_group.add_argument("--signing-key-path",
help="The signing key to sign messages with")
server_group.add_argument("-p", "--bind-port", metavar="PORT",
type=int, help="https port to listen on",
default=8448)
Expand Down Expand Up @@ -83,46 +78,3 @@ def add_arguments(cls, parser):
"Zero is used to indicate synapse "
"should set the soft limit to the hard"
"limit.")

def read_signing_key(self, signing_key_path):
signing_keys = self.read_file(signing_key_path, "signing_key")
try:
return syutil.crypto.signing_key.read_signing_keys(
signing_keys.splitlines(True)
)
except Exception:
raise ConfigError(
"Error reading signing_key."
" Try running again with --generate-config"
)

@classmethod
def generate_config(cls, args, config_dir_path):
super(ServerConfig, cls).generate_config(args, config_dir_path)
base_key_name = os.path.join(config_dir_path, args.server_name)

args.pid_file = os.path.abspath(args.pid_file)

if not args.signing_key_path:
args.signing_key_path = base_key_name + ".signing.key"

if not os.path.exists(args.signing_key_path):
with open(args.signing_key_path, "w") as signing_key_file:
syutil.crypto.signing_key.write_signing_keys(
signing_key_file,
(syutil.crypto.signing_key.generate_signing_key("auto"),),
)
else:
signing_keys = cls.read_file(args.signing_key_path, "signing_key")
if len(signing_keys.split("\n")[0].split()) == 1:
# handle keys in the old format.
key = syutil.crypto.signing_key.decode_signing_key_base64(
syutil.crypto.signing_key.NACL_ED25519,
"auto",
signing_keys.split("\n")[0]
)
with open(args.signing_key_path, "w") as signing_key_file:
syutil.crypto.signing_key.write_signing_keys(
signing_key_file,
(key,),
)
37 changes: 31 additions & 6 deletions synapse/crypto/keyclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@

logger = logging.getLogger(__name__)

KEY_API_V1 = b"/_matrix/key/v1/"


@defer.inlineCallbacks
def fetch_server_key(server_name, ssl_context_factory):
def fetch_server_key(server_name, ssl_context_factory, path=KEY_API_V1):
"""Fetch the keys for a remote server."""

factory = SynapseKeyClientFactory()
factory.path = path
endpoint = matrix_federation_endpoint(
reactor, server_name, ssl_context_factory, timeout=30
)
Expand All @@ -42,13 +45,19 @@ def fetch_server_key(server_name, ssl_context_factory):
server_response, server_certificate = yield protocol.remote_key
defer.returnValue((server_response, server_certificate))
return
except SynapseKeyClientError as e:
logger.exception("Error getting key for %r" % (server_name,))
if e.status.startswith("4"):
# Don't retry for 4xx responses.
raise IOError("Cannot get key for %r" % server_name)
except Exception as e:
logger.exception(e)
raise IOError("Cannot get key for %s" % server_name)
raise IOError("Cannot get key for %r" % server_name)


class SynapseKeyClientError(Exception):
"""The key wasn't retrieved from the remote server."""
status = None
pass


Expand All @@ -66,17 +75,30 @@ def __init__(self):
def connectionMade(self):
self.host = self.transport.getHost()
logger.debug("Connected to %s", self.host)
self.sendCommand(b"GET", b"/_matrix/key/v1/")
self.sendCommand(b"GET", self.path)
self.endHeaders()
self.timer = reactor.callLater(
self.timeout,
self.on_timeout
)

def errback(self, error):
if not self.remote_key.called:
self.remote_key.errback(error)

def callback(self, result):
if not self.remote_key.called:
self.remote_key.callback(result)

def handleStatus(self, version, status, message):
if status != b"200":
# logger.info("Non-200 response from %s: %s %s",
# self.transport.getHost(), status, message)
error = SynapseKeyClientError(
"Non-200 response %r from %r" % (status, self.host)
)
error.status = status
self.errback(error)
self.transport.abortConnection()

def handleResponse(self, response_body_bytes):
Expand All @@ -89,15 +111,18 @@ def handleResponse(self, response_body_bytes):
return

certificate = self.transport.getPeerCertificate()
self.remote_key.callback((json_response, certificate))
self.callback((json_response, certificate))
self.transport.abortConnection()
self.timer.cancel()

def on_timeout(self):
logger.debug("Timeout waiting for response from %s", self.host)
self.remote_key.errback(IOError("Timeout waiting for response"))
self.errback(IOError("Timeout waiting for response"))
self.transport.abortConnection()


class SynapseKeyClientFactory(Factory):
protocol = SynapseKeyClientProtocol
def protocol(self):
protocol = SynapseKeyClientProtocol()
protocol.path = self.path
return protocol
Loading