Implementation of MSC3967: Don't require UIA for initial upload of cross signing keys (#15077)
This commit is contained in:
parent
2b78981736
commit
916b8061d2
|
@ -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(
|
self.msc3966_exact_event_property_contains = experimental.get(
|
||||||
"msc3966_exact_event_property_contains", False
|
"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
|
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(
|
def _check_cross_signing_key(
|
||||||
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None
|
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None
|
||||||
|
|
|
@ -312,15 +312,29 @@ class SigningKeyUploadServlet(RestServlet):
|
||||||
user_id = requester.user.to_string()
|
user_id = requester.user.to_string()
|
||||||
body = parse_json_object_from_request(request)
|
body = parse_json_object_from_request(request)
|
||||||
|
|
||||||
await self.auth_handler.validate_user_via_ui_auth(
|
if self.hs.config.experimental.msc3967_enabled:
|
||||||
requester,
|
if await self.e2e_keys_handler.is_cross_signing_set_up_for_user(user_id):
|
||||||
request,
|
# If we already have a master key then cross signing is set up and we require UIA to reset
|
||||||
body,
|
await self.auth_handler.validate_user_via_ui_auth(
|
||||||
"add a device signing key to your account",
|
requester,
|
||||||
# Allow skipping of UI auth since this is frequently called directly
|
request,
|
||||||
# after login and it is silly to ask users to re-auth immediately.
|
body,
|
||||||
can_skip_ui_auth=True,
|
"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,
|
||||||
|
body,
|
||||||
|
"add a device signing key to your account",
|
||||||
|
# Allow skipping of UI auth since this is frequently called directly
|
||||||
|
# after login and it is silly to ask users to re-auth immediately.
|
||||||
|
can_skip_ui_auth=True,
|
||||||
|
)
|
||||||
|
|
||||||
result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body)
|
result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body)
|
||||||
return 200, result
|
return 200, result
|
||||||
|
|
|
@ -14,12 +14,21 @@
|
||||||
|
|
||||||
from http import HTTPStatus
|
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.api.errors import Codes
|
||||||
from synapse.rest import admin
|
from synapse.rest import admin
|
||||||
from synapse.rest.client import keys, login
|
from synapse.rest.client import keys, login
|
||||||
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
from tests.http.server._base import make_request_with_cancellation_test
|
from tests.http.server._base import make_request_with_cancellation_test
|
||||||
|
from tests.unittest import override_config
|
||||||
|
|
||||||
|
|
||||||
class KeyQueryTestCase(unittest.HomeserverTestCase):
|
class KeyQueryTestCase(unittest.HomeserverTestCase):
|
||||||
|
@ -118,3 +127,135 @@ class KeyQueryTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
self.assertEqual(200, channel.code, msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.result["body"])
|
||||||
self.assertIn(bob, channel.json_body["device_keys"])
|
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 New Issue