You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1398 lines
52 KiB

  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. # Copyright 2019 New Vector Ltd
  3. # Copyright 2019,2020 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import abc
  17. from typing import (
  18. TYPE_CHECKING,
  19. Collection,
  20. Dict,
  21. Iterable,
  22. List,
  23. Mapping,
  24. Optional,
  25. Sequence,
  26. Tuple,
  27. Union,
  28. cast,
  29. overload,
  30. )
  31. import attr
  32. from canonicaljson import encode_canonical_json
  33. from typing_extensions import Literal
  34. from synapse.api.constants import DeviceKeyAlgorithms
  35. from synapse.appservice import (
  36. TransactionOneTimeKeysCount,
  37. TransactionUnusedFallbackKeys,
  38. )
  39. from synapse.logging.opentracing import log_kv, set_tag, trace
  40. from synapse.storage._base import SQLBaseStore, db_to_json
  41. from synapse.storage.database import (
  42. DatabasePool,
  43. LoggingDatabaseConnection,
  44. LoggingTransaction,
  45. make_in_list_sql_clause,
  46. make_tuple_in_list_sql_clause,
  47. )
  48. from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore
  49. from synapse.storage.engines import PostgresEngine
  50. from synapse.storage.util.id_generators import StreamIdGenerator
  51. from synapse.types import JsonDict
  52. from synapse.util import json_encoder
  53. from synapse.util.caches.descriptors import cached, cachedList
  54. from synapse.util.cancellation import cancellable
  55. from synapse.util.iterutils import batch_iter
  56. if TYPE_CHECKING:
  57. from synapse.handlers.e2e_keys import SignatureListItem
  58. from synapse.server import HomeServer
  59. @attr.s(slots=True, auto_attribs=True)
  60. class DeviceKeyLookupResult:
  61. """The type returned by get_e2e_device_keys_and_signatures"""
  62. display_name: Optional[str]
  63. # the key data from e2e_device_keys_json. Typically includes fields like
  64. # "algorithm", "keys" (including the curve25519 identity key and the ed25519 signing
  65. # key) and "signatures" (a map from (user id) to (key id/device_id) to signature.)
  66. keys: Optional[JsonDict]
  67. class EndToEndKeyBackgroundStore(SQLBaseStore):
  68. def __init__(
  69. self,
  70. database: DatabasePool,
  71. db_conn: LoggingDatabaseConnection,
  72. hs: "HomeServer",
  73. ):
  74. super().__init__(database, db_conn, hs)
  75. self.db_pool.updates.register_background_index_update(
  76. "e2e_cross_signing_keys_idx",
  77. index_name="e2e_cross_signing_keys_stream_idx",
  78. table="e2e_cross_signing_keys",
  79. columns=["stream_id"],
  80. unique=True,
  81. )
  82. class EndToEndKeyWorkerStore(EndToEndKeyBackgroundStore, CacheInvalidationWorkerStore):
  83. def __init__(
  84. self,
  85. database: DatabasePool,
  86. db_conn: LoggingDatabaseConnection,
  87. hs: "HomeServer",
  88. ):
  89. super().__init__(database, db_conn, hs)
  90. self._allow_device_name_lookup_over_federation = (
  91. self.hs.config.federation.allow_device_name_lookup_over_federation
  92. )
  93. async def get_e2e_device_keys_for_federation_query(
  94. self, user_id: str
  95. ) -> Tuple[int, List[JsonDict]]:
  96. """Get all devices (with any device keys) for a user
  97. Returns:
  98. (stream_id, devices)
  99. """
  100. now_stream_id = self.get_device_stream_token()
  101. devices = await self.get_e2e_device_keys_and_signatures([(user_id, None)])
  102. if devices:
  103. user_devices = devices[user_id]
  104. results = []
  105. for device_id, device in user_devices.items():
  106. result: JsonDict = {"device_id": device_id}
  107. keys = device.keys
  108. if keys:
  109. result["keys"] = keys
  110. device_display_name = None
  111. if self._allow_device_name_lookup_over_federation:
  112. device_display_name = device.display_name
  113. if device_display_name:
  114. result["device_display_name"] = device_display_name
  115. results.append(result)
  116. return now_stream_id, results
  117. return now_stream_id, []
  118. @trace
  119. @cancellable
  120. async def get_e2e_device_keys_for_cs_api(
  121. self,
  122. query_list: Collection[Tuple[str, Optional[str]]],
  123. include_displaynames: bool = True,
  124. ) -> Dict[str, Dict[str, JsonDict]]:
  125. """Fetch a list of device keys, formatted suitably for the C/S API.
  126. Args:
  127. query_list: List of pairs of user_ids and device_ids.
  128. include_displaynames: Whether to include the displayname of returned devices
  129. (if one exists).
  130. Returns:
  131. Dict mapping from user-id to dict mapping from device_id to
  132. key data. The key data will be a dict in the same format as the
  133. DeviceKeys type returned by POST /_matrix/client/r0/keys/query.
  134. """
  135. set_tag("query_list", str(query_list))
  136. if not query_list:
  137. return {}
  138. results = await self.get_e2e_device_keys_and_signatures(query_list)
  139. # Build the result structure, un-jsonify the results, and add the
  140. # "unsigned" section
  141. rv: Dict[str, Dict[str, JsonDict]] = {}
  142. for user_id, device_keys in results.items():
  143. rv[user_id] = {}
  144. for device_id, device_info in device_keys.items():
  145. r = device_info.keys
  146. if r is None:
  147. continue
  148. r["unsigned"] = {}
  149. if include_displaynames:
  150. # Include the device's display name in the "unsigned" dictionary
  151. display_name = device_info.display_name
  152. if display_name is not None:
  153. r["unsigned"]["device_display_name"] = display_name
  154. rv[user_id][device_id] = r
  155. return rv
  156. @overload
  157. async def get_e2e_device_keys_and_signatures(
  158. self,
  159. query_list: Collection[Tuple[str, Optional[str]]],
  160. include_all_devices: Literal[False] = False,
  161. ) -> Dict[str, Dict[str, DeviceKeyLookupResult]]:
  162. ...
  163. @overload
  164. async def get_e2e_device_keys_and_signatures(
  165. self,
  166. query_list: Collection[Tuple[str, Optional[str]]],
  167. include_all_devices: bool = False,
  168. include_deleted_devices: Literal[False] = False,
  169. ) -> Dict[str, Dict[str, DeviceKeyLookupResult]]:
  170. ...
  171. @overload
  172. async def get_e2e_device_keys_and_signatures(
  173. self,
  174. query_list: Collection[Tuple[str, Optional[str]]],
  175. include_all_devices: Literal[True],
  176. include_deleted_devices: Literal[True],
  177. ) -> Dict[str, Dict[str, Optional[DeviceKeyLookupResult]]]:
  178. ...
  179. @trace
  180. @cancellable
  181. async def get_e2e_device_keys_and_signatures(
  182. self,
  183. query_list: Collection[Tuple[str, Optional[str]]],
  184. include_all_devices: bool = False,
  185. include_deleted_devices: bool = False,
  186. ) -> Union[
  187. Dict[str, Dict[str, DeviceKeyLookupResult]],
  188. Dict[str, Dict[str, Optional[DeviceKeyLookupResult]]],
  189. ]:
  190. """Fetch a list of device keys
  191. Any cross-signatures made on the keys by the owner of the device are also
  192. included.
  193. The cross-signatures are added to the `signatures` field within the `keys`
  194. object in the response.
  195. Args:
  196. query_list: List of pairs of user_ids and device_ids. Device id can be None
  197. to indicate "all devices for this user"
  198. include_all_devices: whether to return devices without device keys
  199. include_deleted_devices: whether to include null entries for
  200. devices which no longer exist (but were in the query_list).
  201. This option only takes effect if include_all_devices is true.
  202. Returns:
  203. Dict mapping from user-id to dict mapping from device_id to
  204. key data.
  205. """
  206. set_tag("include_all_devices", include_all_devices)
  207. set_tag("include_deleted_devices", include_deleted_devices)
  208. result = await self.db_pool.runInteraction(
  209. "get_e2e_device_keys",
  210. self._get_e2e_device_keys_txn,
  211. query_list,
  212. include_all_devices,
  213. include_deleted_devices,
  214. )
  215. # get the (user_id, device_id) tuples to look up cross-signatures for
  216. signature_query = (
  217. (user_id, device_id)
  218. for user_id, dev in result.items()
  219. for device_id, d in dev.items()
  220. if d is not None and d.keys is not None
  221. )
  222. for batch in batch_iter(signature_query, 50):
  223. cross_sigs_result = await self.db_pool.runInteraction(
  224. "get_e2e_cross_signing_signatures_for_devices",
  225. self._get_e2e_cross_signing_signatures_for_devices_txn,
  226. batch,
  227. )
  228. # add each cross-signing signature to the correct device in the result dict.
  229. for user_id, key_id, device_id, signature in cross_sigs_result:
  230. target_device_result = result[user_id][device_id]
  231. # We've only looked up cross-signatures for non-deleted devices with key
  232. # data.
  233. assert target_device_result is not None
  234. assert target_device_result.keys is not None
  235. target_device_signatures = target_device_result.keys.setdefault(
  236. "signatures", {}
  237. )
  238. signing_user_signatures = target_device_signatures.setdefault(
  239. user_id, {}
  240. )
  241. signing_user_signatures[key_id] = signature
  242. log_kv(result)
  243. return result
  244. def _get_e2e_device_keys_txn(
  245. self,
  246. txn: LoggingTransaction,
  247. query_list: Collection[Tuple[str, Optional[str]]],
  248. include_all_devices: bool = False,
  249. include_deleted_devices: bool = False,
  250. ) -> Dict[str, Dict[str, Optional[DeviceKeyLookupResult]]]:
  251. """Get information on devices from the database
  252. The results include the device's keys and self-signatures, but *not* any
  253. cross-signing signatures which have been added subsequently (for which, see
  254. get_e2e_device_keys_and_signatures)
  255. """
  256. query_clauses: List[str] = []
  257. query_params_list: List[List[object]] = []
  258. if include_all_devices is False:
  259. include_deleted_devices = False
  260. if include_deleted_devices:
  261. deleted_devices = set(query_list)
  262. # Split the query list into queries for users and queries for particular
  263. # devices.
  264. user_list = []
  265. user_device_list = []
  266. for user_id, device_id in query_list:
  267. if device_id is None:
  268. user_list.append(user_id)
  269. else:
  270. user_device_list.append((user_id, device_id))
  271. if user_list:
  272. user_id_in_list_clause, user_args = make_in_list_sql_clause(
  273. txn.database_engine, "user_id", user_list
  274. )
  275. query_clauses.append(user_id_in_list_clause)
  276. query_params_list.append(user_args)
  277. if user_device_list:
  278. # Divide the device queries into batches, to avoid excessively large
  279. # queries.
  280. for user_device_batch in batch_iter(user_device_list, 1024):
  281. (
  282. user_device_id_in_list_clause,
  283. user_device_args,
  284. ) = make_tuple_in_list_sql_clause(
  285. txn.database_engine, ("user_id", "device_id"), user_device_batch
  286. )
  287. query_clauses.append(user_device_id_in_list_clause)
  288. query_params_list.append(user_device_args)
  289. result: Dict[str, Dict[str, Optional[DeviceKeyLookupResult]]] = {}
  290. for query_clause, query_params in zip(query_clauses, query_params_list):
  291. sql = (
  292. "SELECT user_id, device_id, "
  293. " d.display_name, "
  294. " k.key_json"
  295. " FROM devices d"
  296. " %s JOIN e2e_device_keys_json k USING (user_id, device_id)"
  297. " WHERE %s AND NOT d.hidden"
  298. ) % (
  299. "LEFT" if include_all_devices else "INNER",
  300. query_clause,
  301. )
  302. txn.execute(sql, query_params)
  303. for user_id, device_id, display_name, key_json in txn:
  304. assert device_id is not None
  305. if include_deleted_devices:
  306. deleted_devices.remove((user_id, device_id))
  307. result.setdefault(user_id, {})[device_id] = DeviceKeyLookupResult(
  308. display_name, db_to_json(key_json) if key_json else None
  309. )
  310. if include_deleted_devices:
  311. for user_id, device_id in deleted_devices:
  312. if device_id is None:
  313. continue
  314. result.setdefault(user_id, {})[device_id] = None
  315. return result
  316. def _get_e2e_cross_signing_signatures_for_devices_txn(
  317. self, txn: LoggingTransaction, device_query: Iterable[Tuple[str, str]]
  318. ) -> List[Tuple[str, str, str, str]]:
  319. """Get cross-signing signatures for a given list of devices
  320. Returns signatures made by the owners of the devices.
  321. Returns: a list of results; each entry in the list is a tuple of
  322. (user_id, key_id, target_device_id, signature).
  323. """
  324. signature_query_clauses = []
  325. signature_query_params = []
  326. for user_id, device_id in device_query:
  327. signature_query_clauses.append(
  328. "target_user_id = ? AND target_device_id = ? AND user_id = ?"
  329. )
  330. signature_query_params.extend([user_id, device_id, user_id])
  331. signature_sql = """
  332. SELECT user_id, key_id, target_device_id, signature
  333. FROM e2e_cross_signing_signatures WHERE %s
  334. """ % (
  335. " OR ".join("(" + q + ")" for q in signature_query_clauses)
  336. )
  337. txn.execute(signature_sql, signature_query_params)
  338. return cast(
  339. List[
  340. Tuple[
  341. str,
  342. str,
  343. str,
  344. str,
  345. ]
  346. ],
  347. txn.fetchall(),
  348. )
  349. async def get_e2e_one_time_keys(
  350. self, user_id: str, device_id: str, key_ids: List[str]
  351. ) -> Dict[Tuple[str, str], str]:
  352. """Retrieve a number of one-time keys for a user
  353. Args:
  354. user_id: id of user to get keys for
  355. device_id: id of device to get keys for
  356. key_ids: list of key ids (excluding algorithm) to retrieve
  357. Returns:
  358. A map from (algorithm, key_id) to json string for key
  359. """
  360. rows = await self.db_pool.simple_select_many_batch(
  361. table="e2e_one_time_keys_json",
  362. column="key_id",
  363. iterable=key_ids,
  364. retcols=("algorithm", "key_id", "key_json"),
  365. keyvalues={"user_id": user_id, "device_id": device_id},
  366. desc="add_e2e_one_time_keys_check",
  367. )
  368. result = {(row["algorithm"], row["key_id"]): row["key_json"] for row in rows}
  369. log_kv({"message": "Fetched one time keys for user", "one_time_keys": result})
  370. return result
  371. async def add_e2e_one_time_keys(
  372. self,
  373. user_id: str,
  374. device_id: str,
  375. time_now: int,
  376. new_keys: Iterable[Tuple[str, str, str]],
  377. ) -> None:
  378. """Insert some new one time keys for a device. Errors if any of the
  379. keys already exist.
  380. Args:
  381. user_id: id of user to get keys for
  382. device_id: id of device to get keys for
  383. time_now: insertion time to record (ms since epoch)
  384. new_keys: keys to add - each a tuple of (algorithm, key_id, key json)
  385. """
  386. def _add_e2e_one_time_keys(txn: LoggingTransaction) -> None:
  387. set_tag("user_id", user_id)
  388. set_tag("device_id", device_id)
  389. set_tag("new_keys", str(new_keys))
  390. # We are protected from race between lookup and insertion due to
  391. # a unique constraint. If there is a race of two calls to
  392. # `add_e2e_one_time_keys` then they'll conflict and we will only
  393. # insert one set.
  394. self.db_pool.simple_insert_many_txn(
  395. txn,
  396. table="e2e_one_time_keys_json",
  397. keys=(
  398. "user_id",
  399. "device_id",
  400. "algorithm",
  401. "key_id",
  402. "ts_added_ms",
  403. "key_json",
  404. ),
  405. values=[
  406. (user_id, device_id, algorithm, key_id, time_now, json_bytes)
  407. for algorithm, key_id, json_bytes in new_keys
  408. ],
  409. )
  410. self._invalidate_cache_and_stream(
  411. txn, self.count_e2e_one_time_keys, (user_id, device_id)
  412. )
  413. await self.db_pool.runInteraction(
  414. "add_e2e_one_time_keys_insert", _add_e2e_one_time_keys
  415. )
  416. @cached(max_entries=10000)
  417. async def count_e2e_one_time_keys(
  418. self, user_id: str, device_id: str
  419. ) -> Dict[str, int]:
  420. """Count the number of one time keys the server has for a device
  421. Returns:
  422. A mapping from algorithm to number of keys for that algorithm.
  423. """
  424. def _count_e2e_one_time_keys(txn: LoggingTransaction) -> Dict[str, int]:
  425. sql = (
  426. "SELECT algorithm, COUNT(key_id) FROM e2e_one_time_keys_json"
  427. " WHERE user_id = ? AND device_id = ?"
  428. " GROUP BY algorithm"
  429. )
  430. txn.execute(sql, (user_id, device_id))
  431. # Initially set the key count to 0. This ensures that the client will always
  432. # receive *some count*, even if it's 0.
  433. result = {DeviceKeyAlgorithms.SIGNED_CURVE25519: 0}
  434. # Override entries with the count of any keys we pulled from the database
  435. for algorithm, key_count in txn:
  436. result[algorithm] = key_count
  437. return result
  438. return await self.db_pool.runInteraction(
  439. "count_e2e_one_time_keys", _count_e2e_one_time_keys
  440. )
  441. async def count_bulk_e2e_one_time_keys_for_as(
  442. self, user_ids: Collection[str]
  443. ) -> TransactionOneTimeKeysCount:
  444. """
  445. Counts, in bulk, the one-time keys for all the users specified.
  446. Intended to be used by application services for populating OTK counts in
  447. transactions.
  448. Return structure is of the shape:
  449. user_id -> device_id -> algorithm -> count
  450. Empty algorithm -> count dicts are created if needed to represent a
  451. lack of unused one-time keys.
  452. """
  453. def _count_bulk_e2e_one_time_keys_txn(
  454. txn: LoggingTransaction,
  455. ) -> TransactionOneTimeKeysCount:
  456. user_in_where_clause, user_parameters = make_in_list_sql_clause(
  457. self.database_engine, "user_id", user_ids
  458. )
  459. sql = f"""
  460. SELECT user_id, device_id, algorithm, COUNT(key_id)
  461. FROM devices
  462. LEFT JOIN e2e_one_time_keys_json USING (user_id, device_id)
  463. WHERE {user_in_where_clause}
  464. GROUP BY user_id, device_id, algorithm
  465. """
  466. txn.execute(sql, user_parameters)
  467. result: TransactionOneTimeKeysCount = {}
  468. for user_id, device_id, algorithm, count in txn:
  469. # We deliberately construct empty dictionaries for
  470. # users and devices without any unused one-time keys.
  471. # We *could* omit these empty dicts if there have been no
  472. # changes since the last transaction, but we currently don't
  473. # do any change tracking!
  474. device_count_by_algo = result.setdefault(user_id, {}).setdefault(
  475. device_id, {}
  476. )
  477. if algorithm is not None:
  478. # algorithm will be None if this device has no keys.
  479. device_count_by_algo[algorithm] = count
  480. return result
  481. return await self.db_pool.runInteraction(
  482. "count_bulk_e2e_one_time_keys", _count_bulk_e2e_one_time_keys_txn
  483. )
  484. async def get_e2e_bulk_unused_fallback_key_types(
  485. self, user_ids: Collection[str]
  486. ) -> TransactionUnusedFallbackKeys:
  487. """
  488. Finds, in bulk, the types of unused fallback keys for all the users specified.
  489. Intended to be used by application services for populating unused fallback
  490. keys in transactions.
  491. Return structure is of the shape:
  492. user_id -> device_id -> algorithms
  493. Empty lists are created for devices if there are no unused fallback
  494. keys. This matches the response structure of MSC3202.
  495. """
  496. if len(user_ids) == 0:
  497. return {}
  498. def _get_bulk_e2e_unused_fallback_keys_txn(
  499. txn: LoggingTransaction,
  500. ) -> TransactionUnusedFallbackKeys:
  501. user_in_where_clause, user_parameters = make_in_list_sql_clause(
  502. self.database_engine, "devices.user_id", user_ids
  503. )
  504. # We can't use USING here because we require the `.used` condition
  505. # to be part of the JOIN condition so that we generate empty lists
  506. # when all keys are used (as opposed to just when there are no keys at all).
  507. sql = f"""
  508. SELECT devices.user_id, devices.device_id, algorithm
  509. FROM devices
  510. LEFT JOIN e2e_fallback_keys_json AS fallback_keys
  511. ON devices.user_id = fallback_keys.user_id
  512. AND devices.device_id = fallback_keys.device_id
  513. AND NOT fallback_keys.used
  514. WHERE
  515. {user_in_where_clause}
  516. """
  517. txn.execute(sql, user_parameters)
  518. result: TransactionUnusedFallbackKeys = {}
  519. for user_id, device_id, algorithm in txn:
  520. # We deliberately construct empty dictionaries and lists for
  521. # users and devices without any unused fallback keys.
  522. # We *could* omit these empty dicts if there have been no
  523. # changes since the last transaction, but we currently don't
  524. # do any change tracking!
  525. device_unused_keys = result.setdefault(user_id, {}).setdefault(
  526. device_id, []
  527. )
  528. if algorithm is not None:
  529. # algorithm will be None if this device has no keys.
  530. device_unused_keys.append(algorithm)
  531. return result
  532. return await self.db_pool.runInteraction(
  533. "_get_bulk_e2e_unused_fallback_keys", _get_bulk_e2e_unused_fallback_keys_txn
  534. )
  535. async def set_e2e_fallback_keys(
  536. self, user_id: str, device_id: str, fallback_keys: JsonDict
  537. ) -> None:
  538. """Set the user's e2e fallback keys.
  539. Args:
  540. user_id: the user whose keys are being set
  541. device_id: the device whose keys are being set
  542. fallback_keys: the keys to set. This is a map from key ID (which is
  543. of the form "algorithm:id") to key data.
  544. """
  545. await self.db_pool.runInteraction(
  546. "set_e2e_fallback_keys_txn",
  547. self._set_e2e_fallback_keys_txn,
  548. user_id,
  549. device_id,
  550. fallback_keys,
  551. )
  552. await self.invalidate_cache_and_stream(
  553. "get_e2e_unused_fallback_key_types", (user_id, device_id)
  554. )
  555. def _set_e2e_fallback_keys_txn(
  556. self,
  557. txn: LoggingTransaction,
  558. user_id: str,
  559. device_id: str,
  560. fallback_keys: JsonDict,
  561. ) -> None:
  562. # fallback_keys will usually only have one item in it, so using a for
  563. # loop (as opposed to calling simple_upsert_many_txn) won't be too bad
  564. # FIXME: make sure that only one key per algorithm is uploaded
  565. for key_id, fallback_key in fallback_keys.items():
  566. algorithm, key_id = key_id.split(":", 1)
  567. old_key_json = self.db_pool.simple_select_one_onecol_txn(
  568. txn,
  569. table="e2e_fallback_keys_json",
  570. keyvalues={
  571. "user_id": user_id,
  572. "device_id": device_id,
  573. "algorithm": algorithm,
  574. },
  575. retcol="key_json",
  576. allow_none=True,
  577. )
  578. new_key_json = encode_canonical_json(fallback_key).decode("utf-8")
  579. # If the uploaded key is the same as the current fallback key,
  580. # don't do anything. This prevents marking the key as unused if it
  581. # was already used.
  582. if old_key_json != new_key_json:
  583. self.db_pool.simple_upsert_txn(
  584. txn,
  585. table="e2e_fallback_keys_json",
  586. keyvalues={
  587. "user_id": user_id,
  588. "device_id": device_id,
  589. "algorithm": algorithm,
  590. },
  591. values={
  592. "key_id": key_id,
  593. "key_json": json_encoder.encode(fallback_key),
  594. "used": False,
  595. },
  596. )
  597. @cached(max_entries=10000)
  598. async def get_e2e_unused_fallback_key_types(
  599. self, user_id: str, device_id: str
  600. ) -> Sequence[str]:
  601. """Returns the fallback key types that have an unused key.
  602. Args:
  603. user_id: the user whose keys are being queried
  604. device_id: the device whose keys are being queried
  605. Returns:
  606. a list of key types
  607. """
  608. return await self.db_pool.simple_select_onecol(
  609. "e2e_fallback_keys_json",
  610. keyvalues={"user_id": user_id, "device_id": device_id, "used": False},
  611. retcol="algorithm",
  612. desc="get_e2e_unused_fallback_key_types",
  613. )
  614. async def get_e2e_cross_signing_key(
  615. self, user_id: str, key_type: str, from_user_id: Optional[str] = None
  616. ) -> Optional[JsonDict]:
  617. """Returns a user's cross-signing key.
  618. Args:
  619. user_id: the user whose key is being requested
  620. key_type: the type of key that is being requested: either 'master'
  621. for a master key, 'self_signing' for a self-signing key, or
  622. 'user_signing' for a user-signing key
  623. from_user_id: if specified, signatures made by this user on
  624. the self-signing key will be included in the result
  625. Returns:
  626. dict of the key data or None if not found
  627. """
  628. res = await self.get_e2e_cross_signing_keys_bulk([user_id], from_user_id)
  629. user_keys = res.get(user_id)
  630. if not user_keys:
  631. return None
  632. return user_keys.get(key_type)
  633. @cached(num_args=1)
  634. def _get_bare_e2e_cross_signing_keys(self, user_id: str) -> Mapping[str, JsonDict]:
  635. """Dummy function. Only used to make a cache for
  636. _get_bare_e2e_cross_signing_keys_bulk.
  637. """
  638. raise NotImplementedError()
  639. @cachedList(
  640. cached_method_name="_get_bare_e2e_cross_signing_keys",
  641. list_name="user_ids",
  642. num_args=1,
  643. )
  644. async def _get_bare_e2e_cross_signing_keys_bulk(
  645. self, user_ids: Iterable[str]
  646. ) -> Dict[str, Optional[Mapping[str, JsonDict]]]:
  647. """Returns the cross-signing keys for a set of users. The output of this
  648. function should be passed to _get_e2e_cross_signing_signatures_txn if
  649. the signatures for the calling user need to be fetched.
  650. Args:
  651. user_ids: the users whose keys are being requested
  652. Returns:
  653. A mapping from user ID to key type to key data. If a user's cross-signing
  654. keys were not found, either their user ID will not be in the dict, or
  655. their user ID will map to None.
  656. """
  657. result = await self.db_pool.runInteraction(
  658. "get_bare_e2e_cross_signing_keys_bulk",
  659. self._get_bare_e2e_cross_signing_keys_bulk_txn,
  660. user_ids,
  661. )
  662. # The `Optional` comes from the `@cachedList` decorator.
  663. return cast(Dict[str, Optional[Mapping[str, JsonDict]]], result)
  664. def _get_bare_e2e_cross_signing_keys_bulk_txn(
  665. self,
  666. txn: LoggingTransaction,
  667. user_ids: Iterable[str],
  668. ) -> Dict[str, Dict[str, JsonDict]]:
  669. """Returns the cross-signing keys for a set of users. The output of this
  670. function should be passed to _get_e2e_cross_signing_signatures_txn if
  671. the signatures for the calling user need to be fetched.
  672. Args:
  673. txn: db connection
  674. user_ids: the users whose keys are being requested
  675. Returns:
  676. Mapping from user ID to key type to key data.
  677. If a user's cross-signing keys were not found, their user ID will not be in
  678. the dict.
  679. """
  680. result: Dict[str, Dict[str, JsonDict]] = {}
  681. for user_chunk in batch_iter(user_ids, 100):
  682. clause, params = make_in_list_sql_clause(
  683. txn.database_engine, "user_id", user_chunk
  684. )
  685. # Fetch the latest key for each type per user.
  686. if isinstance(self.database_engine, PostgresEngine):
  687. # The `DISTINCT ON` clause will pick the *first* row it
  688. # encounters, so ordering by stream ID desc will ensure we get
  689. # the latest key.
  690. sql = """
  691. SELECT DISTINCT ON (user_id, keytype) user_id, keytype, keydata, stream_id
  692. FROM e2e_cross_signing_keys
  693. WHERE %(clause)s
  694. ORDER BY user_id, keytype, stream_id DESC
  695. """ % {
  696. "clause": clause
  697. }
  698. else:
  699. # SQLite has special handling for bare columns when using
  700. # MIN/MAX with a `GROUP BY` clause where it picks the value from
  701. # a row that matches the MIN/MAX.
  702. sql = """
  703. SELECT user_id, keytype, keydata, MAX(stream_id)
  704. FROM e2e_cross_signing_keys
  705. WHERE %(clause)s
  706. GROUP BY user_id, keytype
  707. """ % {
  708. "clause": clause
  709. }
  710. txn.execute(sql, params)
  711. rows = self.db_pool.cursor_to_dict(txn)
  712. for row in rows:
  713. user_id = row["user_id"]
  714. key_type = row["keytype"]
  715. key = db_to_json(row["keydata"])
  716. user_keys = result.setdefault(user_id, {})
  717. user_keys[key_type] = key
  718. return result
  719. def _get_e2e_cross_signing_signatures_txn(
  720. self,
  721. txn: LoggingTransaction,
  722. keys: Dict[str, Optional[Dict[str, JsonDict]]],
  723. from_user_id: str,
  724. ) -> Dict[str, Optional[Dict[str, JsonDict]]]:
  725. """Returns the cross-signing signatures made by a user on a set of keys.
  726. Args:
  727. txn: db connection
  728. keys: a map of user ID to key type to key data.
  729. This dict will be modified to add signatures.
  730. from_user_id: fetch the signatures made by this user
  731. Returns:
  732. Mapping from user ID to key type to key data.
  733. The return value will be the same as the keys argument, with the
  734. modifications included.
  735. """
  736. # find out what cross-signing keys (a.k.a. devices) we need to get
  737. # signatures for. This is a map of (user_id, device_id) to key type
  738. # (device_id is the key's public part).
  739. devices: Dict[Tuple[str, str], str] = {}
  740. for user_id, user_keys in keys.items():
  741. if user_keys is None:
  742. continue
  743. for key_type, key in user_keys.items():
  744. device_id = None
  745. for k in key["keys"].values():
  746. device_id = k
  747. # `key` ought to be a `CrossSigningKey`, whose .keys property is a
  748. # dictionary with a single entry:
  749. # "algorithm:base64_public_key": "base64_public_key"
  750. # See https://spec.matrix.org/v1.1/client-server-api/#cross-signing
  751. assert isinstance(device_id, str)
  752. devices[(user_id, device_id)] = key_type
  753. for batch in batch_iter(devices.keys(), size=100):
  754. sql = """
  755. SELECT target_user_id, target_device_id, key_id, signature
  756. FROM e2e_cross_signing_signatures
  757. WHERE user_id = ?
  758. AND (%s)
  759. """ % (
  760. " OR ".join(
  761. "(target_user_id = ? AND target_device_id = ?)" for _ in batch
  762. )
  763. )
  764. query_params = [from_user_id]
  765. for item in batch:
  766. # item is a (user_id, device_id) tuple
  767. query_params.extend(item)
  768. txn.execute(sql, query_params)
  769. rows = self.db_pool.cursor_to_dict(txn)
  770. # and add the signatures to the appropriate keys
  771. for row in rows:
  772. key_id: str = row["key_id"]
  773. target_user_id: str = row["target_user_id"]
  774. target_device_id: str = row["target_device_id"]
  775. key_type = devices[(target_user_id, target_device_id)]
  776. # We need to copy everything, because the result may have come
  777. # from the cache. dict.copy only does a shallow copy, so we
  778. # need to recursively copy the dicts that will be modified.
  779. user_keys = keys[target_user_id]
  780. # `user_keys` cannot be `None` because we only fetched signatures for
  781. # users with keys
  782. assert user_keys is not None
  783. user_keys = keys[target_user_id] = user_keys.copy()
  784. target_user_key = user_keys[key_type] = user_keys[key_type].copy()
  785. if "signatures" in target_user_key:
  786. signatures = target_user_key["signatures"] = target_user_key[
  787. "signatures"
  788. ].copy()
  789. if from_user_id in signatures:
  790. user_sigs = signatures[from_user_id] = signatures[from_user_id]
  791. user_sigs[key_id] = row["signature"]
  792. else:
  793. signatures[from_user_id] = {key_id: row["signature"]}
  794. else:
  795. target_user_key["signatures"] = {
  796. from_user_id: {key_id: row["signature"]}
  797. }
  798. return keys
  799. @cancellable
  800. async def get_e2e_cross_signing_keys_bulk(
  801. self, user_ids: List[str], from_user_id: Optional[str] = None
  802. ) -> Dict[str, Optional[Mapping[str, JsonDict]]]:
  803. """Returns the cross-signing keys for a set of users.
  804. Args:
  805. user_ids: the users whose keys are being requested
  806. from_user_id: if specified, signatures made by this user on
  807. the self-signing keys will be included in the result
  808. Returns:
  809. A map of user ID to key type to key data. If a user's cross-signing
  810. keys were not found, either their user ID will not be in the dict,
  811. or their user ID will map to None.
  812. """
  813. result = await self._get_bare_e2e_cross_signing_keys_bulk(user_ids)
  814. if from_user_id:
  815. result = cast(
  816. Dict[str, Optional[Mapping[str, JsonDict]]],
  817. await self.db_pool.runInteraction(
  818. "get_e2e_cross_signing_signatures",
  819. self._get_e2e_cross_signing_signatures_txn,
  820. result,
  821. from_user_id,
  822. ),
  823. )
  824. return result
  825. async def get_all_user_signature_changes_for_remotes(
  826. self, instance_name: str, last_id: int, current_id: int, limit: int
  827. ) -> Tuple[List[Tuple[int, tuple]], int, bool]:
  828. """Get updates for groups replication stream.
  829. Note that the user signature stream represents when a user signs their
  830. device with their user-signing key, which is not published to other
  831. users or servers, so no `destination` is needed in the returned
  832. list. However, this is needed to poke workers.
  833. Args:
  834. instance_name: The writer we want to fetch updates from. Unused
  835. here since there is only ever one writer.
  836. last_id: The token to fetch updates from. Exclusive.
  837. current_id: The token to fetch updates up to. Inclusive.
  838. limit: The requested limit for the number of rows to return. The
  839. function may return more or fewer rows.
  840. Returns:
  841. A tuple consisting of: the updates, a token to use to fetch
  842. subsequent updates, and whether we returned fewer rows than exists
  843. between the requested tokens due to the limit.
  844. The token returned can be used in a subsequent call to this
  845. function to get further updatees.
  846. The updates are a list of 2-tuples of stream ID and the row data
  847. """
  848. if last_id == current_id:
  849. return [], current_id, False
  850. def _get_all_user_signature_changes_for_remotes_txn(
  851. txn: LoggingTransaction,
  852. ) -> Tuple[List[Tuple[int, tuple]], int, bool]:
  853. sql = """
  854. SELECT stream_id, from_user_id AS user_id
  855. FROM user_signature_stream
  856. WHERE ? < stream_id AND stream_id <= ?
  857. ORDER BY stream_id ASC
  858. LIMIT ?
  859. """
  860. txn.execute(sql, (last_id, current_id, limit))
  861. updates = [(row[0], (row[1:])) for row in txn]
  862. limited = False
  863. upto_token = current_id
  864. if len(updates) >= limit:
  865. upto_token = updates[-1][0]
  866. limited = True
  867. return updates, upto_token, limited
  868. return await self.db_pool.runInteraction(
  869. "get_all_user_signature_changes_for_remotes",
  870. _get_all_user_signature_changes_for_remotes_txn,
  871. )
  872. @abc.abstractmethod
  873. def get_device_stream_token(self) -> int:
  874. """Get the current stream id from the _device_list_id_gen"""
  875. ...
  876. async def claim_e2e_one_time_keys(
  877. self, query_list: Iterable[Tuple[str, str, str]]
  878. ) -> Dict[str, Dict[str, Dict[str, str]]]:
  879. """Take a list of one time keys out of the database.
  880. Args:
  881. query_list: An iterable of tuples of (user ID, device ID, algorithm).
  882. Returns:
  883. A map of user ID -> a map device ID -> a map of key ID -> JSON bytes.
  884. """
  885. @trace
  886. def _claim_e2e_one_time_key_simple(
  887. txn: LoggingTransaction, user_id: str, device_id: str, algorithm: str
  888. ) -> Optional[Tuple[str, str]]:
  889. """Claim OTK for device for DBs that don't support RETURNING.
  890. Returns:
  891. A tuple of key name (algorithm + key ID) and key JSON, if an
  892. OTK was found.
  893. """
  894. sql = """
  895. SELECT key_id, key_json FROM e2e_one_time_keys_json
  896. WHERE user_id = ? AND device_id = ? AND algorithm = ?
  897. LIMIT 1
  898. """
  899. txn.execute(sql, (user_id, device_id, algorithm))
  900. otk_row = txn.fetchone()
  901. if otk_row is None:
  902. return None
  903. key_id, key_json = otk_row
  904. self.db_pool.simple_delete_one_txn(
  905. txn,
  906. table="e2e_one_time_keys_json",
  907. keyvalues={
  908. "user_id": user_id,
  909. "device_id": device_id,
  910. "algorithm": algorithm,
  911. "key_id": key_id,
  912. },
  913. )
  914. self._invalidate_cache_and_stream(
  915. txn, self.count_e2e_one_time_keys, (user_id, device_id)
  916. )
  917. return f"{algorithm}:{key_id}", key_json
  918. @trace
  919. def _claim_e2e_one_time_key_returning(
  920. txn: LoggingTransaction, user_id: str, device_id: str, algorithm: str
  921. ) -> Optional[Tuple[str, str]]:
  922. """Claim OTK for device for DBs that support RETURNING.
  923. Returns:
  924. A tuple of key name (algorithm + key ID) and key JSON, if an
  925. OTK was found.
  926. """
  927. # We can use RETURNING to do the fetch and DELETE in once step.
  928. sql = """
  929. DELETE FROM e2e_one_time_keys_json
  930. WHERE user_id = ? AND device_id = ? AND algorithm = ?
  931. AND key_id IN (
  932. SELECT key_id FROM e2e_one_time_keys_json
  933. WHERE user_id = ? AND device_id = ? AND algorithm = ?
  934. LIMIT 1
  935. )
  936. RETURNING key_id, key_json
  937. """
  938. txn.execute(
  939. sql, (user_id, device_id, algorithm, user_id, device_id, algorithm)
  940. )
  941. otk_row = txn.fetchone()
  942. if otk_row is None:
  943. return None
  944. self._invalidate_cache_and_stream(
  945. txn, self.count_e2e_one_time_keys, (user_id, device_id)
  946. )
  947. key_id, key_json = otk_row
  948. return f"{algorithm}:{key_id}", key_json
  949. results: Dict[str, Dict[str, Dict[str, str]]] = {}
  950. for user_id, device_id, algorithm in query_list:
  951. if self.database_engine.supports_returning:
  952. # If we support RETURNING clause we can use a single query that
  953. # allows us to use autocommit mode.
  954. _claim_e2e_one_time_key = _claim_e2e_one_time_key_returning
  955. db_autocommit = True
  956. else:
  957. _claim_e2e_one_time_key = _claim_e2e_one_time_key_simple
  958. db_autocommit = False
  959. claim_row = await self.db_pool.runInteraction(
  960. "claim_e2e_one_time_keys",
  961. _claim_e2e_one_time_key,
  962. user_id,
  963. device_id,
  964. algorithm,
  965. db_autocommit=db_autocommit,
  966. )
  967. if claim_row:
  968. device_results = results.setdefault(user_id, {}).setdefault(
  969. device_id, {}
  970. )
  971. device_results[claim_row[0]] = claim_row[1]
  972. continue
  973. # No one-time key available, so see if there's a fallback
  974. # key
  975. row = await self.db_pool.simple_select_one(
  976. table="e2e_fallback_keys_json",
  977. keyvalues={
  978. "user_id": user_id,
  979. "device_id": device_id,
  980. "algorithm": algorithm,
  981. },
  982. retcols=("key_id", "key_json", "used"),
  983. desc="_get_fallback_key",
  984. allow_none=True,
  985. )
  986. if row is None:
  987. continue
  988. key_id = row["key_id"]
  989. key_json = row["key_json"]
  990. used = row["used"]
  991. # Mark fallback key as used if not already.
  992. if not used:
  993. await self.db_pool.simple_update_one(
  994. table="e2e_fallback_keys_json",
  995. keyvalues={
  996. "user_id": user_id,
  997. "device_id": device_id,
  998. "algorithm": algorithm,
  999. "key_id": key_id,
  1000. },
  1001. updatevalues={"used": True},
  1002. desc="_get_fallback_key_set_used",
  1003. )
  1004. await self.invalidate_cache_and_stream(
  1005. "get_e2e_unused_fallback_key_types", (user_id, device_id)
  1006. )
  1007. device_results = results.setdefault(user_id, {}).setdefault(device_id, {})
  1008. device_results[f"{algorithm}:{key_id}"] = key_json
  1009. return results
  1010. class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore):
  1011. def __init__(
  1012. self,
  1013. database: DatabasePool,
  1014. db_conn: LoggingDatabaseConnection,
  1015. hs: "HomeServer",
  1016. ):
  1017. super().__init__(database, db_conn, hs)
  1018. self._cross_signing_id_gen = StreamIdGenerator(
  1019. db_conn,
  1020. hs.get_replication_notifier(),
  1021. "e2e_cross_signing_keys",
  1022. "stream_id",
  1023. )
  1024. async def set_e2e_device_keys(
  1025. self, user_id: str, device_id: str, time_now: int, device_keys: JsonDict
  1026. ) -> bool:
  1027. """Stores device keys for a device. Returns whether there was a change
  1028. or the keys were already in the database.
  1029. """
  1030. def _set_e2e_device_keys_txn(txn: LoggingTransaction) -> bool:
  1031. set_tag("user_id", user_id)
  1032. set_tag("device_id", device_id)
  1033. set_tag("time_now", time_now)
  1034. set_tag("device_keys", str(device_keys))
  1035. old_key_json = self.db_pool.simple_select_one_onecol_txn(
  1036. txn,
  1037. table="e2e_device_keys_json",
  1038. keyvalues={"user_id": user_id, "device_id": device_id},
  1039. retcol="key_json",
  1040. allow_none=True,
  1041. )
  1042. # In py3 we need old_key_json to match new_key_json type. The DB
  1043. # returns unicode while encode_canonical_json returns bytes.
  1044. new_key_json = encode_canonical_json(device_keys).decode("utf-8")
  1045. if old_key_json == new_key_json:
  1046. log_kv({"Message": "Device key already stored."})
  1047. return False
  1048. self.db_pool.simple_upsert_txn(
  1049. txn,
  1050. table="e2e_device_keys_json",
  1051. keyvalues={"user_id": user_id, "device_id": device_id},
  1052. values={"ts_added_ms": time_now, "key_json": new_key_json},
  1053. )
  1054. log_kv({"message": "Device keys stored."})
  1055. return True
  1056. return await self.db_pool.runInteraction(
  1057. "set_e2e_device_keys", _set_e2e_device_keys_txn
  1058. )
  1059. async def delete_e2e_keys_by_device(self, user_id: str, device_id: str) -> None:
  1060. def delete_e2e_keys_by_device_txn(txn: LoggingTransaction) -> None:
  1061. log_kv(
  1062. {
  1063. "message": "Deleting keys for device",
  1064. "device_id": device_id,
  1065. "user_id": user_id,
  1066. }
  1067. )
  1068. self.db_pool.simple_delete_txn(
  1069. txn,
  1070. table="e2e_device_keys_json",
  1071. keyvalues={"user_id": user_id, "device_id": device_id},
  1072. )
  1073. self.db_pool.simple_delete_txn(
  1074. txn,
  1075. table="e2e_one_time_keys_json",
  1076. keyvalues={"user_id": user_id, "device_id": device_id},
  1077. )
  1078. self._invalidate_cache_and_stream(
  1079. txn, self.count_e2e_one_time_keys, (user_id, device_id)
  1080. )
  1081. self.db_pool.simple_delete_txn(
  1082. txn,
  1083. table="dehydrated_devices",
  1084. keyvalues={"user_id": user_id, "device_id": device_id},
  1085. )
  1086. self.db_pool.simple_delete_txn(
  1087. txn,
  1088. table="e2e_fallback_keys_json",
  1089. keyvalues={"user_id": user_id, "device_id": device_id},
  1090. )
  1091. self._invalidate_cache_and_stream(
  1092. txn, self.get_e2e_unused_fallback_key_types, (user_id, device_id)
  1093. )
  1094. await self.db_pool.runInteraction(
  1095. "delete_e2e_keys_by_device", delete_e2e_keys_by_device_txn
  1096. )
  1097. def _set_e2e_cross_signing_key_txn(
  1098. self,
  1099. txn: LoggingTransaction,
  1100. user_id: str,
  1101. key_type: str,
  1102. key: JsonDict,
  1103. stream_id: int,
  1104. ) -> None:
  1105. """Set a user's cross-signing key.
  1106. Args:
  1107. txn: db connection
  1108. user_id: the user to set the signing key for
  1109. key_type: the type of key that is being set: either 'master'
  1110. for a master key, 'self_signing' for a self-signing key, or
  1111. 'user_signing' for a user-signing key
  1112. key: the key data
  1113. stream_id
  1114. """
  1115. # the 'key' dict will look something like:
  1116. # {
  1117. # "user_id": "@alice:example.com",
  1118. # "usage": ["self_signing"],
  1119. # "keys": {
  1120. # "ed25519:base64+self+signing+public+key": "base64+self+signing+public+key",
  1121. # },
  1122. # "signatures": {
  1123. # "@alice:example.com": {
  1124. # "ed25519:base64+master+public+key": "base64+signature"
  1125. # }
  1126. # }
  1127. # }
  1128. # The "keys" property must only have one entry, which will be the public
  1129. # key, so we just grab the first value in there
  1130. pubkey = next(iter(key["keys"].values()))
  1131. # The cross-signing keys need to occupy the same namespace as devices,
  1132. # since signatures are identified by device ID. So add an entry to the
  1133. # device table to make sure that we don't have a collision with device
  1134. # IDs.
  1135. # We only need to do this for local users, since remote servers should be
  1136. # responsible for checking this for their own users.
  1137. if self.hs.is_mine_id(user_id):
  1138. self.db_pool.simple_insert_txn(
  1139. txn,
  1140. "devices",
  1141. values={
  1142. "user_id": user_id,
  1143. "device_id": pubkey,
  1144. "display_name": key_type + " signing key",
  1145. "hidden": True,
  1146. },
  1147. )
  1148. # and finally, store the key itself
  1149. self.db_pool.simple_insert_txn(
  1150. txn,
  1151. "e2e_cross_signing_keys",
  1152. values={
  1153. "user_id": user_id,
  1154. "keytype": key_type,
  1155. "keydata": json_encoder.encode(key),
  1156. "stream_id": stream_id,
  1157. },
  1158. )
  1159. self._invalidate_cache_and_stream(
  1160. txn, self._get_bare_e2e_cross_signing_keys, (user_id,)
  1161. )
  1162. async def set_e2e_cross_signing_key(
  1163. self, user_id: str, key_type: str, key: JsonDict
  1164. ) -> None:
  1165. """Set a user's cross-signing key.
  1166. Args:
  1167. user_id: the user to set the user-signing key for
  1168. key_type: the type of cross-signing key to set
  1169. key: the key data
  1170. """
  1171. async with self._cross_signing_id_gen.get_next() as stream_id:
  1172. return await self.db_pool.runInteraction(
  1173. "add_e2e_cross_signing_key",
  1174. self._set_e2e_cross_signing_key_txn,
  1175. user_id,
  1176. key_type,
  1177. key,
  1178. stream_id,
  1179. )
  1180. async def store_e2e_cross_signing_signatures(
  1181. self, user_id: str, signatures: "Iterable[SignatureListItem]"
  1182. ) -> None:
  1183. """Stores cross-signing signatures.
  1184. Args:
  1185. user_id: the user who made the signatures
  1186. signatures: signatures to add
  1187. """
  1188. await self.db_pool.simple_insert_many(
  1189. "e2e_cross_signing_signatures",
  1190. keys=(
  1191. "user_id",
  1192. "key_id",
  1193. "target_user_id",
  1194. "target_device_id",
  1195. "signature",
  1196. ),
  1197. values=[
  1198. (
  1199. user_id,
  1200. item.signing_key_id,
  1201. item.target_user_id,
  1202. item.target_device_id,
  1203. item.signature,
  1204. )
  1205. for item in signatures
  1206. ],
  1207. desc="add_e2e_signing_key",
  1208. )