Compare commits
3 Commits
v1.140.0rc
...
kegan/4243
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b338f886d6 | ||
|
|
c0ffe61adb | ||
|
|
aefeb3cb58 |
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -32,6 +32,7 @@ from synapse.storage.database import (
|
||||
LoggingDatabaseConnection,
|
||||
LoggingTransaction,
|
||||
)
|
||||
from synapse.storage.databases.main.account_keys import AccountKeysStore
|
||||
from synapse.storage.databases.main.sliding_sync import SlidingSyncStore
|
||||
from synapse.storage.databases.main.stats import UserSortOrder
|
||||
from synapse.storage.databases.main.thread_subscriptions import (
|
||||
@@ -163,6 +164,7 @@ class DataStore(
|
||||
TaskSchedulerWorkerStore,
|
||||
SlidingSyncStore,
|
||||
DelayedEventsStore,
|
||||
AccountKeysStore,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
165
synapse/storage/databases/main/account_keys.py
Normal file
165
synapse/storage/databases/main/account_keys.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2025 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
|
||||
from typing import TYPE_CHECKING, Collection, Dict, List, Tuple, cast
|
||||
|
||||
from signedjson.key import (
|
||||
decode_signing_key_base64,
|
||||
generate_signing_key,
|
||||
get_verify_key,
|
||||
)
|
||||
from signedjson.types import SigningKey
|
||||
from unpaddedbase64 import encode_base64
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.storage._base import SQLBaseStore
|
||||
from synapse.storage.database import (
|
||||
DatabasePool,
|
||||
LoggingDatabaseConnection,
|
||||
LoggingTransaction,
|
||||
make_in_list_sql_clause,
|
||||
)
|
||||
from synapse.types import get_domain_from_id, get_localpart_from_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
|
||||
class AccountKeysStore(SQLBaseStore):
|
||||
def __init__(
|
||||
self,
|
||||
database: DatabasePool,
|
||||
db_conn: LoggingDatabaseConnection,
|
||||
hs: "HomeServer",
|
||||
):
|
||||
super().__init__(database, db_conn, hs)
|
||||
|
||||
async def get_or_create_account_key_user_id_for_account_name_user_id(
|
||||
self, account_name_user_id: str
|
||||
) -> Tuple[str, SigningKey]:
|
||||
"""
|
||||
Get or create an account key for the given account name user ID.
|
||||
The user ID must belong to this server.
|
||||
|
||||
Args:
|
||||
account_name_user_id: An account name user ID e.g "@alice:example.com"
|
||||
Returns:
|
||||
A tuple of account key user ID e.g @l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:example.com
|
||||
and the private key for the account.
|
||||
Raises:
|
||||
if the provided account name user ID is not owned by this homeserver, or if the user
|
||||
ID is invalid in some way.
|
||||
"""
|
||||
if not self.hs.is_mine_id(account_name_user_id):
|
||||
raise SynapseError(
|
||||
500,
|
||||
(
|
||||
"get_or_create_account_key_user_id_for_account_name_user_id: this server cannot"
|
||||
f" create an account key for other servers: {account_name_user_id}"
|
||||
),
|
||||
)
|
||||
|
||||
row = await self.db_pool.simple_select_one(
|
||||
table="account_keys",
|
||||
keyvalues={
|
||||
"account_name_user_id": account_name_user_id,
|
||||
},
|
||||
retcols=["account_key_user_id", "account_key"],
|
||||
allow_none=True,
|
||||
desc="get_or_create_account_key_user_id_for_account_name_user_id.get_key_txn",
|
||||
)
|
||||
if row is not None:
|
||||
return row[0], decode_account_key(row[1])
|
||||
|
||||
# create a new account key for this account inside a txn to ensure we lock correctly.
|
||||
def create_key_txn(txn: LoggingTransaction) -> Tuple[str, str]:
|
||||
key, public_key_str = generate_account_key()
|
||||
account_key_user_id = (
|
||||
f"@{public_key_str}:{get_domain_from_id(account_name_user_id)}"
|
||||
)
|
||||
|
||||
# Race to insert the key. The first one to make it will be returned here as we don't clobber
|
||||
sql = (
|
||||
"INSERT INTO account_keys(account_name_user_id, account_key_user_id, account_key)"
|
||||
" VALUES(?, ?, ?)"
|
||||
" ON CONFLICT DO NOTHING"
|
||||
)
|
||||
txn.execute(
|
||||
sql,
|
||||
(
|
||||
account_name_user_id,
|
||||
account_key_user_id,
|
||||
encode_base64(key.encode(), urlsafe=True),
|
||||
),
|
||||
)
|
||||
sql = "SELECT account_key_user_id, account_key FROM account_keys WHERE account_name_user_id = ?"
|
||||
txn.execute(sql, (account_name_user_id,))
|
||||
return cast(Tuple[str, str], txn.fetchone())
|
||||
|
||||
row = await self.db_pool.runInteraction(
|
||||
"get_or_create_account_key_user_id_for_account_name_user_id.create_key_txn",
|
||||
create_key_txn,
|
||||
)
|
||||
return row[0], decode_account_key(row[1])
|
||||
|
||||
async def get_account_name_user_ids_for_account_key_user_ids(
|
||||
self,
|
||||
account_key_user_ids: Collection[str],
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Fetch the verified account name user IDs for the given account key user IDs. Unknown account key
|
||||
user IDs will be omitted from the dict.
|
||||
|
||||
Args:
|
||||
account_key_user_ids: A list of user IDs in account key format e.g
|
||||
["@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:example.com"]
|
||||
|
||||
Returns:
|
||||
A map of account key user IDs to account name user IDs e.g.
|
||||
{"@l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ:example.com":"@alice:example.com"}
|
||||
"""
|
||||
|
||||
clause, args = make_in_list_sql_clause(
|
||||
self.database_engine, "account_key_user_id", account_key_user_ids
|
||||
)
|
||||
|
||||
def f(txn: LoggingTransaction) -> List[Tuple[str, str]]:
|
||||
sql = f"SELECT account_key_user_id, account_name_user_id FROM account_keys WHERE {clause} AND account_name_user_id IS NOT NULL"
|
||||
txn.execute(sql, args)
|
||||
return cast(List[Tuple[str, str]], txn.fetchall())
|
||||
|
||||
rows = await self.db_pool.runInteraction(
|
||||
"get_account_name_user_ids_for_account_key_user_ids", f
|
||||
)
|
||||
return {row[0]: row[1] for row in rows}
|
||||
|
||||
|
||||
def generate_account_key() -> Tuple[SigningKey, str]:
|
||||
signing_key = generate_signing_key("1")
|
||||
verify_key_str = encode_base64(get_verify_key(signing_key).encode(), urlsafe=True)
|
||||
return signing_key, verify_key_str
|
||||
|
||||
|
||||
def decode_account_key(signing_key: str) -> SigningKey:
|
||||
return decode_signing_key_base64(
|
||||
"ed25519",
|
||||
"1",
|
||||
signing_key,
|
||||
)
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
SCHEMA_VERSION = 92 # remember to update the list below when updating
|
||||
SCHEMA_VERSION = 93 # remember to update the list below when updating
|
||||
"""Represents the expectations made by the codebase about the database schema
|
||||
|
||||
This should be incremented whenever the codebase changes its requirements on the
|
||||
|
||||
25
synapse/storage/schema/main/delta/93/01_account_keys.sql
Normal file
25
synapse/storage/schema/main/delta/93/01_account_keys.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
--
|
||||
-- This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
--
|
||||
-- Copyright (C) 2025 New Vector, Ltd
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as
|
||||
-- published by the Free Software Foundation, either version 3 of the
|
||||
-- License, or (at your option) any later version.
|
||||
--
|
||||
-- See the GNU Affero General Public License for more details:
|
||||
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
-- Keeps a record of MSC4243 account key <--> account name mappings for all servers.
|
||||
-- This mapping is permanent.
|
||||
CREATE TABLE account_keys (
|
||||
account_key_user_id TEXT PRIMARY KEY NOT NULL,
|
||||
-- nullable if we cannot talk to the remote server.
|
||||
account_name_user_id TEXT,
|
||||
-- the private key as urlsafe base64, only for local accounts
|
||||
account_key TEXT,
|
||||
UNIQUE(account_key_user_id, account_name_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX account_keys_key_for_name ON account_keys (account_name_user_id) WHERE account_name_user_id IS NOT NULL;
|
||||
@@ -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):
|
||||
|
||||
92
tests/storage/test_account_keys.py
Normal file
92
tests/storage/test_account_keys.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2025 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
from signedjson.key import get_verify_key
|
||||
from unpaddedbase64 import encode_base64
|
||||
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import get_localpart_from_id
|
||||
from synapse.util import Clock
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class AccountKeysTestCase(unittest.HomeserverTestCase):
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.store = self.hs.get_datastores().main
|
||||
self.user = "@user:test"
|
||||
|
||||
def test_get_or_create_account_key_user_id_for_account_name_user_id(self) -> None:
|
||||
key_user_id, key = self.get_success(
|
||||
self.store.get_or_create_account_key_user_id_for_account_name_user_id(
|
||||
self.user
|
||||
)
|
||||
)
|
||||
# asserts the localpart is unpadded urlsafe base64
|
||||
self.assertRegex(key_user_id, r"^@[A-Za-z0-9\-_]{43}:test$")
|
||||
# asserts the public key is the localpart
|
||||
self.assertEquals(encode_base64(get_verify_key(key).encode(), urlsafe=True), get_localpart_from_id(key_user_id))
|
||||
# asserts the key ID is 1
|
||||
self.assertEquals(key.version, "1")
|
||||
# assert that repeated calls return the same key
|
||||
key_user_id2, key2 = self.get_success(
|
||||
self.store.get_or_create_account_key_user_id_for_account_name_user_id(
|
||||
self.user
|
||||
)
|
||||
)
|
||||
self.assertEquals(key_user_id, key_user_id2)
|
||||
self.assertEquals(key.encode(), key2.encode())
|
||||
|
||||
def test_get_account_name_user_ids_for_account_key_user_ids(self) -> None:
|
||||
key_user_id, _ = self.get_success(
|
||||
self.store.get_or_create_account_key_user_id_for_account_name_user_id(
|
||||
self.user,
|
||||
)
|
||||
)
|
||||
result = self.get_success(
|
||||
self.store.get_account_name_user_ids_for_account_key_user_ids(
|
||||
[key_user_id]
|
||||
),
|
||||
)
|
||||
self.assertEquals(result[key_user_id], self.user)
|
||||
|
||||
def test_get_account_name_user_ids_for_account_key_user_ids_multiple(self) -> None:
|
||||
key_user_id_alice, _ = self.get_success(
|
||||
self.store.get_or_create_account_key_user_id_for_account_name_user_id(
|
||||
"@alice:test",
|
||||
)
|
||||
)
|
||||
key_user_id_bob, _ = self.get_success(
|
||||
self.store.get_or_create_account_key_user_id_for_account_name_user_id(
|
||||
"@bob:test",
|
||||
)
|
||||
)
|
||||
key_user_id_unknown = "@6fey6W1wS3-vbvUmHZnTd6Gi3o-TIxvIcwtEQP4nrW0:test"
|
||||
result = self.get_success(
|
||||
self.store.get_account_name_user_ids_for_account_key_user_ids(
|
||||
[key_user_id_alice, key_user_id_bob, key_user_id_unknown]
|
||||
),
|
||||
)
|
||||
self.assertEquals(result[key_user_id_alice], "@alice:test")
|
||||
self.assertEquals(result[key_user_id_bob], "@bob:test")
|
||||
self.assertEquals(result.get(key_user_id_unknown, None), None)
|
||||
Reference in New Issue
Block a user