verifier.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 New Vector 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 logging
  16. import time
  17. from twisted.internet import defer
  18. from twisted.names.error import DNSNameError
  19. import twisted.names.client
  20. import twisted.names.dns
  21. from unpaddedbase64 import decode_base64
  22. import signedjson.sign
  23. import signedjson.key
  24. from signedjson.sign import SignatureVerifyException
  25. from sydent.http.httpclient import FederationHttpClient
  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 Verifier(object):
  33. """
  34. Verifies signed json blobs from Matrix Homeservers by finding the
  35. homeserver's address, contacting it, requesting its keys and
  36. verifying that the signature on the json blob matches.
  37. """
  38. def __init__(self, sydent):
  39. self.sydent = sydent
  40. # Cache of server keys. These are cached until the 'valid_until_ts' time
  41. # in the result.
  42. self.cache = {
  43. # server_name: <result from keys query>,
  44. }
  45. @defer.inlineCallbacks
  46. def _getKeysForServer(self, server_name):
  47. """Get the signing key data from a home server.
  48. """
  49. if server_name in self.cache:
  50. cached = self.cache[server_name]
  51. now = int(time.time() * 1000)
  52. if cached['valid_until_ts'] > now:
  53. defer.returnValue(self.cache[server_name]['verify_keys'])
  54. client = FederationHttpClient(self.sydent)
  55. result = yield client.get_json("https://%s/_matrix/key/v2/server/" % server_name)
  56. if 'verify_keys' not in result:
  57. raise SignatureVerifyException("No key found in response")
  58. if 'valid_until_ts' in result:
  59. # Don't cache anything without a valid_until_ts or we wouldn't
  60. # know when to expire it.
  61. logger.info("Got keys for %s: caching until %s", server_name, result['valid_until_ts'])
  62. self.cache[server_name] = result
  63. defer.returnValue(result['verify_keys'])
  64. @defer.inlineCallbacks
  65. def verifyServerSignedJson(self, signed_json, acceptable_server_names=None):
  66. """Given a signed json object, try to verify any one
  67. of the signatures on it
  68. XXX: This contains a fairly noddy version of the home server
  69. SRV lookup and signature verification. It only looks at
  70. the first SRV result. It does no caching (just fetches the
  71. signature each time and does not contact any other servers
  72. to do perspectives checks.
  73. :param acceptable_server_names: If provided and not None,
  74. only signatures from servers in this list will be accepted.
  75. :type acceptable_server_names: list of strings
  76. :return a tuple of the server name and key name that was
  77. successfully verified. If the json cannot be verified,
  78. raises SignatureVerifyException.
  79. """
  80. if 'signatures' not in signed_json:
  81. raise SignatureVerifyException("Signature missing")
  82. for server_name, sigs in signed_json['signatures'].items():
  83. if acceptable_server_names is not None:
  84. if server_name not in acceptable_server_names:
  85. continue
  86. server_keys = yield self._getKeysForServer(server_name)
  87. for key_name, sig in sigs.items():
  88. if key_name in server_keys:
  89. if 'key' not in server_keys[key_name]:
  90. logger.warn("Ignoring key %s with no 'key'")
  91. continue
  92. key_bytes = decode_base64(server_keys[key_name]['key'])
  93. verify_key = signedjson.key.decode_verify_key_bytes(key_name, key_bytes)
  94. logger.info("verifying sig from key %r", key_name)
  95. signedjson.sign.verify_signed_json(signed_json, server_name, verify_key)
  96. logger.info("Verified signature with key %s from %s", key_name, server_name)
  97. defer.returnValue((server_name, key_name))
  98. logger.warn(
  99. "No matching key found for signature block %r in server keys %r",
  100. signed_json['signatures'], server_keys,
  101. )
  102. logger.warn(
  103. "Unable to verify any signatures from block %r. Acceptable server names: %r",
  104. signed_json['signatures'], acceptable_server_names,
  105. )
  106. raise SignatureVerifyException("No matching signature found")
  107. @defer.inlineCallbacks
  108. def authenticate_request(self, request, content):
  109. """Authenticates a Matrix federation request based on the X-Matrix header
  110. XXX: Copied largely from synapse
  111. :param request: The request object to authenticate
  112. :param content: The content of the request, if any
  113. :type content: bytes or None
  114. :returns: The origin of the server whose signature was validated
  115. """
  116. json_request = {
  117. "method": request.method,
  118. "uri": request.uri,
  119. "destination_is": self.sydent.server_name,
  120. "signatures": {},
  121. }
  122. if content is not None:
  123. json_request["content"] = content
  124. origin = None
  125. def parse_auth_header(header_str):
  126. try:
  127. params = auth.split(" ")[1].split(",")
  128. param_dict = dict(kv.split("=") for kv in params)
  129. def strip_quotes(value):
  130. if value.startswith("\""):
  131. return value[1:-1]
  132. else:
  133. return value
  134. origin = strip_quotes(param_dict["origin"])
  135. key = strip_quotes(param_dict["key"])
  136. sig = strip_quotes(param_dict["sig"])
  137. return (origin, key, sig)
  138. except Exception:
  139. raise SignatureVerifyException("Malformed Authorization header")
  140. auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
  141. if not auth_headers:
  142. raise NoAuthenticationError("Missing Authorization headers")
  143. for auth in auth_headers:
  144. if auth.startswith("X-Matrix"):
  145. (origin, key, sig) = parse_auth_header(auth)
  146. json_request["origin"] = origin
  147. json_request["signatures"].setdefault(origin, {})[key] = sig
  148. if not json_request["signatures"]:
  149. raise NoAuthenticationError("Missing X-Matrix Authorization header")
  150. yield self.verifyServerSignedJson(json_request, [origin])
  151. logger.info("Verified request from HS %s", origin)
  152. defer.returnValue(origin)