Skip to content

WIP: xades (ETSI) implementation #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
12 changes: 6 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
description='Python XML Signature library',
long_description=open('README.rst').read(),
install_requires=[
'lxml >= 4.2.1, < 5',
'defusedxml >= 0.5.0, < 1',
'lxml >= 4.4.1, < 5',
'defusedxml >= 0.6.0, < 1',
'eight >= 0.4.2, < 1',
'cryptography >= 2.1.4, < 3',
'asn1crypto >= 0.24.0',
'pyOpenSSL >= 17.5.0, < 19',
'certifi >= 2018.1.18'
'cryptography >= 2.8, < 3',
'asn1crypto >= 1.2.0',
'pyOpenSSL >= 18.0.0, < 19',
'certifi >= 2019.9.11'
],
extras_require={
':python_version == "2.7"': [
Expand Down
114 changes: 100 additions & 14 deletions signxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from base64 import b64encode, b64decode
from enum import Enum
import re

from eight import str, bytes
from lxml import etree
Expand Down Expand Up @@ -279,6 +280,10 @@ def __init__(self, method=methods.enveloped, signature_algorithm="rsa-sha256", d
self.c14n_alg = c14n_algorithm
self.namespaces = dict(ds=namespaces.ds)
self._parser = None
"""
List of built references to incorporate to the element ds:SignedInfo
"""
self.refs = []

def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, key_name=None, key_info=None,
id_attribute=None):
Expand Down Expand Up @@ -308,7 +313,7 @@ def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, k
Custom reference URI or list of reference URIs to incorporate into the signature. When ``method`` is set to
``detached`` or ``enveloped``, reference URIs are set to this value and only the referenced elements are
signed.
:type reference_uri: string or list
:type reference_uri: string or list or XML ElementTree Element
:param key_name: Add a KeyName element in the KeyInfo element that may be used by the signer to communicate a
key identifier to the recipient. Typically, KeyName contains an identifier related to the key pair used to
sign the message.
Expand Down Expand Up @@ -343,7 +348,8 @@ 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)
signed_info_element, signature_value_element = self._build_sig(sig_root, reference_uris, c14n_inputs)
doc_root, sig_root, reference_uris, c14n_inputs = self._pre_build_sig(doc_root, sig_root, reference_uris, c14n_inputs)
signed_info_element, signature_value_element = self._build_sig(doc_root, sig_root, reference_uris, c14n_inputs)

if key is None:
raise InvalidInput('Parameter "key" is required')
Expand Down Expand Up @@ -400,6 +406,10 @@ def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, k
x509_certificate.text = strip_pem_header(dump_certificate(FILETYPE_PEM, cert))
else:
sig_root.append(key_info)

#ensure the right order of elements
doc_root, sig_root, c14n_inputs = self._sort_elements(
doc_root, sig_root, c14n_inputs)
else:
raise NotImplementedError()

Expand Down Expand Up @@ -467,7 +477,62 @@ def _unpack(self, data, reference_uris):
reference_uris = ["#object"]
return sig_root, doc_root, c14n_inputs, reference_uris

def _build_sig(self, sig_root, reference_uris, c14n_inputs):
def _pre_build_sig(self, doc_root, sig_root, reference_uris, c14n_inputs):
"""
To overwrite

By default, this method add self.refs to reference_uris, elements
ready to incorporate into sig_root without changes

Use cases:
- Set the attribute 'Id' of the element 'Signature' when the
in the _unpack function the attribute is deleted, this will override
the digest value of built references
- Add the refs and c14_inputs to preserve the validation
exmple:

query = doc_root.xpath(
f"//{xades_ns}:QualifyingProperties", namespaces=namespaces
)
if len(query) > 1:
raise NotImplementedError("Multiple QualifyingProperties")
sig_id = query[0].attrib["Target"]
if sig_id.startswith("#"):
sig_id = sig_id.replace("#", "")
sig_root.set("Id", sig_id)
for ref in self.refs:
reference_uris.append(ref)
uri = ref
if etree.iselement(ref):
uri = uri.attrib["URI"]
c14n_inputs.append(
self.get_root(self._resolve_reference(doc_root, {"URI": uri}))
)
"""
return doc_root, sig_root, reference_uris, c14n_inputs


def _build_sig(self, doc_root, sig_root, reference_uris, c14n_inputs):
"""
:param reference_uris: the references to include o buld.
:cases:
- elements: if the reference_uri is an element, this will be added
without modifications
- dict: if the reference_uri is a dict, the reference element will be
created using the attributes in the dict
i.e: {"URI": URI, "Type": Type, "Id": ID}
- string: if refence_uri is a string, this will be interpreted as the
attribute `URI`
"""
def _check_brothers(element1, element2):
"""helper method to determiante if two elements have the same tag
:param element1: element
:type element1: etree._Element
:param element2: elemnt
:type element2: etree._Element
"""
return element1.tag == element2.tag

signed_info = SubElement(sig_root, ds_tag("SignedInfo"), nsmap=self.namespaces)
c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg)
if self.sign_alg.startswith("hmac-"):
Expand All @@ -476,20 +541,41 @@ def _build_sig(self, sig_root, reference_uris, c14n_inputs):
algorithm_id = self.known_signature_digest_tags[self.sign_alg]
signature_method = SubElement(signed_info, ds_tag("SignatureMethod"), Algorithm=algorithm_id)
for i, reference_uri in enumerate(reference_uris):
reference = SubElement(signed_info, ds_tag("Reference"), URI=reference_uri)
if self.method == methods.enveloped:
transforms = SubElement(reference, ds_tag("Transforms"))
SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature")
SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg)
digest_method = SubElement(reference, ds_tag("DigestMethod"),
Algorithm=self.known_digest_tags[self.digest_alg])
digest_value = SubElement(reference, ds_tag("DigestValue"))
payload_c14n = self._c14n(c14n_inputs[i], algorithm=self.c14n_alg)
digest = self._get_digest(payload_c14n, self._get_digest_method_by_tag(self.digest_alg))
digest_value.text = digest
if etree.iselement(reference_uri):
# adding built references
signed_info.append(reference_uri)
else:
if isinstance(reference_uri, dict):
reference = SubElement(signed_info, ds_tag("Reference"), **reference_uri)
else:
reference = SubElement(signed_info, ds_tag("Reference"), URI=reference_uri)

