mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-21 02:44:00 +01:00
Implementation of MSC3967: Don't require UIA for initial upload of cross signing keys (#15077)
This commit is contained in:
parent
2b78981736
commit
916b8061d2
5 changed files with 182 additions and 9 deletions
1
changelog.d/15077.feature
Normal file
1
changelog.d/15077.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Experimental support for MSC3967 to not require UIA for setting up cross-signing on first use.
|
|
@ -194,3 +194,6 @@ class ExperimentalConfig(Config):
|
|||
self.msc3966_exact_event_property_contains = experimental.get(
|
||||
"msc3966_exact_event_property_contains", False
|
||||
)
|
||||
|
||||
# MSC3967: Do not require UIA when first uploading cross signing keys
|
||||
self.msc3967_enabled = experimental.get("msc3967_enabled", False)
|
||||
|
|
|
@ -1301,6 +1301,20 @@ class E2eKeysHandler:
|
|||
|
||||
return desired_key_data
|
||||
|
||||
async def is_cross_signing_set_up_for_user(self, user_id: str) -> bool:
|
||||
"""Checks if the user has cross-signing set up
|
||||
|
||||
Args:
|
||||
user_id: The user to check
|
||||
|
||||
Returns:
|
||||
True if the user has cross-signing set up, False otherwise
|
||||
"""
|
||||
existing_master_key = await self.store.get_e2e_cross_signing_key(
|
||||
user_id, "master"
|
||||
)
|
||||
return existing_master_key is not None
|
||||
|
||||
|
||||
def _check_cross_signing_key(
|
||||
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None
|
||||
|
|
|
@ -312,6 +312,20 @@ class SigningKeyUploadServlet(RestServlet):
|
|||
user_id = requester.user.to_string()
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
if self.hs.config.experimental.msc3967_enabled:
|
||||
if await self.e2e_keys_handler.is_cross_signing_set_up_for_user(user_id):
|
||||
# If we already have a master key then cross signing is set up and we require UIA to reset
|
||||
await self.auth_handler.validate_user_via_ui_auth(
|
||||
requester,
|
||||
request,
|
||||
body,
|
||||
"reset the device signing key on your account",
|
||||
# Do not allow skipping of UIA auth.
|
||||
can_skip_ui_auth=False,
|
||||
)
|
||||
# Otherwise we don't require UIA since we are setting up cross signing for first time
|
||||
else:
|
||||
# Previous behaviour is to always require UIA but allow it to be skipped
|
||||
await self.auth_handler.validate_user_via_ui_auth(
|
||||
requester,
|
||||
request,
|
||||
|
|
|
@ -14,12 +14,21 @@
|
|||
|
||||
from http import HTTPStatus
|
||||
|
||||
from signedjson.key import (
|
||||
encode_verify_key_base64,
|
||||
generate_signing_key,
|
||||
get_verify_key,
|
||||
)
|
||||
from signedjson.sign import sign_json
|
||||
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import keys, login
|
||||
from synapse.types import JsonDict
|
||||
|
||||
from tests import unittest
|
||||
from tests.http.server._base import make_request_with_cancellation_test
|
||||
from tests.unittest import override_config
|
||||
|
||||
|
||||
class KeyQueryTestCase(unittest.HomeserverTestCase):
|
||||
|
@ -118,3 +127,135 @@ class KeyQueryTestCase(unittest.HomeserverTestCase):
|
|||
|
||||
self.assertEqual(200, channel.code, msg=channel.result["body"])
|
||||
self.assertIn(bob, channel.json_body["device_keys"])
|
||||
|
||||
def make_device_keys(self, user_id: str, device_id: str) -> JsonDict:
|
||||
# We only generate a master key to simplify the test.
|
||||
master_signing_key = generate_signing_key(device_id)
|
||||
master_verify_key = encode_verify_key_base64(get_verify_key(master_signing_key))
|
||||
|
||||
return {
|
||||
"master_key": sign_json(
|
||||
{
|
||||
"user_id": user_id,
|
||||
"usage": ["master"],
|
||||
"keys": {"ed25519:" + master_verify_key: master_verify_key},
|
||||
},
|
||||
user_id,
|
||||
master_signing_key,
|
||||
),
|
||||
}
|
||||
|
||||
def test_device_signing_with_uia(self) -> None:
|
||||
"""Device signing key upload requires UIA."""
|
||||
password = "wonderland"
|
||||
device_id = "ABCDEFGHI"
|
||||
alice_id = self.register_user("alice", password)
|
||||
alice_token = self.login("alice", password, device_id=device_id)
|
||||
|
||||
content = self.make_device_keys(alice_id, device_id)
|
||||
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
content,
|
||||
alice_token,
|
||||
)
|
||||
|
||||
self.assertEqual(channel.code, HTTPStatus.UNAUTHORIZED, channel.result)
|
||||
# Grab the session
|
||||
session = channel.json_body["session"]
|
||||
# Ensure that flows are what is expected.
|
||||
self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
|
||||
|
||||
# add UI auth
|
||||
content["auth"] = {
|
||||
"type": "m.login.password",
|
||||
"identifier": {"type": "m.id.user", "user": alice_id},
|
||||
"password": password,
|
||||
"session": session,
|
||||
}
|
||||
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
content,
|
||||
alice_token,
|
||||
)
|
||||
|
||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||
|
||||
@override_config({"ui_auth": {"session_timeout": "15m"}})
|
||||
def test_device_signing_with_uia_session_timeout(self) -> None:
|
||||
"""Device signing key upload requires UIA buy passes with grace period."""
|
||||
password = "wonderland"
|
||||
device_id = "ABCDEFGHI"
|
||||
alice_id = self.register_user("alice", password)
|
||||
alice_token = self.login("alice", password, device_id=device_id)
|
||||
|
||||
content = self.make_device_keys(alice_id, device_id)
|
||||
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
content,
|
||||
alice_token,
|
||||
)
|
||||
|
||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"experimental_features": {"msc3967_enabled": True},
|
||||
"ui_auth": {"session_timeout": "15s"},
|
||||
}
|
||||
)
|
||||
def test_device_signing_with_msc3967(self) -> None:
|
||||
"""Device signing key follows MSC3967 behaviour when enabled."""
|
||||
password = "wonderland"
|
||||
device_id = "ABCDEFGHI"
|
||||
alice_id = self.register_user("alice", password)
|
||||
alice_token = self.login("alice", password, device_id=device_id)
|
||||
|
||||
keys1 = self.make_device_keys(alice_id, device_id)
|
||||
|
||||
# Initial request should succeed as no existing keys are present.
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
keys1,
|
||||
alice_token,
|
||||
)
|
||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||
|
||||
keys2 = self.make_device_keys(alice_id, device_id)
|
||||
|
||||
# Subsequent request should require UIA as keys already exist even though session_timeout is set.
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
keys2,
|
||||
alice_token,
|
||||
)
|
||||
self.assertEqual(channel.code, HTTPStatus.UNAUTHORIZED, channel.result)
|
||||
|
||||
# Grab the session
|
||||
session = channel.json_body["session"]
|
||||
# Ensure that flows are what is expected.
|
||||
self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
|
||||
|
||||
# add UI auth
|
||||
keys2["auth"] = {
|
||||
"type": "m.login.password",
|
||||
"identifier": {"type": "m.id.user", "user": alice_id},
|
||||
"password": password,
|
||||
"session": session,
|
||||
}
|
||||
|
||||
# Request should complete
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
keys2,
|
||||
alice_token,
|
||||
)
|
||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||
|
|
Loading…
Reference in a new issue