Skip to content

Commit bed00bb

Browse files
sandhosekegsay-elementerikjohnston
authored
Allow resigning of events with a new signing key (#19668)
This adds a way to re-sign all locally-created events with a new signing key, which is useful when rotating server signing keys. This doesn't trigger automatically, instead needs to be triggered when needed via the admin API. c.f. matrix-org/internal-config#1670 (comment) for internal discussion. --------- Co-authored-by: Kegan Dougall <kegan@element.io> Co-authored-by: Erik Johnston <erikj@element.io>
1 parent 1a94960 commit bed00bb

10 files changed

Lines changed: 594 additions & 33 deletions

File tree

changelog.d/19668.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a way to re-sign local events with a new signing key.

docs/usage/administration/admin_api/background_updates.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,6 @@ The following JSON body parameters are available:
107107
- `job_name` - A string which job to run. Valid values are:
108108
- `populate_stats_process_rooms` - Recalculate the stats for all rooms.
109109
- `regenerate_directory` - Recalculate the [user directory](../../../user_directory.md) if it is stale or out of sync.
110+
- `event_resign` - Re-sign all locally-sent events with the current signing key. This is useful after rotating the server's signing key to ensure all historical events are signed with the new key. Optional additional parameters:
111+
- `old_key` - Only re-sign events whose signature verifies against this key. Format: `"ed25519:key_id base64_public_key"` (e.g. `"ed25519:my_old_key XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"`).
112+
- `before_ts` - Only re-sign events with a `received_ts` less than this value (milliseconds since the epoch).

poetry.lock

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ dependencies = [
2929
# We require 2.0.0 for immutabledict support.
3030
"canonicaljson>=2.0.0,<3.0.0",
3131
# we use the type definitions added in signedjson 1.1.
32-
"signedjson>=1.1.0,<2.0.0",
32+
# 1.1.0 erroneously removed decode_verify_key_base64 (reintroduced in 1.1.1).
33+
# 1.1.1 is mispackaged (importlib-metadata dependency without minimum version bound)
34+
# 1.1.2, 1.1.3 and 1.1.4 were all released on the same day, so no good reason to use the older version.
35+
"signedjson>=1.1.4,<2.0.0",
3336
# validating SSL certs for IP addresses requires service_identity 18.1.
3437
"service-identity>=18.1.0",
3538
# Twisted 18.9 introduces some logger improvements that the structured

synapse/crypto/event_signing.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727

2828
from canonicaljson import encode_canonical_json
2929
from signedjson.sign import sign_json
30-
from signedjson.types import SigningKey
30+
from signedjson.types import SigningKey, VerifyKey
3131
from unpaddedbase64 import decode_base64, encode_base64
3232

3333
from synapse.api.errors import Codes, SynapseError
3434
from synapse.api.room_versions import RoomVersion
3535
from synapse.events import EventBase
3636
from synapse.events.utils import prune_event, prune_event_dict
3737
from synapse.logging.opentracing import trace
38-
from synapse.types import JsonDict
38+
from synapse.types import JsonDict, UserID
3939

4040
logger = logging.getLogger(__name__)
4141

@@ -192,3 +192,54 @@ def add_hashes_and_signatures(
192192
event_dict["signatures"] = compute_event_signature(
193193
room_version, event_dict, signature_name=signature_name, signing_key=signing_key
194194
)
195+
196+
197+
def resign_event(
198+
ev: EventBase,
199+
server_name: str,
200+
signing_key: SigningKey,
201+
time_now: int | None = None,
202+
) -> JsonDict:
203+
"""Re-sign the provided event with the given signing key. Any existing signatures on the event
204+
for this server_name are removed.
205+
206+
If there has been no signature for this event by this server_name, the event is still re-signed.
207+
If there have been signatures on this event by this server_name, the event is not re-checked for
208+
validity. As such, only events that have valid signatures should be passed into this function
209+
e.g. from the event_json table in the database.
210+
"""
211+
event_dict = ev.get_pdu_json(time_now=time_now)
212+
event_dict["signatures"].pop(
213+
server_name, None
214+
) # remove existing signatures for this server_name
215+
event_dict["signatures"].update(
216+
compute_event_signature(
217+
ev.room_version,
218+
event_dict,
219+
server_name,
220+
signing_key,
221+
)
222+
)
223+
return event_dict
224+
225+
226+
def event_needs_resigning(
227+
ev: EventBase, server_name: str, verify_key: VerifyKey
228+
) -> bool:
229+
"""Check if this event needs re-signing.
230+
231+
This returns True if all of the following are True:
232+
- the event `sender` domain matches the `server_name` provided.
233+
- the event has not been already signed with this `verify_key`.
234+
"""
235+
sender = UserID.from_string(ev.sender)
236+
if sender.domain != server_name:
237+
return False
238+
want_key_id = verify_key.alg + ":" + verify_key.version
239+
signed_with_current_key_id = ev.signatures.get(server_name, {}).get(
240+
want_key_id, None
241+
)
242+
if signed_with_current_key_id:
243+
return False
244+
245+
return True

synapse/rest/admin/background_updates.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# [This file includes modifications made by New Vector Limited]
1919
#
2020
#
21+
import json
2122
import logging
2223
from http import HTTPStatus
2324
from typing import TYPE_CHECKING
@@ -150,6 +151,24 @@ async def on_POST(self, request: SynapseRequest) -> tuple[int, JsonDict]:
150151
"populate_user_directory_process_users",
151152
),
152153
]
154+
elif job_name == "event_resign":
155+
old_key = body.get("old_key")
156+
if old_key is not None and not isinstance(old_key, str):
157+
raise SynapseError(
158+
HTTPStatus.BAD_REQUEST,
159+
"'old_key' must be a string",
160+
)
161+
before_ts = body.get("before_ts")
162+
if before_ts is not None and not isinstance(before_ts, int):
163+
raise SynapseError(
164+
HTTPStatus.BAD_REQUEST,
165+
"'before_ts' must be an integer",
166+
)
167+
progress = {
168+
"old_key": old_key,
169+
"before_ts": before_ts,
170+
}
171+
jobs = [("event_resign", json.dumps(progress), "")]
153172
else:
154173
raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid job_name")
155174

0 commit comments

Comments
 (0)