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

Cross-signing [2/4] - upload/download keys #5769

Merged
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d1c7c2a
allow devices to be marked as "hidden"
uhoreg Jul 25, 2019
c659b9f
allow uploading keys for cross-signing
uhoreg Jul 25, 2019
781ade8
apply changes from PR review
uhoreg Jul 31, 2019
2997a91
add changelog file
uhoreg Jul 31, 2019
bc95890
Merge branch 'cross-signing_hidden' into cross-signing_keys
uhoreg Jul 31, 2019
c1f0a56
Merge branch 'develop' into cross-signing_hidden
uhoreg Jul 31, 2019
185188b
remove extra SQL query param
uhoreg Jul 31, 2019
430ea08
PostgreSQL, Y U no like?
uhoreg Jul 31, 2019
73b26f8
really fix queries to work with Postgres
uhoreg Jul 31, 2019
d78a4fe
don't need to return the hidden column any more
uhoreg Aug 1, 2019
336c546
Merge branch 'cross-signing_hidden' into cross-signing_keys
uhoreg Aug 1, 2019
fac1cdc
make changes from PR review
uhoreg Aug 2, 2019
d28d1e2
add changelog
uhoreg Aug 2, 2019
8c9adcc
fix formatting
uhoreg Aug 2, 2019
7c3abc6
apply PR review suggestions
uhoreg Aug 21, 2019
814f253
make isort happy
uhoreg Aug 21, 2019
3b0b22c
use stream ID generator instead of timestamp
uhoreg Aug 29, 2019
96bda56
black
uhoreg Aug 29, 2019
e3d3fbf
Merge branch 'uhoreg/e2e_cross-signing_merged' into cross-signing_keys
uhoreg Aug 29, 2019
e701128
Fix coverage in sytest and use plugins for buildkite (#5922)
hawkowl Aug 29, 2019
5625abe
Fix buildkite pipeline plugin matrix-org/annotate using the wrong var…
hawkowl Aug 30, 2019
3057095
Revert "Use the v2 lookup API for 3PID invites (#5897)" (#5937)
anoadragon453 Aug 30, 2019
d19505a
Removed unused jenkins/ folder and script (#5938)
hawkowl Aug 30, 2019
4765f0c
Add m.id_access_token flag (#5930)
anoadragon453 Aug 30, 2019
4fca313
Move buildkite config to the pipelines repo (#5943)
hawkowl Aug 30, 2019
4548d1f
Remove unnecessary parentheses around return statements (#5931)
anoadragon453 Aug 30, 2019
2a012e8
Revert "Add m.id_access_token flag (#5930)" (#5945)
anoadragon453 Aug 30, 2019
cee00a3
Update INSTALL.md to say that Python 2 is no longer supported (#5953)
aaronraimist Sep 2, 2019
ce7803b
fix thumbnail storage location (#5915)
L0ric0 Sep 2, 2019
36f34e6
Remove unused methods from c/s api v1 in register.py (#5963)
anoadragon453 Sep 2, 2019
a90d16d
Opentrace device lists (#5853)
JorikSchellekens Sep 3, 2019
2a44782
Remove double return statements (#5962)
anoadragon453 Sep 3, 2019
8401bcd
fix typo
ara4n Sep 3, 2019
0eac707
Ensure an auth instance is available to ListMediaInRoom (#5967)
turt2live Sep 3, 2019
894c1a5
Docker packaging should not su-exec or chmod if already running as UI…
michaelkaye Sep 3, 2019
a98b858
Remove unnecessary variable declaration
anoadragon453 Sep 3, 2019
6b6086b
Fix docstring
anoadragon453 Sep 3, 2019
2f416fc
Ensure the list media admin API is always available (#5966)
turt2live Sep 3, 2019
ea128a3
code cleanups
anoadragon453 Sep 3, 2019
6e834e9
Fix and refactor room and user stats (#5971)
erikjohnston Sep 4, 2019
b09d443
Cleanup event auth type initialisation (#5975)
anoadragon453 Sep 4, 2019
b736c6c
Remove bind_email and bind_msisdn (#5964)
anoadragon453 Sep 4, 2019
faf72a4
Merge branch 'develop' into cross-signing_keys
uhoreg Sep 4, 2019
a22d58c
add user signature stream change cache to slaved device store
uhoreg Sep 4, 2019
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 changelog.d/5759.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow devices to be marked as hidden, for use by features such as cross-signing.
1 change: 1 addition & 0 deletions changelog.d/5769.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow uploading of cross-signing keys.
1 change: 1 addition & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Codes(object):
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"


class CodeMessageException(RuntimeError):
Expand Down
17 changes: 17 additions & 0 deletions synapse/handlers/device.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 OpenMarket Ltd
# Copyright 2019 New Vector 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.
Expand Down Expand Up @@ -406,6 +408,21 @@ def notify_device_update(self, user_id, device_ids):
for host in hosts:
self.federation_sender.send_device_messages(host)

@defer.inlineCallbacks
def notify_user_signature_update(self, from_user_id, user_ids):
"""Notify a user that they have made new signatures of other users.

Args:
from_user_id (str): the user who made the signature
user_ids (list[str]): the users IDs that have new signatures
"""

position = yield self.store.add_user_signature_change_to_streams(
from_user_id, user_ids
)

self.notifier.on_new_event("device_list_key", position, users=[from_user_id])

@defer.inlineCallbacks
def on_federation_query_user_devices(self, user_id):
stream_id, devices = yield self.store.get_devices_with_keys_by_user(user_id)
Expand Down
210 changes: 205 additions & 5 deletions synapse/handlers/e2e_keys.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 OpenMarket Ltd
# Copyright 2018 New Vector Ltd
# Copyright 2018-2019 New Vector 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.
Expand All @@ -19,12 +20,17 @@
from six import iteritems

from canonicaljson import encode_canonical_json, json
from signedjson.sign import SignatureVerifyException, verify_signed_json

from twisted.internet import defer

from synapse.api.errors import CodeMessageException, SynapseError
from synapse.api.errors import CodeMessageException, Codes, SynapseError
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.types import UserID, get_domain_from_id
from synapse.types import (
UserID,
get_domain_from_id,
get_verify_key_from_cross_signing_key,
)
from synapse.util import unwrapFirstError
from synapse.util.retryutils import NotRetryingDestination

Expand All @@ -47,7 +53,7 @@ def __init__(self, hs):
)

@defer.inlineCallbacks
def query_devices(self, query_body, timeout):
def query_devices(self, query_body, timeout, from_user_id):
""" Handle a device key query from a client

{
Expand All @@ -65,6 +71,11 @@ def query_devices(self, query_body, timeout):
}
}
}

Args:
from_user_id (str): the user making the query. This is used when
adding cross-signing signatures to limit what signatures users
can see.
"""

device_keys_query = query_body.get("device_keys", {})
Expand Down Expand Up @@ -120,6 +131,11 @@ def query_devices(self, query_body, timeout):
r = remote_queries_not_in_cache.setdefault(domain, {})
r[user_id] = remote_queries[user_id]

# Get cached cross-signing keys
cross_signing_keys = yield self.query_cross_signing_keys(
device_keys_query, from_user_id
)

# Now fetch any devices that we don't have in our cache
@defer.inlineCallbacks
def do_remote_query(destination):
Expand Down Expand Up @@ -182,6 +198,14 @@ def do_remote_query(destination):
if user_id in destination_query:
results[user_id] = keys

for user_id, key in remote_result["master_keys"].items():
if user_id in destination_query:
cross_signing_keys["master"][user_id] = key

for user_id, key in remote_result["self_signing_keys"].items():
if user_id in destination_query:
cross_signing_keys["self_signing"][user_id] = key

except Exception as e:
failure = _exception_to_failure(e)
failures[destination] = failure
Expand All @@ -196,7 +220,73 @@ def do_remote_query(destination):
).addErrback(unwrapFirstError)
)

return {"device_keys": results, "failures": failures}
ret = {"device_keys": results, "failures": failures}

for key, value in iteritems(cross_signing_keys):
ret[key + "_keys"] = value
uhoreg marked this conversation as resolved.
Show resolved Hide resolved

return ret

@defer.inlineCallbacks
def query_cross_signing_keys(self, query, from_user_id):
uhoreg marked this conversation as resolved.
Show resolved Hide resolved
"""Get cross-signing keys for users
uhoreg marked this conversation as resolved.
Show resolved Hide resolved

Args:
query (Iterable[string]) an iterable of user IDs. A dict whose keys
are user IDs satisfies this, so the query format used for
query_devices can be used here.
from_user_id (str): the user making the query. This is used when
adding cross-signing signatures to limit what signatures users
can see.

Returns:
defer.Deferred[dict[str, dict[str, dict]]]: map from
(master|self_signing|user_signing) -> user_id -> key
"""
master_keys = {}
self_signing_keys = {}
user_signing_keys = {}

for user_id in query:
# XXX: consider changing the store functions to allow querying
# multiple users simultaneously.
try:
key = yield self.store.get_e2e_cross_signing_key(
user_id, "master", from_user_id
)
if key:
master_keys[user_id] = key
except Exception as e:
logger.info("Error getting master key: %s", e)
uhoreg marked this conversation as resolved.
Show resolved Hide resolved

try:
key = yield self.store.get_e2e_cross_signing_key(
user_id, "self_signing", from_user_id
)
if key:
self_signing_keys[user_id] = key
except Exception as e:
logger.info("Error getting self-signing key: %s", e)

# users can see other users' master and self-signing keys, but can
# only see their own user-signing keys
if from_user_id == user_id:
try:
key = yield self.store.get_e2e_cross_signing_key(
user_id, "user_signing", from_user_id
)
if key:
user_signing_keys[user_id] = key
except Exception as e:
logger.info("Error getting user-signing key: %s", e)

defer.returnValue(
uhoreg marked this conversation as resolved.
Show resolved Hide resolved
{
"master": master_keys,
"self_signing": self_signing_keys,
"user_signing": user_signing_keys,
}
)

@defer.inlineCallbacks
def query_local_devices(self, query):
Expand Down Expand Up @@ -397,6 +487,116 @@ def _upload_one_time_keys_for_user(

yield self.store.add_e2e_one_time_keys(user_id, device_id, time_now, new_keys)

@defer.inlineCallbacks
def upload_signing_keys_for_user(self, user_id, keys):
"""Upload signing keys for cross-signing

Args:
user_id (string): the user uploading the keys
keys (dict[string, dict]): the signing keys
"""

# if a master key is uploaded, then check it. Otherwise, load the
# stored master key, to check signatures on other keys
if "master_key" in keys:
master_key = keys["master_key"]

_check_cross_signing_key(master_key, user_id, "master")
else:
master_key = yield self.store.get_e2e_cross_signing_key(user_id, "master")

# if there is no master key, then we can't do anything, because all the
# other cross-signing keys need to be signed by the master key
if not master_key:
raise SynapseError(400, "No master key available", Codes.MISSING_PARAM)

try:
master_key_id, master_verify_key = get_verify_key_from_cross_signing_key(
master_key
)
except ValueError:
if "master_key" in keys:
# the invalid key came from the request
raise SynapseError(400, "Invalid master key", Codes.INVALID_PARAM)
else:
# the invalid key came from the database
logger.error("Invalid master key found for user %s", user_id)
raise SynapseError(500, "Invalid master key")

# for the other cross-signing keys, make sure that they have valid
# signatures from the master key
if "self_signing_key" in keys:
self_signing_key = keys["self_signing_key"]

_check_cross_signing_key(
self_signing_key, user_id, "self_signing", master_verify_key
)

if "user_signing_key" in keys:
user_signing_key = keys["user_signing_key"]

_check_cross_signing_key(
user_signing_key, user_id, "user_signing", master_verify_key
)

# if everything checks out, then store the keys and send notifications
deviceids = []
if "master_key" in keys:
yield self.store.set_e2e_cross_signing_key(user_id, "master", master_key)
deviceids.append(master_verify_key.version)
if "self_signing_key" in keys:
yield self.store.set_e2e_cross_signing_key(
user_id, "self_signing", self_signing_key
)
try:
deviceids.append(
get_verify_key_from_cross_signing_key(self_signing_key)[1].version
)
except ValueError:
raise SynapseError(400, "Invalid self-signing key", Codes.INVALID_PARAM)
if "user_signing_key" in keys:
yield self.store.set_e2e_cross_signing_key(
user_id, "user_signing", user_signing_key
)
# the signature stream matches the semantics that we want for
# user-signing key updates: only the user themselves is notified of
# their own user-signing key updates
yield self.device_handler.notify_user_signature_update(user_id, [user_id])

# master key and self-signing key updates match the semantics of device
# list updates: all users who share an encrypted room are notified
if len(deviceids):
yield self.device_handler.notify_device_update(user_id, deviceids)

defer.returnValue({})


def _check_cross_signing_key(key, user_id, key_type, signing_key=None):
"""Check a cross-signing key uploaded by a user. Performs some basic sanity
checking, and ensures that it is signed, if a signature is required.

Args:
key (dict): the key data to verify
user_id (str): the user whose key is being checked
key_type (str): the type of key that the key should be
signing_key (VerifyKey): (optional) the signing key that the key should
be signed with. If omitted, signatures will not be checked.
"""
if (
key.get("user_id") != user_id
or key_type not in key.get("usage", [])
or len(key.get("keys", {})) != 1
):
raise SynapseError(400, ("Invalid %s key" % (key_type,)), Codes.INVALID_PARAM)

if signing_key:
try:
verify_signed_json(key, user_id, signing_key)
except SignatureVerifyException:
raise SynapseError(
400, ("Invalid signature on %s key" % key_type), Codes.INVALID_SIGNATURE
)


def _exception_to_failure(e):
if isinstance(e, CodeMessageException):
Expand Down
7 changes: 6 additions & 1 deletion synapse/handlers/sync.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2018 New Vector Ltd
# Copyright 2018, 2019 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -1110,6 +1110,11 @@ def _generate_sync_entry_for_device_list(
# weren't in the previous sync *or* they left and rejoined.
users_that_have_changed.update(newly_joined_or_invited_users)

user_signatures_changed = yield self.store.get_users_whose_signatures_changed(
user_id, since_token.device_list_key
)
users_that_have_changed.update(user_signatures_changed)

# Now find users that we no longer track
for room_id in newly_left_rooms:
left_users = yield self.state.get_current_users_in_room(room_id)
Expand Down
Loading