event_signing.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import hashlib
  16. import logging
  17. from canonicaljson import encode_canonical_json
  18. from signedjson.sign import sign_json
  19. from unpaddedbase64 import decode_base64, encode_base64
  20. from synapse.api.errors import Codes, SynapseError
  21. from synapse.events.utils import prune_event, prune_event_dict
  22. logger = logging.getLogger(__name__)
  23. def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
  24. """Check whether the hash for this PDU matches the contents"""
  25. name, expected_hash = compute_content_hash(event.get_pdu_json(), hash_algorithm)
  26. logger.debug(
  27. "Verifying content hash on %s (expecting: %s)",
  28. event.event_id,
  29. encode_base64(expected_hash),
  30. )
  31. # some malformed events lack a 'hashes'. Protect against it being missing
  32. # or a weird type by basically treating it the same as an unhashed event.
  33. hashes = event.get("hashes")
  34. if not isinstance(hashes, dict):
  35. raise SynapseError(400, "Malformed 'hashes'", Codes.UNAUTHORIZED)
  36. if name not in hashes:
  37. raise SynapseError(
  38. 400,
  39. "Algorithm %s not in hashes %s" % (name, list(hashes)),
  40. Codes.UNAUTHORIZED,
  41. )
  42. message_hash_base64 = hashes[name]
  43. try:
  44. message_hash_bytes = decode_base64(message_hash_base64)
  45. except Exception:
  46. raise SynapseError(
  47. 400, "Invalid base64: %s" % (message_hash_base64,), Codes.UNAUTHORIZED
  48. )
  49. return message_hash_bytes == expected_hash
  50. def compute_content_hash(event_dict, hash_algorithm):
  51. """Compute the content hash of an event, which is the hash of the
  52. unredacted event.
  53. Args:
  54. event_dict (dict): The unredacted event as a dict
  55. hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
  56. to hash the event
  57. Returns:
  58. tuple[str, bytes]: A tuple of the name of hash and the hash as raw
  59. bytes.
  60. """
  61. event_dict = dict(event_dict)
  62. event_dict.pop("age_ts", None)
  63. event_dict.pop("unsigned", None)
  64. event_dict.pop("signatures", None)
  65. event_dict.pop("hashes", None)
  66. event_dict.pop("outlier", None)
  67. event_dict.pop("destinations", None)
  68. event_json_bytes = encode_canonical_json(event_dict)
  69. hashed = hash_algorithm(event_json_bytes)
  70. return hashed.name, hashed.digest()
  71. def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256):
  72. """Computes the event reference hash. This is the hash of the redacted
  73. event.
  74. Args:
  75. event (FrozenEvent)
  76. hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
  77. to hash the event
  78. Returns:
  79. tuple[str, bytes]: A tuple of the name of hash and the hash as raw
  80. bytes.
  81. """
  82. tmp_event = prune_event(event)
  83. event_dict = tmp_event.get_pdu_json()
  84. event_dict.pop("signatures", None)
  85. event_dict.pop("age_ts", None)
  86. event_dict.pop("unsigned", None)
  87. event_json_bytes = encode_canonical_json(event_dict)
  88. hashed = hash_algorithm(event_json_bytes)
  89. return hashed.name, hashed.digest()
  90. def compute_event_signature(event_dict, signature_name, signing_key):
  91. """Compute the signature of the event for the given name and key.
  92. Args:
  93. event_dict (dict): The event as a dict
  94. signature_name (str): The name of the entity signing the event
  95. (typically the server's hostname).
  96. signing_key (syutil.crypto.SigningKey): The key to sign with
  97. Returns:
  98. dict[str, dict[str, str]]: Returns a dictionary in the same format of
  99. an event's signatures field.
  100. """
  101. redact_json = prune_event_dict(event_dict)
  102. redact_json.pop("age_ts", None)
  103. redact_json.pop("unsigned", None)
  104. logger.debug("Signing event: %s", encode_canonical_json(redact_json))
  105. redact_json = sign_json(redact_json, signature_name, signing_key)
  106. logger.debug("Signed event: %s", encode_canonical_json(redact_json))
  107. return redact_json["signatures"]
  108. def add_hashes_and_signatures(
  109. event_dict, signature_name, signing_key, hash_algorithm=hashlib.sha256
  110. ):
  111. """Add content hash and sign the event
  112. Args:
  113. event_dict (dict): The event to add hashes to and sign
  114. signature_name (str): The name of the entity signing the event
  115. (typically the server's hostname).
  116. signing_key (syutil.crypto.SigningKey): The key to sign with
  117. hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
  118. to hash the event
  119. """
  120. name, digest = compute_content_hash(event_dict, hash_algorithm=hash_algorithm)
  121. event_dict.setdefault("hashes", {})[name] = encode_base64(digest)
  122. event_dict["signatures"] = compute_event_signature(
  123. event_dict, signature_name=signature_name, signing_key=signing_key
  124. )