Refactor event signing to work on dicts
This is in preparation for making EventBuilder format agnostic, which means event signing should be done against the event dict rather than the EventBuilder object.
This commit is contained in:
parent
7072fe3084
commit
855a151015
|
@ -23,14 +23,14 @@ from signedjson.sign import sign_json
|
|||
from unpaddedbase64 import decode_base64, encode_base64
|
||||
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.events.utils import prune_event
|
||||
from synapse.events.utils import prune_event, prune_event_dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
|
||||
"""Check whether the hash for this PDU matches the contents"""
|
||||
name, expected_hash = compute_content_hash(event, hash_algorithm)
|
||||
name, expected_hash = compute_content_hash(event.get_pdu_json(), hash_algorithm)
|
||||
logger.debug("Expecting hash: %s", encode_base64(expected_hash))
|
||||
|
||||
# some malformed events lack a 'hashes'. Protect against it being missing
|
||||
|
@ -59,35 +59,70 @@ def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
|
|||
return message_hash_bytes == expected_hash
|
||||
|
||||
|
||||
def compute_content_hash(event, hash_algorithm):
|
||||
event_json = event.get_pdu_json()
|
||||
event_json.pop("age_ts", None)
|
||||
event_json.pop("unsigned", None)
|
||||
event_json.pop("signatures", None)
|
||||
event_json.pop("hashes", None)
|
||||
event_json.pop("outlier", None)
|
||||
event_json.pop("destinations", None)
|
||||
def compute_content_hash(event_dict, hash_algorithm):
|
||||
"""Compute the content hash of an event, which is the hash of the
|
||||
unredacted event.
|
||||
|
||||
event_json_bytes = encode_canonical_json(event_json)
|
||||
Args:
|
||||
event_dict (dict): The unredacted event as a dict
|
||||
hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
|
||||
to hash the event
|
||||
|
||||
Returns:
|
||||
tuple[str, bytes]: A tuple of the name of hash and the hash as raw
|
||||
bytes.
|
||||
"""
|
||||
event_dict = dict(event_dict)
|
||||
event_dict.pop("age_ts", None)
|
||||
event_dict.pop("unsigned", None)
|
||||
event_dict.pop("signatures", None)
|
||||
event_dict.pop("hashes", None)
|
||||
event_dict.pop("outlier", None)
|
||||
event_dict.pop("destinations", None)
|
||||
|
||||
event_json_bytes = encode_canonical_json(event_dict)
|
||||
|
||||
hashed = hash_algorithm(event_json_bytes)
|
||||
return (hashed.name, hashed.digest())
|
||||
|
||||
|
||||
def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256):
|
||||
"""Computes the event reference hash. This is the hash of the redacted
|
||||
event.
|
||||
|
||||
Args:
|
||||
event (FrozenEvent)
|
||||
hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
|
||||
to hash the event
|
||||
|
||||
Returns:
|
||||
tuple[str, bytes]: A tuple of the name of hash and the hash as raw
|
||||
bytes.
|
||||
"""
|
||||
tmp_event = prune_event(event)
|
||||
event_json = tmp_event.get_pdu_json()
|
||||
event_json.pop("signatures", None)
|
||||
event_json.pop("age_ts", None)
|
||||
event_json.pop("unsigned", None)
|
||||
event_json_bytes = encode_canonical_json(event_json)
|
||||
event_dict = tmp_event.get_pdu_json()
|
||||
event_dict.pop("signatures", None)
|
||||
event_dict.pop("age_ts", None)
|
||||
event_dict.pop("unsigned", None)
|
||||
event_json_bytes = encode_canonical_json(event_dict)
|
||||
hashed = hash_algorithm(event_json_bytes)
|
||||
return (hashed.name, hashed.digest())
|
||||
|
||||
|
||||
def compute_event_signature(event, signature_name, signing_key):
|
||||
tmp_event = prune_event(event)
|
||||
redact_json = tmp_event.get_pdu_json()
|
||||
def compute_event_signature(event_dict, signature_name, signing_key):
|
||||
"""Compute the signature of the event for the given name and key.
|
||||
|
||||
Args:
|
||||
event_dict (dict): The event as a dict
|
||||
signature_name (str): The name of the entity signing the event
|
||||
(typically the server's hostname).
|
||||
signing_key (syutil.crypto.SigningKey): The key to sign with
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, str]]: Returns a dictionary in the same format of
|
||||
an event's signatures field.
|
||||
"""
|
||||
redact_json = prune_event_dict(event_dict)
|
||||
redact_json.pop("age_ts", None)
|
||||
redact_json.pop("unsigned", None)
|
||||
logger.debug("Signing event: %s", encode_canonical_json(redact_json))
|
||||
|
@ -98,23 +133,27 @@ def compute_event_signature(event, signature_name, signing_key):
|
|||
|
||||
def add_hashes_and_signatures(event, signature_name, signing_key,
|
||||
hash_algorithm=hashlib.sha256):
|
||||
# if hasattr(event, "old_state_events"):
|
||||
# state_json_bytes = encode_canonical_json(
|
||||
# [e.event_id for e in event.old_state_events.values()]
|
||||
# )
|
||||
# hashed = hash_algorithm(state_json_bytes)
|
||||
# event.state_hash = {
|
||||
# hashed.name: encode_base64(hashed.digest())
|
||||
# }
|
||||
"""Add content hash and sign the event
|
||||
|
||||
name, digest = compute_content_hash(event, hash_algorithm=hash_algorithm)
|
||||
Args:
|
||||
event_dict (EventBuilder): The event to add hashes to and sign
|
||||
signature_name (str): The name of the entity signing the event
|
||||
(typically the server's hostname).
|
||||
signing_key (syutil.crypto.SigningKey): The key to sign with
|
||||
hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
|
||||
to hash the event
|
||||
"""
|
||||
|
||||
name, digest = compute_content_hash(
|
||||
event.get_pdu_json(), hash_algorithm=hash_algorithm,
|
||||
)
|
||||
|
||||
if not hasattr(event, "hashes"):
|
||||
event.hashes = {}
|
||||
event.hashes[name] = encode_base64(digest)
|
||||
|
||||
event.signatures = compute_event_signature(
|
||||
event,
|
||||
event.get_pdu_json(),
|
||||
signature_name=signature_name,
|
||||
signing_key=signing_key,
|
||||
)
|
||||
|
|
|
@ -38,8 +38,31 @@ def prune_event(event):
|
|||
This is used when we "redact" an event. We want to remove all fields that
|
||||
the user has specified, but we do want to keep necessary information like
|
||||
type, state_key etc.
|
||||
|
||||
Args:
|
||||
event (FrozenEvent)
|
||||
|
||||
Returns:
|
||||
FrozenEvent
|
||||
"""
|
||||
pruned_event_dict = prune_event_dict(event.get_dict())
|
||||
|
||||
from . import event_type_from_format_version
|
||||
return event_type_from_format_version(event.format_version)(
|
||||
pruned_event_dict, event.internal_metadata.get_dict()
|
||||
)
|
||||
|
||||
|
||||
def prune_event_dict(event_dict):
|
||||
"""Redacts the event_dict in the same way as `prune_event`, except it
|
||||
operates on dicts rather than event objects
|
||||
|
||||
Args:
|
||||
event_dict (dict)
|
||||
|
||||
Returns:
|
||||
dict: A copy of the pruned event dict
|
||||
"""
|
||||
event_type = event.type
|
||||
|
||||
allowed_keys = [
|
||||
"event_id",
|
||||
|
@ -59,13 +82,13 @@ def prune_event(event):
|
|||
"membership",
|
||||
]
|
||||
|
||||
event_dict = event.get_dict()
|
||||
event_type = event_dict["type"]
|
||||
|
||||
new_content = {}
|
||||
|
||||
def add_fields(*fields):
|
||||
for field in fields:
|
||||
if field in event.content:
|
||||
if field in event_dict["content"]:
|
||||
new_content[field] = event_dict["content"][field]
|
||||
|
||||
if event_type == EventTypes.Member:
|
||||
|
@ -98,17 +121,17 @@ def prune_event(event):
|
|||
|
||||
allowed_fields["content"] = new_content
|
||||
|
||||
allowed_fields["unsigned"] = {}
|
||||
unsigned = {}
|
||||
allowed_fields["unsigned"] = unsigned
|
||||
|
||||
if "age_ts" in event.unsigned:
|
||||
allowed_fields["unsigned"]["age_ts"] = event.unsigned["age_ts"]
|
||||
if "replaces_state" in event.unsigned:
|
||||
allowed_fields["unsigned"]["replaces_state"] = event.unsigned["replaces_state"]
|
||||
event_unsigned = event_dict.get("unsigned", {})
|
||||
|
||||
return type(event)(
|
||||
allowed_fields,
|
||||
internal_metadata_dict=event.internal_metadata.get_dict()
|
||||
)
|
||||
if "age_ts" in event_unsigned:
|
||||
unsigned["age_ts"] = event_unsigned["age_ts"]
|
||||
if "replaces_state" in event_unsigned:
|
||||
unsigned["replaces_state"] = event_unsigned["replaces_state"]
|
||||
|
||||
return allowed_fields
|
||||
|
||||
|
||||
def _copy_field(src, dst, field):
|
||||
|
|
Loading…
Reference in New Issue