if self.method == methods.enveloped:
transforms = SubElement(reference, ds_tag("Transforms"))
if _check_brothers(doc_root, c14n_inputs[i]):
SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature")
SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg)
digest_method = SubElement(reference, ds_tag("DigestMethod"),
Algorithm=self.known_digest_tags[self.digest_alg])
digest_value = SubElement(reference, ds_tag("DigestValue"))
payload_c14n = self._c14n(c14n_inputs[i], algorithm=self.c14n_alg)
digest = self._get_digest(payload_c14n, self._get_digest_method_by_tag(self.digest_alg))
digest_value.text = digest
signature_value = SubElement(sig_root, ds_tag("SignatureValue"))
return signed_info, signature_value

def _sort_elements(self, doc_root, sig_root, c14n_inputs):
"""
This method is implemented to preserve the order in signature structure
case: If one or more elements are in the element "Signature" before
appending SignedInfo, SignatureValue and KeyInfo, the structure's order
will be corrupted once them have been added.
"""
ds_object = doc_root.xpath("//ds:Object", namespaces=namespaces)[0]
sig_root.insert(len(sig_root), ds_object)

return doc_root, sig_root, c14n_inputs

def _serialize_key_value(self, key, key_info_element):
key_value = SubElement(key_info_element, ds_tag("KeyValue"))
if self.sign_alg.startswith("rsa-"):
Expand Down
49 changes: 49 additions & 0 deletions signxml/xades/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#############
XAdES by ETSI
#############

