Skip to content

Commit 259442f

Browse files
kegsayreivilibre
andauthored
bugfix: make msc3967 idempotent (#16943)
MSC3967 was updated recently to make it more robust to network failures: > there is an existing cross-signing master key and it exactly matches the cross-signing master key provided in the request body. If there are any additional keys provided in the request (self signing key, user signing key) they MUST also match the existing keys stored on the server. In other words, the request contains no new keys. If there are new keys, UIA MUST be performed. https://github.com/matrix-org/matrix-spec-proposals/blob/hughns/device-signing-upload-uia/proposals/3967-device-signing-upload-uia.md#proposal This covers the case where the 200 OK is lost in transit so the client retries the upload, only to then get UIA'd. Complement tests: matrix-org/complement#713 - passing example https://github.com/element-hq/synapse/actions/runs/7976948122/job/21778795094?pr=16943#step:7:8820 ### Pull Request Checklist <!-- Please read https://element-hq.github.io/synapse/latest/development/contributing_guide.html before submitting your pull request --> * [x] Pull request is based on the develop branch * [x] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [x] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: reivilibre <[email protected]>
1 parent fe4719a commit 259442f

File tree

6 files changed

+102
-3
lines changed

6 files changed

+102
-3
lines changed

changelog.d/16943.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make the CSAPI endpoint `/keys/device_signing/upload` idempotent.

docker/complement/conf/workers-shared-extra.yaml.j2

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ experimental_features:
102102
msc3391_enabled: true
103103
# Filtering /messages by relation type.
104104
msc3874_enabled: true
105+
# no UIA for x-signing upload for the first time
106+
msc3967_enabled: true
105107

106108
server_notices:
107109
system_mxid_localpart: _server

scripts-dev/complement.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ fi
214214

215215
extra_test_args=()
216216

217-
test_packages="./tests/csapi ./tests ./tests/msc3874 ./tests/msc3890 ./tests/msc3391 ./tests/msc3930 ./tests/msc3902"
217+
test_packages="./tests/csapi ./tests ./tests/msc3874 ./tests/msc3890 ./tests/msc3391 ./tests/msc3930 ./tests/msc3902 ./tests/msc3967"
218218

219219
# Enable dirty runs, so tests will reuse the same container where possible.
220220
# This significantly speeds up tests, but increases the possibility of test pollution.

synapse/handlers/e2e_keys.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,42 @@ async def check_cross_signing_setup(self, user_id: str) -> Tuple[bool, bool]:
14761476
else:
14771477
return exists, self.clock.time_msec() < ts_replacable_without_uia_before
14781478

1479+
async def has_different_keys(self, user_id: str, body: JsonDict) -> bool:
1480+
"""
1481+
Check if a key provided in `body` differs from the same key stored in the DB. Returns
1482+
true on the first difference. If a key exists in `body` but does not exist in the DB,
1483+
returns True. If `body` has no keys, this always returns False.
1484+
Note by 'key' we mean Matrix key rather than JSON key.
1485+
1486+
The purpose of this function is to detect whether or not we need to apply UIA checks.
1487+
We must apply UIA checks if any key in the database is being overwritten. If a key is
1488+
being inserted for the first time, or if the key exactly matches what is in the database,
1489+
then no UIA check needs to be performed.
1490+
1491+
Args:
1492+
user_id: The user who sent the `body`.
1493+
body: The JSON request body from POST /keys/device_signing/upload
1494+
Returns:
1495+
True if any key in `body` has a different value in the database.
1496+
"""
1497+
# Ensure that each key provided in the request body exactly matches the one we have stored.
1498+
# The first time we see the DB having a different key to the matching request key, bail.
1499+
# Note: we do not care if the DB has a key which the request does not specify, as we only
1500+
# care about *replacements* or *insertions* (i.e UPSERT)
1501+
req_body_key_to_db_key = {
1502+
"master_key": "master",
1503+
"self_signing_key": "self_signing",
1504+
"user_signing_key": "user_signing",
1505+
}
1506+
for req_body_key, db_key in req_body_key_to_db_key.items():
1507+
if req_body_key in body:
1508+
existing_key = await self.store.get_e2e_cross_signing_key(
1509+
user_id, db_key
1510+
)
1511+
if existing_key != body[req_body_key]:
1512+
return True
1513+
return False
1514+
14791515

14801516
def _check_cross_signing_key(
14811517
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None

synapse/rest/client/keys.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,18 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
409409
# But first-time setup is fine
410410

411411
elif self.hs.config.experimental.msc3967_enabled:
412-
# If we already have a master key then cross signing is set up and we require UIA to reset
412+
# MSC3967 allows this endpoint to 200 OK for idempotency. Resending exactly the same
413+
# keys should just 200 OK without doing a UIA prompt.
414+
keys_are_different = await self.e2e_keys_handler.has_different_keys(
415+
user_id, body
416+
)
417+
if not keys_are_different:
418+
# FIXME: we do not fallthrough to upload_signing_keys_for_user because confusingly
419+
# if we do, we 500 as it looks like it tries to INSERT the same key twice, causing a
420+
# unique key constraint violation. This sounds like a bug?
421+
return 200, {}
422+
# the keys are different, is x-signing set up? If no, then the keys don't exist which is
423+
# why they are different. If yes, then we need to UIA to change them.
413424
if is_cross_signing_setup:
414425
await self.auth_handler.validate_user_via_ui_auth(
415426
requester,
@@ -420,7 +431,6 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
420431
can_skip_ui_auth=False,
421432
)
422433
# Otherwise we don't require UIA since we are setting up cross signing for first time
423-
424434
else:
425435
# Previous behaviour is to always require UIA but allow it to be skipped
426436
await self.auth_handler.validate_user_via_ui_auth(

tests/handlers/test_e2e_keys.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,56 @@ def test_query_devices_remote_no_sync(self) -> None:
11011101
},
11021102
)
11031103

1104+
def test_has_different_keys(self) -> None:
1105+
"""check that has_different_keys returns True when the keys provided are different to what
1106+
is in the database."""
1107+
local_user = "@boris:" + self.hs.hostname
1108+
keys1 = {
1109+
"master_key": {
1110+
# private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
1111+
"user_id": local_user,
1112+
"usage": ["master"],
1113+
"keys": {
1114+
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
1115+
},
1116+
}
1117+
}
1118+
self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
1119+
is_different = self.get_success(
1120+
self.handler.has_different_keys(
1121+
local_user,
1122+
{
1123+
"master_key": keys1["master_key"],
1124+
},
1125+
)
1126+
)
1127+
self.assertEqual(is_different, False)
1128+
# change the usage => different keys
1129+
keys1["master_key"]["usage"] = ["develop"]
1130+
is_different = self.get_success(
1131+
self.handler.has_different_keys(
1132+
local_user,
1133+
{
1134+
"master_key": keys1["master_key"],
1135+
},
1136+
)
1137+
)
1138+
self.assertEqual(is_different, True)
1139+
keys1["master_key"]["usage"] = ["master"] # reset
1140+
# change the key => different keys
1141+
keys1["master_key"]["keys"] = {
1142+
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unIc0rncs": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unIc0rncs"
1143+
}
1144+
is_different = self.get_success(
1145+
self.handler.has_different_keys(
1146+
local_user,
1147+
{
1148+
"master_key": keys1["master_key"],
1149+
},
1150+
)
1151+
)
1152+
self.assertEqual(is_different, True)
1153+
11041154
def test_query_devices_remote_sync(self) -> None:
11051155
"""Tests that querying keys for a remote user that we share a room with,
11061156
but haven't yet fetched the keys for, returns the cross signing keys

0 commit comments

Comments
 (0)