Skip to content

Commit

Permalink
Merge pull request #265 from fyntex/release/v0.16.1
Browse files Browse the repository at this point in the history
Release v0.16.1
  • Loading branch information
ycouce-cdd committed Jan 13, 2022
2 parents 4513a79 + b455e74 commit 50f7c07
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.16.0
current_version = 0.16.1
commit = True
tag = True

Expand Down
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
History
-------

0.16.1 (2022-01-13)
+++++++++++++++++++++++

* (PR #264, 2022-01-13) rtc.xml_utils: Add method to verify signature of AEC XML document

0.16.0 (2021-12-24)
+++++++++++++++++++++++

Expand Down
2 changes: 1 addition & 1 deletion cl_sii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""


__version__ = '0.16.0'
__version__ = '0.16.1'
71 changes: 70 additions & 1 deletion cl_sii/rtc/xml_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from __future__ import annotations

from typing import Any, ClassVar
import logging
from typing import Any, ClassVar, Optional

import signxml

from cl_sii.dte.parse import DTE_XMLNS_MAP
from cl_sii.libs import crypto_utils, xml_utils

from .data_models_aec import AecXml


logger = logging.getLogger(__name__)


class AecXMLVerifier(signxml.XMLVerifier):
Expand All @@ -27,3 +34,65 @@ def _get_signature(self, root: Any) -> object:
return root
else:
return self._find(root, "Signature", anywhere=False)


###############################################################################
# functions
###############################################################################

def verify_aec_signature(
aec_xml_doc: xml_utils.XmlElement,
aec_xml: AecXml
) -> Optional[bool]:
"""
Verify signature of AEC XML document ``aec_xml_doc``.
:param aec_xml_doc: An AEC XML document, as returned by ``xml_utils.parse_untrusted_xml()``.
:param aec_xml: An instance of ``data_models_aec.AecXml`` with the data in the "cesión"'s
AEC XML document parsed from `aec_xml_doc` by ``parse_aec.parse_aec_xml``.
:raises ValueError: If the attribute `signature_x509_cert_der` of the AecXml is None.
:raises Exception: on unrecoverable errors
"""
signature_verified: Optional[bool]
signature_x509_cert: Optional[crypto_utils.X509Cert]

if aec_xml.signature_x509_cert_der is None:
raise ValueError("Field 'signature_x509_cert_der' can not be None.")

try:
signature_x509_cert = crypto_utils.load_der_x509_cert(
aec_xml.signature_x509_cert_der,
)
except ValueError:
signature_verified = None
logger.debug(
"The X.509 certificate could not be loaded from AEC's digital "
"signature's DER-encoded X.509 certificate."
)
return signature_verified

try:
aec_xml_verifier = AecXMLVerifier()
xml_utils.verify_xml_signature(
aec_xml_doc,
trusted_x509_cert=signature_x509_cert,
xml_verifier=aec_xml_verifier,
xml_verifier_supports_multiple_signatures=True,
)
except xml_utils.XmlSignatureUnverified:
signature_verified = False
logger.debug("AEC's digital signature did not verify")
except xml_utils.XmlSignatureInvalid:
signature_verified = False
logger.debug("AEC's digital signature is invalid")
except Exception:
signature_verified = None
logger.exception(
"Unexpected error when trying to verify digital signature of XML document. "
"X509 certificate: %s",
signature_x509_cert,
)
else:
signature_verified = True

return signature_verified
65 changes: 64 additions & 1 deletion tests/test_rtc_xml_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

import dataclasses
import io
import unittest

from cl_sii.libs.crypto_utils import load_pem_x509_cert
from cl_sii.libs.xml_utils import parse_untrusted_xml, verify_xml_signature, write_xml_doc
from cl_sii.rtc.xml_utils import AecXMLVerifier
from cl_sii.rtc.parse_aec import parse_aec_xml
from cl_sii.rtc.xml_utils import AecXMLVerifier, verify_aec_signature

from .utils import read_test_file_bytes

Expand Down Expand Up @@ -59,3 +61,64 @@ def test_xml_utils_verify_xml_signature_ok_external_trusted_cert(self) -> None:
write_xml_doc(signature_xml, f)
signature_xml_bytes = f.getvalue()
self.assertEqual(signature_xml_bytes, self.with_valid_signature_signature_xml)


class FunctionVerifyAecSignatureTest(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
super().setUpClass()

cls.xml_doc_cert_pem_bytes = read_test_file_bytes(
'test_data/sii-crypto/AEC--76354771-K--33--170--SEQ-2-cert.pem',
)

cls.with_valid_signature = read_test_file_bytes(
'test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n.xml',
)
cls.with_valid_signature_signed_data = read_test_file_bytes(
'test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n-signed_data.xml',
)
cls.with_valid_signature_signed_xml = read_test_file_bytes(
'test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n-signed_xml.xml',
)
cls.with_valid_signature_signature_xml = read_test_file_bytes(
'test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n-signature_xml.xml', # noqa: E501
)

def test_ok_external_trusted_cert(self) -> None:
aec_xml_doc = parse_untrusted_xml(self.with_valid_signature)
aec_xml = parse_aec_xml(aec_xml_doc)

is_signature_verified = verify_aec_signature(aec_xml_doc=aec_xml_doc, aec_xml=aec_xml)

self.assertTrue(is_signature_verified)

def test_ok_for_bad_certificate_value(self) -> None:
aec_xml_doc = parse_untrusted_xml(self.with_valid_signature)
aec_xml_obj = parse_aec_xml(aec_xml_doc)

aec_xml = dataclasses.replace(
aec_xml_obj,
signature_x509_cert_der=b'hello',
)

is_signature_verified = verify_aec_signature(aec_xml_doc=aec_xml_doc, aec_xml=aec_xml)

self.assertIsNone(is_signature_verified)

def test_fail_for_missing_certificate_value(self) -> None:
aec_xml_doc = parse_untrusted_xml(self.with_valid_signature)
aec_xml_obj = parse_aec_xml(aec_xml_doc)

aec_xml = dataclasses.replace(
aec_xml_obj,
signature_value=None,
signature_x509_cert_der=None,
)

with self.assertRaises(ValueError) as assert_raises_cm:
verify_aec_signature(aec_xml_doc=aec_xml_doc, aec_xml=aec_xml)

expected_error = "Field 'signature_x509_cert_der' can not be None."
self.assertEqual(assert_raises_cm.exception.args, (expected_error,))

0 comments on commit 50f7c07

Please sign in to comment.