messaging.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. # SPDX-License-Identifier: Apache-2.0
  2. # Copyright 2020 Contributors to OpenLEADR
  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. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. from lxml import etree
  13. import xmltodict
  14. from jinja2 import Environment, PackageLoader
  15. from signxml import XMLSigner, XMLVerifier, methods
  16. from uuid import uuid4
  17. from lxml.etree import Element
  18. from asyncio import iscoroutine
  19. from openleadr import errors
  20. from datetime import datetime, timezone, timedelta
  21. import os
  22. from openleadr import utils
  23. from .preflight import preflight_message
  24. import logging
  25. logger = logging.getLogger('openleadr')
  26. SIGNER = XMLSigner(method=methods.detached,
  27. c14n_algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315")
  28. SIGNER.namespaces['oadr'] = "http://openadr.org/oadr-2.0b/2012/07"
  29. VERIFIER = XMLVerifier()
  30. XML_SCHEMA_LOCATION = os.path.join(os.path.dirname(__file__), 'schema', 'oadr_20b.xsd')
  31. with open(XML_SCHEMA_LOCATION) as file:
  32. XML_SCHEMA = etree.XMLSchema(etree.parse(file))
  33. XML_PARSER = etree.XMLParser(schema=XML_SCHEMA)
  34. def parse_message(data):
  35. """
  36. Parse a message and distill its usable parts. Returns a message type and payload.
  37. :param data str: The XML string that is received
  38. Returns a message type (str) and a message payload (dict)
  39. """
  40. message_dict = xmltodict.parse(data, process_namespaces=True, namespaces=NAMESPACES)
  41. message_type, message_payload = message_dict['oadrPayload']['oadrSignedObject'].popitem()
  42. message_payload = utils.normalize_dict(message_payload)
  43. return message_type, message_payload
  44. def create_message(message_type, cert=None, key=None, passphrase=None, **message_payload):
  45. """
  46. Create and optionally sign an OpenADR message. Returns an XML string.
  47. """
  48. message_payload = preflight_message(message_type, message_payload)
  49. template = TEMPLATES.get_template(f'{message_type}.xml')
  50. signed_object = utils.flatten_xml(template.render(**message_payload))
  51. envelope = TEMPLATES.get_template('oadrPayload.xml')
  52. if cert and key:
  53. tree = etree.fromstring(signed_object)
  54. signature_tree = SIGNER.sign(tree,
  55. key=key,
  56. cert=cert,
  57. passphrase=utils.ensure_bytes(passphrase),
  58. reference_uri="#oadrSignedObject",
  59. signature_properties=_create_replay_protect())
  60. signature = etree.tostring(signature_tree).decode('utf-8')
  61. else:
  62. signature = None
  63. msg = envelope.render(template=f'{message_type}',
  64. signature=signature,
  65. signed_object=signed_object)
  66. return msg
  67. def validate_xml_schema(content):
  68. """
  69. Validates the XML tree against the schema. Return the XML tree.
  70. """
  71. if isinstance(content, str):
  72. content = content.encode('utf-8')
  73. tree = etree.fromstring(content, XML_PARSER)
  74. return tree
  75. def validate_xml_signature(xml_tree, cert_fingerprint=None):
  76. """
  77. Validate the XMLDSIG signature and the ReplayProtect element.
  78. """
  79. cert = utils.extract_pem_cert(xml_tree)
  80. if cert_fingerprint:
  81. fingerprint = utils.certificate_fingerprint(cert)
  82. if fingerprint != cert_fingerprint:
  83. raise errors.FingerprintMismatch("The certificate fingerprint was incorrect. "
  84. f"Expected: {cert_fingerprint};"
  85. f"Received: {fingerprint}")
  86. VERIFIER.verify(xml_tree, x509_cert=utils.ensure_bytes(cert), expect_references=2)
  87. _verify_replay_protect(xml_tree)
  88. async def authenticate_message(request, message_tree, message_payload, fingerprint_lookup):
  89. if request.secure and 'ven_id' in message_payload:
  90. print("Getting cert fingerprint from request")
  91. connection_fingerprint = utils.get_cert_fingerprint_from_request(request)
  92. print("Checking cert fingerprint")
  93. if connection_fingerprint is None:
  94. msg = ("Your request must use a client side SSL certificate, of which the "
  95. "fingerprint must match the fingerprint that you have given to this VTN")
  96. raise errors.NotRegisteredOrAuthorizedError(msg)
  97. try:
  98. ven_id = message_payload.get('ven_id')
  99. expected_fingerprint = fingerprint_lookup(ven_id)
  100. if iscoroutine(expected_fingerprint):
  101. expected_fingerprint = await expected_fingerprint
  102. except ValueError:
  103. msg = (f"Your venID {ven_id} is not known to this VTN. Make sure you use the venID "
  104. "that you receive from this VTN during the registration step")
  105. raise errors.NotRegisteredOrAuthorizedError(msg)
  106. if expected_fingerprint is None:
  107. msg = ("This VTN server does not know what your certificate fingerprint is. Please "
  108. "deliver your fingerprint to the VTN (outside of OpenADR). You used the "
  109. "following fingerprint to make this request:")
  110. raise errors.NotRegisteredOrAuthorizedError(msg)
  111. print("Checking connection fingerprint")
  112. if connection_fingerprint != expected_fingerprint:
  113. msg = (f"The fingerprint of your HTTPS certificate {connection_fingerprint} "
  114. f"does not match the expected fingerprint {expected_fingerprint}")
  115. raise errors.NotRegisteredOrAuthorizedError(msg)
  116. print("Checking message fingerprint")
  117. message_cert = utils.extract_pem_cert(message_tree)
  118. message_fingerprint = utils.certificate_fingerprint(message_cert)
  119. if message_fingerprint != expected_fingerprint:
  120. msg = (f"The fingerprint of the certificate used to sign the message "
  121. f"{message_fingerprint} did not match the fingerprint that this "
  122. f"VTN has for you {expected_fingerprint}. Make sure you use the correct "
  123. "certificate to sign your messages.")
  124. raise errors.NotRegisteredOrAuthorizedError(msg)
  125. print("Validating XML signature")
  126. try:
  127. validate_xml_signature(message_tree)
  128. except ValueError:
  129. msg = ("The message signature did not match the message contents. Please make sure "
  130. "you are using the correct XMLDSig algorithm and C14n canonicalization.")
  131. raise errors.NotRegisteredOrAuthorizedError(msg)
  132. def _create_replay_protect():
  133. dt_element = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}timestamp")
  134. dt_element.text = utils.datetimeformat(datetime.now(timezone.utc))
  135. nonce_element = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}nonce")
  136. nonce_element.text = uuid4().hex
  137. el = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}ReplayProtect",
  138. nsmap={'dsp': 'http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties'},
  139. attrib={'Id': 'myid', 'Target': '#mytarget'})
  140. el.append(dt_element)
  141. el.append(nonce_element)
  142. return el
  143. def _verify_replay_protect(xml_tree):
  144. try:
  145. ns = "{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}"
  146. timestamp = utils.parse_datetime(xml_tree.findtext(f".//{ns}timestamp"))
  147. nonce = xml_tree.findtext(f".//{ns}nonce")
  148. except Exception:
  149. raise ValueError("Missing or malformed ReplayProtect element in the message signature.")
  150. else:
  151. if timestamp < datetime.now(timezone.utc) - REPLAY_PROTECT_MAX_TIME_DELTA:
  152. raise ValueError("The message was signed too long ago.")
  153. elif (timestamp, nonce) in NONCE_CACHE:
  154. raise ValueError("This combination of timestamp and nonce was already used.")
  155. _update_nonce_cache(timestamp, nonce)
  156. def _update_nonce_cache(timestamp, nonce):
  157. for timestamp, nonce in list(NONCE_CACHE):
  158. if timestamp < datetime.now(timezone.utc) - REPLAY_PROTECT_MAX_TIME_DELTA:
  159. NONCE_CACHE.remove((timestamp, nonce))
  160. NONCE_CACHE.add((timestamp, nonce))
  161. # Replay protect settings
  162. REPLAY_PROTECT_MAX_TIME_DELTA = timedelta(seconds=5)
  163. NONCE_CACHE = set()
  164. # Settings for jinja2
  165. TEMPLATES = Environment(loader=PackageLoader('openleadr', 'templates'))
  166. TEMPLATES.filters['datetimeformat'] = utils.datetimeformat
  167. TEMPLATES.filters['timedeltaformat'] = utils.timedeltaformat
  168. TEMPLATES.filters['booleanformat'] = utils.booleanformat
  169. TEMPLATES.trim_blocks = True
  170. TEMPLATES.lstrip_blocks = True
  171. # Settings for xmltodict
  172. NAMESPACES = {
  173. 'http://docs.oasis-open.org/ns/energyinterop/201110': None,
  174. 'http://openadr.org/oadr-2.0b/2012/07': None,
  175. 'urn:ietf:params:xml:ns:icalendar-2.0': None,
  176. 'http://docs.oasis-open.org/ns/energyinterop/201110/payloads': None,
  177. 'http://docs.oasis-open.org/ns/emix/2011/06': None,
  178. 'urn:ietf:params:xml:ns:icalendar-2.0:stream': None,
  179. 'http://docs.oasis-open.org/ns/emix/2011/06/power': None,
  180. 'http://docs.oasis-open.org/ns/emix/2011/06/siscale': None,
  181. 'http://www.w3.org/2000/09/xmldsig#': None,
  182. 'http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties': None
  183. }