diff --git a/rust/src/e2e_keys/mod.rs b/rust/src/e2e_keys/mod.rs new file mode 100644 index 0000000000..f5c7dd1e38 --- /dev/null +++ b/rust/src/e2e_keys/mod.rs @@ -0,0 +1,83 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations 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: + * . + * + * Originally licensed under the Apache License, Version 2.0: + * . + * + * [This file includes modifications made by Element Creations Ltd] + */ + +use pyo3::{ + pyclass, pymethods, + types::{PyAnyMethods, PyModule, PyModuleMethods}, + Bound, Py, PyAny, PyResult, Python, +}; + +pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(py, "e2e_keys")?; + child_module.add_class::()?; + + m.add_submodule(&child_module)?; + + py.import("sys")? + .getattr("modules")? + .set_item("synapse.synapse_rust.e2e_keys", child_module)?; + + Ok(()) +} + +/// A pending cross-signing signature. +#[derive(Debug)] +#[pyclass(frozen)] +pub struct SignatureListItem { + /// Full key ID of the signing key, e.g. `"ed25519:ABCDEF"`. + #[pyo3(get)] + pub signing_key_id: String, + + /// User whose key was signed. + #[pyo3(get)] + pub target_user_id: String, + + /// Device ID (or master-key ID) that the signature targets. + #[pyo3(get)] + pub target_device_id: String, + + /// Raw signature value. + #[pyo3(get)] + pub signature: Py, +} + +#[pymethods] +impl SignatureListItem { + #[new] + fn py_new( + signing_key_id: String, + target_user_id: String, + target_device_id: String, + signature: Py, + ) -> Self { + Self { + signing_key_id, + target_user_id, + target_device_id, + signature, + } + } + + fn __repr__(&self) -> String { + format!( + "SignatureListItem(signing_key_id={:?}, target_user_id={:?}, target_device_id={:?})", + self.signing_key_id, self.target_user_id, self.target_device_id, + ) + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index cc89862e4e..c809b65418 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -7,6 +7,7 @@ use pyo3_log::ResetHandle; pub mod acl; pub mod canonical_json; pub mod duration; +pub mod e2e_keys; pub mod errors; pub mod events; pub mod http; @@ -64,6 +65,7 @@ fn synapse_rust(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { acl::register_module(py, m)?; push::register_module(py, m)?; + e2e_keys::register_module(py, m)?; events::register_module(py, m)?; http_client::register_module(py, m)?; rendezvous::register_module(py, m)?; diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 64f705a3da..b67c2ca985 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -22,7 +22,6 @@ import logging from typing import TYPE_CHECKING, Iterable, Mapping -import attr from canonicaljson import encode_canonical_json from signedjson.key import VerifyKey, decode_verify_key_bytes from signedjson.sign import SignatureVerifyException, verify_signed_json @@ -35,6 +34,7 @@ from synapse.api.errors import CodeMessageException, Codes, NotFoundError, Synap from synapse.handlers.device import DeviceWriterHandler from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.logging.opentracing import log_kv, set_tag, tag_args, trace +from synapse.synapse_rust.e2e_keys import SignatureListItem from synapse.types import ( JsonDict, JsonMapping, @@ -1132,7 +1132,7 @@ class E2eKeysHandler: async def _process_self_signatures( self, user_id: str, signatures: JsonDict - ) -> tuple[list["SignatureListItem"], dict[str, dict[str, dict]]]: + ) -> tuple[list[SignatureListItem], dict[str, dict[str, dict]]]: """Process uploaded signatures of the user's own keys. Signatures of the user's own keys from this API come in two forms: @@ -1150,7 +1150,7 @@ class E2eKeysHandler: Raises: SynapseError: if the input is malformed """ - signature_list: list["SignatureListItem"] = [] + signature_list: list[SignatureListItem] = [] failures: dict[str, dict[str, JsonDict]] = {} if not signatures: return signature_list, failures @@ -1252,7 +1252,7 @@ class E2eKeysHandler: signed_master_key: JsonDict, stored_master_key: JsonMapping, devices: dict[str, dict[str, JsonDict]], - ) -> list["SignatureListItem"]: + ) -> list[SignatureListItem]: """Check signatures of a user's master key made by their devices. Args: @@ -1296,7 +1296,7 @@ class E2eKeysHandler: async def _process_other_signatures( self, user_id: str, signatures: dict[str, dict] - ) -> tuple[list["SignatureListItem"], dict[str, dict[str, dict]]]: + ) -> tuple[list[SignatureListItem], dict[str, dict[str, dict]]]: """Process uploaded signatures of other users' keys. These will be the target user's master keys, signed by the uploading user's user-signing key. @@ -1312,7 +1312,7 @@ class E2eKeysHandler: Raises: SynapseError: if the input is malformed """ - signature_list: list["SignatureListItem"] = [] + signature_list: list[SignatureListItem] = [] failures: dict[str, dict[str, JsonDict]] = {} if not signatures: return signature_list, failures @@ -1747,16 +1747,6 @@ def _one_time_keys_match(old_key_json: str, new_key: JsonDict) -> bool: return old_key == new_key_copy -@attr.s(slots=True, auto_attribs=True) -class SignatureListItem: - """An item in the signature list as used by upload_signatures_for_device_keys.""" - - signing_key_id: str - target_user_id: str - target_device_id: str - signature: JsonDict - - class SigningKeyEduUpdater: """Handles incoming signing key updates from federation and updates the DB""" diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index c93ebd3dda..000cfca6c2 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -61,8 +61,8 @@ from synapse.util.iterutils import batch_iter from synapse.util.json import json_decoder, json_encoder if TYPE_CHECKING: - from synapse.handlers.e2e_keys import SignatureListItem from synapse.server import HomeServer + from synapse.synapse_rust.e2e_keys import SignatureListItem @attr.s(slots=True, auto_attribs=True) diff --git a/synapse/synapse_rust/e2e_keys.pyi b/synapse/synapse_rust/e2e_keys.pyi new file mode 100644 index 0000000000..07d3513f4d --- /dev/null +++ b/synapse/synapse_rust/e2e_keys.pyi @@ -0,0 +1,24 @@ +from typing import Any + +class SignatureListItem: + """A pending cross-signing signature.""" + + signing_key_id: str + """ Full key ID of the signing key, e.g. `"ed25519:ABCDEF"`.""" + + target_user_id: str + """User whose key was signed.""" + + target_device_id: str + """Device ID (or master-key ID) that the signature targets.""" + + signature: Any + """Raw signature value.""" + + def __init__( + self, + signing_key_id: str, + target_user_id: str, + target_device_id: str, + signature: Any, + ) -> None: ...