From 89be389fbbe64251f3b8e4d77ef68a95b21123dd Mon Sep 17 00:00:00 2001 From: Stan Janssen Date: Thu, 10 Sep 2020 19:36:13 +0200 Subject: [PATCH] Add support for adding Signature Properties to a detached signature This commit adds support for adding your own Signature Properties when you are using the detached method. This use case is relevant in, for example, the OpenADR specification, which requires a ReplayProtect element to be included in the signature. --- signxml/__init__.py | 32 ++++++++++++++++++++++++++++++-- test/test.py | 17 +++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/signxml/__init__.py b/signxml/__init__.py index 6d343e7e..0b11a3d4 100644 --- a/signxml/__init__.py +++ b/signxml/__init__.py @@ -285,7 +285,7 @@ def __init__(self, method=methods.enveloped, signature_algorithm="rsa-sha256", d def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, key_name=None, key_info=None, id_attribute=None, always_add_key_value=False, payload_inclusive_ns_prefixes=frozenset(), - signature_inclusive_ns_prefixes=frozenset()): + signature_inclusive_ns_prefixes=frozenset(), signature_properties=None): """ Sign the data and return the root element of the resulting XML tree. @@ -339,7 +339,11 @@ def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, k :param signature_inclusive_ns_prefixes: Provide a list of XML namespace prefixes whose declarations should be preserved when canonicalizing the signature itself (**InclusiveNamespaces PrefixList**). - :type inclusive_ns_prefixes: string + :type signature_inclusive_ns_prefixes: string + :param signature_properties: + One or more Elements that are to be included in the SignatureProperies section when using the detached + method. + :type signature_properties: :py:class:`lxml.etree.Element` or list of :py:class:`lxml.etree.Element`s :returns: A :py:class:`lxml.etree.Element` object representing the root of the XML tree containing the signature and @@ -364,6 +368,14 @@ def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, k reference_uris = reference_uri sig_root, doc_root, c14n_inputs, reference_uris = self._unpack(data, reference_uris) + + if self.method == methods.detached and signature_properties is not None: + reference_uris.append("#prop") + if signature_properties is not None and not isinstance(signature_properties, list): + signature_properties = [signature_properties] + signature_properties_el = self._build_signature_properties(signature_properties) + c14n_inputs.append(signature_properties_el) + signed_info_element, signature_value_element = self._build_sig(sig_root, reference_uris, c14n_inputs, sig_insp=signature_inclusive_ns_prefixes, payload_insp=payload_inclusive_ns_prefixes) @@ -428,6 +440,10 @@ def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, k if self.method == methods.enveloping: for c14n_input in c14n_inputs: doc_root.append(c14n_input) + + if self.method == methods.detached and signature_properties is not None: + sig_root.append(signature_properties_el) + return doc_root if self.method == methods.enveloped else sig_root def _get_c14n_inputs_from_reference_uris(self, doc_root, reference_uris): @@ -519,6 +535,18 @@ def _build_sig(self, sig_root, reference_uris, c14n_inputs, sig_insp, payload_in signature_value = SubElement(sig_root, ds_tag("SignatureValue")) return signed_info, signature_value + def _build_signature_properties(self, signature_properties): + obj = Element(ds_tag("Object"), attrib={'Id': 'prop'}, nsmap={'ds': "http://www.w3.org/2000/09/xmldsig#"}) + signature_properties_el = Element(ds_tag("SignatureProperties")) + for i, el in enumerate(signature_properties): + signature_property = Element(ds_tag("SignatureProperty"), + attrib={"Id": el.attrib.pop('Id', f"sigprop{i}"), + "Target": el.attrib.pop('Target', f"#sigproptarget{i}")}) + signature_property.append(el) + signature_properties_el.append(signature_property) + obj.append(signature_properties_el) + return obj + def _serialize_key_value(self, key, key_info_element): """ Add the public components of the key to the signature (see https://www.w3.org/TR/xmldsig-core2/#sec-KeyValue). diff --git a/test/test.py b/test/test.py index 4f8461dd..344a5206 100755 --- a/test/test.py +++ b/test/test.py @@ -453,5 +453,22 @@ def test_soap_request_with_inclusive_namespaces(self): expect_references=False, validate_schema=False) + def test_signature_properties_with_detached_method(self): + doc = etree.Element('Test', attrib={'Id': 'mytest'}) + sigprop = etree.Element('{http://somenamespace}MyCustomProperty') + sigprop.text = 'Some Text' + with open(os.path.join(os.path.dirname(__file__), "example.key"), "rb") as file: + key = file.read() + with open(os.path.join(os.path.dirname(__file__), "example.pem"), "rb") as file: + cert = file.read() + signature = XMLSigner(method=methods.detached).sign(doc, + cert=cert, + key=key, + reference_uri="#mytest", + signature_properties=sigprop) + fulldoc = b'' + etree.tostring(signature) + etree.tostring(doc) + b'' + XMLVerifier().verify(etree.fromstring(fulldoc), x509_cert=cert, expect_references=2) + + if __name__ == '__main__': unittest.main()