verifier.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # Copyright 2018 New Vector Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. import time
  16. from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
  17. import signedjson.key # type: ignore
  18. import signedjson.sign # type: ignore
  19. from signedjson.sign import SignatureVerifyException
  20. from twisted.web.server import Request
  21. from unpaddedbase64 import decode_base64
  22. from sydent.http.httpclient import FederationHttpClient
  23. from sydent.util.stringutils import is_valid_matrix_server_name
  24. if TYPE_CHECKING:
  25. from sydent.sydent import Sydent
  26. logger = logging.getLogger(__name__)
  27. class NoAuthenticationError(Exception):
  28. """
  29. Raised when no signature is provided that could be authenticated
  30. """
  31. pass
  32. class InvalidServerName(Exception):
  33. """
  34. Raised when the provided origin parameter is not a valid hostname (plus optional port).
  35. """
  36. pass
  37. class Verifier:
  38. """
  39. Verifies signed json blobs from Matrix Homeservers by finding the
  40. homeserver's address, contacting it, requesting its keys and
  41. verifying that the signature on the json blob matches.
  42. """
  43. def __init__(self, sydent: "Sydent") -> None:
  44. self.sydent = sydent
  45. # Cache of server keys. These are cached until the 'valid_until_ts' time
  46. # in the result.
  47. self.cache: Dict[str, Any] = {
  48. # server_name: <result from keys query>,
  49. }
  50. async def _getKeysForServer(self, server_name: str) -> Dict[str, Dict[str, str]]:
  51. """Get the signing key data from a homeserver.
  52. :param server_name: The name of the server to request the keys from.
  53. :return: The verification keys returned by the server.
  54. """
  55. if server_name in self.cache:
  56. cached = self.cache[server_name]
  57. now = int(time.time() * 1000)
  58. if cached["valid_until_ts"] > now:
  59. return self.cache[server_name]["verify_keys"]
  60. client = FederationHttpClient(self.sydent)
  61. result = await client.get_json(
  62. "matrix://%s/_matrix/key/v2/server/" % server_name, 1024 * 50
  63. )
  64. if "verify_keys" not in result:
  65. raise SignatureVerifyException("No key found in response")
  66. if "valid_until_ts" in result:
  67. if not isinstance(result["valid_until_ts"], int):
  68. raise SignatureVerifyException(
  69. "Invalid valid_until_ts received, must be an integer"
  70. )
  71. # Don't cache anything without a valid_until_ts or we wouldn't
  72. # know when to expire it.
  73. logger.info(
  74. "Got keys for %s: caching until %d",
  75. server_name,
  76. result["valid_until_ts"],
  77. )
  78. self.cache[server_name] = result
  79. return result["verify_keys"]
  80. async def verifyServerSignedJson(
  81. self,
  82. signed_json: Dict[str, Any],
  83. acceptable_server_names: Optional[List[str]] = None,
  84. ) -> Tuple[str, str]:
  85. """Given a signed json object, try to verify any one
  86. of the signatures on it
  87. XXX: This contains a fairly noddy version of the home server
  88. SRV lookup and signature verification. It does no caching (just
  89. fetches the signature each time and does not contact any other
  90. servers to do perspective checks).
  91. :param acceptable_server_names: If provided and not None,
  92. only signatures from servers in this list will be accepted.
  93. :return a tuple of the server name and key name that was
  94. successfully verified.
  95. :raise SignatureVerifyException: The json cannot be verified.
  96. """
  97. if "signatures" not in signed_json:
  98. raise SignatureVerifyException("Signature missing")
  99. for server_name, sigs in signed_json["signatures"].items():
  100. if acceptable_server_names is not None:
  101. if server_name not in acceptable_server_names:
  102. continue
  103. server_keys = await self._getKeysForServer(server_name)
  104. for key_name, sig in sigs.items():
  105. if key_name in server_keys:
  106. if "key" not in server_keys[key_name]:
  107. logger.warning("Ignoring key %s with no 'key'")
  108. continue
  109. key_bytes = decode_base64(server_keys[key_name]["key"])
  110. verify_key = signedjson.key.decode_verify_key_bytes(
  111. key_name, key_bytes
  112. )
  113. logger.info("verifying sig from key %r", key_name)
  114. signedjson.sign.verify_signed_json(
  115. signed_json, server_name, verify_key
  116. )
  117. logger.info(
  118. "Verified signature with key %s from %s", key_name, server_name
  119. )
  120. return (server_name, key_name)
  121. logger.warning(
  122. "No matching key found for signature block %r in server keys %r",
  123. signed_json["signatures"],
  124. server_keys,
  125. )
  126. logger.warning(
  127. "Unable to verify any signatures from block %r. Acceptable server names: %r",
  128. signed_json["signatures"],
  129. acceptable_server_names,
  130. )
  131. raise SignatureVerifyException("No matching signature found")
  132. async def authenticate_request(
  133. self, request: "Request", content: Optional[bytes]
  134. ) -> str:
  135. """Authenticates a Matrix federation request based on the X-Matrix header
  136. XXX: Copied largely from synapse
  137. :param request: The request object to authenticate
  138. :param content: The content of the request, if any
  139. :return: The origin of the server whose signature was validated
  140. """
  141. json_request = {
  142. "method": request.method,
  143. "uri": request.uri,
  144. "destination_is": self.sydent.config.general.server_name,
  145. "signatures": {},
  146. }
  147. if content is not None:
  148. json_request["content"] = content
  149. origin = None
  150. def parse_auth_header(header_str: str) -> Tuple[str, str, str]:
  151. """
  152. Extracts a server name, signing key and payload signature from an
  153. authentication header.
  154. :param header_str: The content of the header
  155. :return: The server name, the signing key, and the payload signature.
  156. """
  157. try:
  158. params = header_str.split(" ")[1].split(",")
  159. param_dict = dict(kv.split("=") for kv in params)
  160. def strip_quotes(value):
  161. if value.startswith('"'):
  162. return value[1:-1]
  163. else:
  164. return value
  165. origin = strip_quotes(param_dict["origin"])
  166. key = strip_quotes(param_dict["key"])
  167. sig = strip_quotes(param_dict["sig"])
  168. return origin, key, sig
  169. except Exception:
  170. raise SignatureVerifyException("Malformed Authorization header")
  171. auth_headers = request.requestHeaders.getRawHeaders("Authorization")
  172. if not auth_headers:
  173. raise NoAuthenticationError("Missing Authorization headers")
  174. for auth in auth_headers:
  175. if auth.startswith("X-Matrix"):
  176. (origin, key, sig) = parse_auth_header(auth)
  177. json_request["origin"] = origin
  178. json_request["signatures"].setdefault(origin, {})[key] = sig
  179. if not json_request["signatures"]:
  180. raise NoAuthenticationError("Missing X-Matrix Authorization header")
  181. if not is_valid_matrix_server_name(json_request["origin"]):
  182. raise InvalidServerName(
  183. "X-Matrix header's origin parameter must be a valid Matrix server name"
  184. )
  185. await self.verifyServerSignedJson(json_request, [origin])
  186. logger.info("Verified request from HS %s", origin)
  187. return origin