`W3C Note: XML Advanced Electronic Signatures (XAdES) <https://www.w3.org/TR/XAdES>`_ is outdated and has aparently been superseeded by standard development by `ETSI <https://www.etsi.org/>`_.

This implementation is guided by the corresponding ETSI documents wich norm and inform that matter.

***********************************************
Electronic Signatures and Infrastructures (ESI)
***********************************************

The framework for standardization of signatures
===============================================
* `ETSI TR 119 000 V1.2.1 (2016-04) | Overview <https://www.etsi.org/deliver/etsi_tr/119000_119099/119000/01.02.01_60/tr_119000v010201p.pdf>`_

XAdES digital signatures
========================

* `ETSI EN 319 132-1 V1.1.1 (2016-04) | Part 1: Building blocks and XAdES baseline signatures <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.01.01_60/en_31913201v010101p.pdf>`_

* `ETSI EN 319 132-2 V1.1.1 (2016-04) | Part 2: Extended XAdES signatures <https://www.etsi.org/deliver/etsi_en/319100_319199/31913202/01.01.01_60/en_31913202v010101p.pdf>`_


Part 1 V1.1.1 (2016-04), in chapter "4.2 XML Namespaces", informs the application of the 201601 releases of the schema files:

* `XAdES01903v132-201601.xsd <schemas/v1.3.2/XAdES01903v132-201601.xsd>`_
* `XAdES01903v141-201601.xsd <schemas/v1.4.1/XAdES01903v141-201601.xsd>`_

Moreover, the following definitions in Part 1 V1.1.1 (2016-04), in chapter "3.1 Definitions", shall be highlighted:

* **legacy XAdES 101 903 signature:** digital signature generated according to ETSI TS 101 903 [i.2]
* **legacy XAdES baseline signature:** digital signature generated according to ETSI TS 103 171 [i.3]
* **legacy XAdES signature:** legacy XAdES 101 903 signature or legacy XAdES baseline signature

Testing Conformance and Interoperability
----------------------------------------

* `ETSI TR 119 134-1 V1.1.1 (2016-06) | Part 1: Overview <https://www.etsi.org/deliver/etsi_tr/119100_119199/11913401/01.01.01_60/tr_11913401v010101p.pdf>`_
* `ETSI TS 119 134-2 V1.1.1 (2016-06) | Part 2: Test suites for testing interoperability of XAdES baseline signatures <https://www.etsi.org/deliver/etsi_ts/119100_119199/11913402/01.01.01_60/ts_11913402v010101p.pdf>`_
* `ETSI TS 119 134-3 V1.1.1 (2016-06) | Part 3: Test suites for testing interoperability of extended XAdES signatures <https://www.etsi.org/deliver/etsi_ts/119100_119199/11913403/01.01.01_60/ts_11913403v010101p.pdf>`_
* `ETSI TS 119 134-4 V1.1.1 (2016-06) | Part 4: Testing Conformance of XAdES baseline signatures <https://www.etsi.org/deliver/etsi_ts/119100_119199/11913404/01.01.01_60/ts_11913404v010101p.pdf>`_
* `ETSI TS 119 134-5 V2.1.1 (2016-06) | Part 5: Testing Conformance of extended XAdES signatures <https://www.etsi.org/deliver/etsi_ts/119100_119199/11913405/02.01.01_60/ts_11913405v020101p.pdf>`_

Conformance Checker Tools
^^^^^^^^^^^^^^^^^^^^^^^^^

* `XAdES baseline and XAdES extended signatures Conformance Testing Tools Documentation <https://signatures-conformance-checker.etsi.org/pub/XAdESConformanceTestChecker_v1.0.pdf>`_
* `Free Online Tool <https://signatures-conformance-checker.etsi.org/pub/index.shtml>`_
Loading