Merge pull request #4493 from matrix-org/erikj/refactor_event_signing

Refactor event signing to work on dicts
This commit is contained in:
Erik Johnston 2019-01-29 09:52:54 +00:00 committed by GitHub
commit 17709f8f9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 41 deletions

1
changelog.d/4493.misc Normal file
View file

@ -0,0 +1 @@
Add infrastructure to support different event formats

View file

@ -23,14 +23,14 @@ from signedjson.sign import sign_json
from unpaddedbase64 import decode_base64, encode_base64 from unpaddedbase64 import decode_base64, encode_base64
from synapse.api.errors import Codes, SynapseError 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__) logger = logging.getLogger(__name__)
def check_event_content_hash(event, hash_algorithm=hashlib.sha256): def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
"""Check whether the hash for this PDU matches the contents""" """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)) logger.debug("Expecting hash: %s", encode_base64(expected_hash))
# some malformed events lack a 'hashes'. Protect against it being missing # 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 return message_hash_bytes == expected_hash
def compute_content_hash(event, hash_algorithm): def compute_content_hash(event_dict, hash_algorithm):
event_json = event.get_pdu_json() """Compute the content hash of an event, which is the hash of the
event_json.pop("age_ts", None) unredacted event.
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)
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) hashed = hash_algorithm(event_json_bytes)
return (hashed.name, hashed.digest()) return (hashed.name, hashed.digest())
def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256): 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) tmp_event = prune_event(event)
event_json = tmp_event.get_pdu_json() event_dict = tmp_event.get_pdu_json()
event_json.pop("signatures", None) event_dict.pop("signatures", None)
event_json.pop("age_ts", None) event_dict.pop("age_ts", None)
event_json.pop("unsigned", None) event_dict.pop("unsigned", None)
event_json_bytes = encode_canonical_json(event_json) event_json_bytes = encode_canonical_json(event_dict)
hashed = hash_algorithm(event_json_bytes) hashed = hash_algorithm(event_json_bytes)
return (hashed.name, hashed.digest()) return (hashed.name, hashed.digest())
def compute_event_signature(event, signature_name, signing_key): def compute_event_signature(event_dict, signature_name, signing_key):
tmp_event = prune_event(event) """Compute the signature of the event for the given name and key.
redact_json = tmp_event.get_pdu_json()
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("age_ts", None)
redact_json.pop("unsigned", None) redact_json.pop("unsigned", None)
logger.debug("Signing event: %s", encode_canonical_json(redact_json)) 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, def add_hashes_and_signatures(event, signature_name, signing_key,
hash_algorithm=hashlib.sha256): hash_algorithm=hashlib.sha256):
# if hasattr(event, "old_state_events"): """Add content hash and sign the event
# 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())
# }
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"): if not hasattr(event, "hashes"):
event.hashes = {} event.hashes = {}
event.hashes[name] = encode_base64(digest) event.hashes[name] = encode_base64(digest)
event.signatures = compute_event_signature( event.signatures = compute_event_signature(
event, event.get_pdu_json(),
signature_name=signature_name, signature_name=signature_name,
signing_key=signing_key, signing_key=signing_key,
) )

View file

@ -38,8 +38,31 @@ def prune_event(event):
This is used when we "redact" an event. We want to remove all fields that 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 the user has specified, but we do want to keep necessary information like
type, state_key etc. 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 = [ allowed_keys = [
"event_id", "event_id",
@ -59,13 +82,13 @@ def prune_event(event):
"membership", "membership",
] ]
event_dict = event.get_dict() event_type = event_dict["type"]
new_content = {} new_content = {}
def add_fields(*fields): def add_fields(*fields):
for field in fields: for field in fields:
if field in event.content: if field in event_dict["content"]:
new_content[field] = event_dict["content"][field] new_content[field] = event_dict["content"][field]
if event_type == EventTypes.Member: if event_type == EventTypes.Member:
@ -98,17 +121,17 @@ def prune_event(event):
allowed_fields["content"] = new_content allowed_fields["content"] = new_content
allowed_fields["unsigned"] = {} unsigned = {}
allowed_fields["unsigned"] = unsigned
if "age_ts" in event.unsigned: event_unsigned = event_dict.get("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"]
return type(event)( if "age_ts" in event_unsigned:
allowed_fields, unsigned["age_ts"] = event_unsigned["age_ts"]
internal_metadata_dict=event.internal_metadata.get_dict() if "replaces_state" in event_unsigned:
) unsigned["replaces_state"] = event_unsigned["replaces_state"]
return allowed_fields
def _copy_field(src, dst, field): def _copy_field(src, dst, field):