mirror of
				https://github.com/matrix-org/synapse.git
				synced 2025-10-31 04:08:21 +00: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
					
				
							
								
								
									
										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,15 +312,29 @@ class SigningKeyUploadServlet(RestServlet): | ||||
|         user_id = requester.user.to_string() | ||||
|         body = parse_json_object_from_request(request) | ||||
| 
 | ||||
|         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, | ||||
|         ) | ||||
|         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, | ||||
|                 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) | ||||
|         return 200, result | ||||
|  | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user