Add MSC4243 flag, verify event signatures from the user ID

This commit is contained in:
Kegan Dougal
2025-09-09 13:11:34 +01:00
parent 39e4f27347
commit aefeb3cb58
4 changed files with 150 additions and 15 deletions

View File

@@ -116,6 +116,8 @@ class RoomVersion:
msc4289_creator_power_enabled: bool
# MSC4291: Room IDs as hashes of the create event
msc4291_room_ids_as_hashes: bool
# MSC4243: User ID localparts as Account Keys
msc4243_account_keys: bool
class RoomVersions:
@@ -140,6 +142,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V2 = RoomVersion(
"2",
@@ -162,6 +165,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V3 = RoomVersion(
"3",
@@ -184,6 +188,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V4 = RoomVersion(
"4",
@@ -206,6 +211,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V5 = RoomVersion(
"5",
@@ -228,6 +234,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V6 = RoomVersion(
"6",
@@ -250,6 +257,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V7 = RoomVersion(
"7",
@@ -272,6 +280,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V8 = RoomVersion(
"8",
@@ -294,6 +303,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V9 = RoomVersion(
"9",
@@ -316,6 +326,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V10 = RoomVersion(
"10",
@@ -338,6 +349,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
MSC1767v10 = RoomVersion(
# MSC1767 (Extensible Events) based on room version "10"
@@ -361,6 +373,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
MSC3757v10 = RoomVersion(
# MSC3757 (Restricting who can overwrite a state event) based on room version "10"
@@ -384,6 +397,7 @@ class RoomVersions:
msc3757_enabled=True,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
V11 = RoomVersion(
"11",
@@ -406,6 +420,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
MSC3757v11 = RoomVersion(
# MSC3757 (Restricting who can overwrite a state event) based on room version "11"
@@ -429,6 +444,7 @@ class RoomVersions:
msc3757_enabled=True,
msc4289_creator_power_enabled=False,
msc4291_room_ids_as_hashes=False,
msc4243_account_keys=False,
)
HydraV11 = RoomVersion(
"org.matrix.hydra.11",
@@ -451,6 +467,7 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=True, # Changed from v11
msc4291_room_ids_as_hashes=True, # Changed from v11
msc4243_account_keys=False,
)
V12 = RoomVersion(
"12",
@@ -473,6 +490,30 @@ class RoomVersions:
msc3757_enabled=False,
msc4289_creator_power_enabled=True, # Changed from v11
msc4291_room_ids_as_hashes=True, # Changed from v11
msc4243_account_keys=False,
)
MSC4243v12 = RoomVersion(
"org.matrix.12.4243",
RoomDisposition.STABLE,
EventFormatVersions.ROOM_V11_HYDRA_PLUS,
StateResolutionVersions.V2_1, # Changed from v11
enforce_key_validity=False, # No longer enforce key validity.
special_case_aliases_auth=False,
strict_canonicaljson=True,
limit_notifications_power_levels=True,
implicit_room_creator=True, # Used by MSC3820
updated_redaction_rules=True, # Used by MSC3820
restricted_join_rule=True,
restricted_join_rule_fix=True,
knock_join_rule=True,
msc3389_relation_redactions=False,
knock_restricted_join_rule=True,
enforce_int_power_levels=True,
msc3931_push_features=(),
msc3757_enabled=False,
msc4289_creator_power_enabled=True,
msc4291_room_ids_as_hashes=True,
msc4243_account_keys=True,
)
@@ -494,6 +535,7 @@ KNOWN_ROOM_VERSIONS: Dict[str, RoomVersion] = {
RoomVersions.MSC3757v10,
RoomVersions.MSC3757v11,
RoomVersions.HydraV11,
RoomVersions.MSC4243v12,
)
}

View File

@@ -47,7 +47,7 @@ from synapse.events import EventBase
from synapse.events.utils import prune_event_dict
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.storage.keys import FetchKeyResult
from synapse.types import JsonDict
from synapse.types import JsonDict, UserID
from synapse.util import unwrapFirstError
from synapse.util.async_helpers import yieldable_gather_results
from synapse.util.batching_queue import BatchingQueue
@@ -83,6 +83,7 @@ class VerifyJsonRequest:
get_json_object: Callable[[], JsonDict]
minimum_valid_until_ts: int
key_ids: List[str]
key_ids_are_public_keys: bool = False
@staticmethod
def from_json_object(
@@ -118,6 +119,7 @@ class VerifyJsonRequest:
lambda: prune_event_dict(event.room_version, event.get_pdu_json()),
minimum_valid_until_ms,
key_ids=key_ids,
key_ids_are_public_keys=False,
)
@@ -265,6 +267,38 @@ class Keyring:
)
)
async def verify_event_for_account_key(
self,
user_id_str: str,
event: EventBase,
) -> None:
"""Verify that the given event has been signed by the provided account key, as determined by
the user_id provided.
Args:
user_id_str: The MSC4243 user ID, consisting of an account key localpart and a domain.
event: The PDU that should be signed with the account key. Room version must support MSC4243.
"""
assert event.room_version.msc4243_account_keys
user_id = UserID.from_string(user_id_str)
key_ids = list(event.signatures.get(user_id.domain, []))
# only keep the key ID that matches the desired user ID we want to verify as.
# Events can be signed by multiple parties e.g invites, restricted joins
expected_key_id = "ed25519:" + user_id.localpart
key_ids = [key_id for key_id in key_ids if key_id == expected_key_id]
assert len(key_ids) == 1 # the user must have signed the event.
await self.process_request(
VerifyJsonRequest(
user_id.domain,
# We defer creating the redacted json object, as it uses a lot more
# memory than the Event object itself.
lambda: prune_event_dict(event.room_version, event.get_pdu_json()),
0, # No validity times
key_ids=key_ids,
key_ids_are_public_keys=True,
)
)
async def process_request(self, verify_request: VerifyJsonRequest) -> None:
"""Processes the `VerifyJsonRequest`. Raises if the object is not signed
by the server, the signatures don't match or we failed to fetch the
@@ -278,6 +312,15 @@ class Keyring:
Codes.UNAUTHORIZED,
)
if verify_request.key_ids_are_public_keys:
# No need to fetch keys as we have them already.
assert len(verify_request.key_ids) == 1
key_id = verify_request.key_ids[0]
key_bytes = decode_base64(key_id.removeprefix("ed25519:"))
verify_key = decode_verify_key_bytes(key_id, key_bytes)
await self._process_json(verify_key, verify_request)
return
found_keys: Dict[str, FetchKeyResult] = {}
# If we are the originating server, short-circuit the key-fetch for any keys

View File

@@ -241,16 +241,28 @@ async def _check_sigs_on_pdu(
sender_domain = get_domain_from_id(pdu.sender)
if not _is_invite_via_3pid(pdu):
try:
await keyring.verify_event_for_server(
sender_domain,
pdu,
pdu.origin_server_ts if room_version.enforce_key_validity else 0,
)
if pdu.room_version.msc4243_account_keys:
await keyring.verify_event_for_account_key(
pdu.sender,
pdu,
)
else:
await keyring.verify_event_for_server(
sender_domain,
pdu,
pdu.origin_server_ts if room_version.enforce_key_validity else 0,
)
except Exception as e:
raise InvalidEventSignatureError(
f"unable to verify signature for sender domain {sender_domain}: {e}",
pdu.event_id,
) from None
if pdu.room_version.msc4243_account_keys:
raise InvalidEventSignatureError(
f"unable to verify signature for account key {pdu.sender}: {e}",
pdu.event_id,
) from None
else:
raise InvalidEventSignatureError(
f"unable to verify signature for sender domain {sender_domain}: {e}",
pdu.event_id,
) from None
# now let's look for events where the sender's domain is different to the
# event id's domain (normally only the case for joins/leaves), and add additional
@@ -283,11 +295,17 @@ async def _check_sigs_on_pdu(
pdu.content[EventContentFields.AUTHORISING_USER]
)
try:
await keyring.verify_event_for_server(
authorising_server,
pdu,
pdu.origin_server_ts if room_version.enforce_key_validity else 0,
)
if pdu.room_version.msc4243_account_keys:
await keyring.verify_event_for_account_key(
pdu.content[EventContentFields.AUTHORISING_USER],
pdu,
)
else:
await keyring.verify_event_for_server(
authorising_server,
pdu,
pdu.origin_server_ts if room_version.enforce_key_validity else 0,
)
except Exception as e:
raise InvalidEventSignatureError(
f"unable to verify signature for authorising serve {authorising_server}: {e}",

View File

@@ -34,12 +34,15 @@ from twisted.internet.defer import Deferred, ensureDeferred
from twisted.internet.testing import MemoryReactor
from synapse.api.errors import SynapseError
from synapse.api.room_versions import RoomVersions
from synapse.crypto import keyring
from synapse.crypto.event_signing import compute_event_signature
from synapse.crypto.keyring import (
PerspectivesKeyFetcher,
ServerKeyFetcher,
StoreKeyFetcher,
)
from synapse.events import make_event_from_dict
from synapse.logging.context import (
ContextRequest,
LoggingContext,
@@ -388,6 +391,35 @@ class KeyringTestCase(unittest.HomeserverTestCase):
mock_fetcher1.get_keys.assert_called_once()
mock_fetcher2.get_keys.assert_called_once()
def test_verify_event_for_account_key(self) -> None:
"""Test basic functionality of verify_event_for_account_key.
- That it parses the user ID correctly.
- That it doesn't rely on key fetchers.
"""
room_version = RoomVersions.MSC4243v12
# Make a signing key and replace the key ID from '1' to be the base64 public key
signing_key = signedjson.key.generate_signing_key("1")
verify_key_str = encode_verify_key_base64(get_verify_key(signing_key))
signing_key.version = verify_key_str
domain = "can.be.anything.com"
signing_user_id = f"@{verify_key_str}:{domain}"
event_dict = {
"type": "m.room.create",
"state_key": "",
"sender": signing_user_id,
"content": {
"room_version": room_version.identifier,
},
}
event_dict["signatures"] = compute_event_signature(
room_version, event_dict, signature_name=domain, signing_key=signing_key
)
event = make_event_from_dict(event_dict, room_version)
kr = keyring.Keyring(self.hs, key_fetchers=None)
self.get_success(kr.verify_event_for_account_key(signing_user_id, event))
@logcontext_clean
class ServerKeyFetcherTestCase(unittest.HomeserverTestCase):