From 366b7e52088299d3471154261c0ad8d6ef6a3254 Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Mon, 6 May 2019 12:00:10 -0400 Subject: [PATCH 1/9] docs: Fix `bumpversion` command The command `bumpversion --new-version 'X.Y.Z'` doesn't work. It returns the error `the following arguments are required: part`. The `part` argument doesn't matter when a complete new version number is specified, but `bumpversion` still complains if that parameter is missing. This change fixes the error by adding a dummy `part` argument. Ref: - [Slack](https://fynpal-hq.slack.com/archives/CFGURBDBK/p1557152981014100?thread_ts=1556809374.013500&cid=CFGURBDBK) - [Slack](https://fynpal-hq.slack.com/archives/CFGURBDBK/p1557154032014300?thread_ts=1556809374.013500&cid=CFGURBDBK) - [Slack](https://fynpal-hq.slack.com/archives/CFGURBDBK/p1557154124014600?thread_ts=1556809374.013500&cid=CFGURBDBK) --- docs/project-maintenance.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project-maintenance.rst b/docs/project-maintenance.rst index 59fba582..258de200 100644 --- a/docs/project-maintenance.rst +++ b/docs/project-maintenance.rst @@ -46,7 +46,7 @@ Add new entry to changelog including the changes summary (remember to format as Either of the following alternatives:: bumpversion major|minor|patch - bumpversion --new-version 'X.Y.Z' + bumpversion --new-version 'X.Y.Z' part # 'part' is a dummy argument. Push commit ``abcd1234`` and tag ``vX.Y.Z`` automatically created by ``bumpversion``:: From 919ad43d90c311ae91d1940eaad5ce5fa816cd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 May 2019 17:22:25 -0400 Subject: [PATCH 2/9] libs.tz_utils: rename timezone constants Aliases created for compatibility. --- cl_sii/libs/tz_utils.py | 14 +++++++++----- cl_sii/rcv/parse.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cl_sii/libs/tz_utils.py b/cl_sii/libs/tz_utils.py index 8c6ec286..c86bbb1f 100644 --- a/cl_sii/libs/tz_utils.py +++ b/cl_sii/libs/tz_utils.py @@ -14,8 +14,12 @@ ] -UTC = pytz.UTC # type: PytzTimezone -TIMEZONE_CL_SANTIAGO = pytz.timezone('America/Santiago') # type: PytzTimezone +TZ_UTC = pytz.UTC # type: PytzTimezone +TZ_CL_SANTIAGO = pytz.timezone('America/Santiago') # type: PytzTimezone + +# TODO: remove +UTC = TZ_UTC +TIMEZONE_CL_SANTIAGO = TZ_CL_SANTIAGO def get_now_tz_aware() -> datetime: @@ -31,7 +35,7 @@ def get_now_tz_aware() -> datetime: # - `pytz.UTC.localize(datetime.utcnow())` # source: 'django.utils.timezone.now' @ Django 2.1.3 - return datetime.utcnow().replace(tzinfo=UTC) + return datetime.utcnow().replace(tzinfo=TZ_UTC) def convert_naive_dt_to_tz_aware(dt: datetime, tz: PytzTimezone) -> datetime: @@ -44,13 +48,13 @@ def convert_naive_dt_to_tz_aware(dt: datetime, tz: PytzTimezone) -> datetime: >>> dt_naive.isoformat() '2018-10-23T01:54:13' - >>> dt_tz_aware_1 = convert_naive_dt_to_tz_aware(dt_naive, UTC) + >>> dt_tz_aware_1 = convert_naive_dt_to_tz_aware(dt_naive, TZ_UTC) >>> dt_tz_aware_1 datetime.datetime(2018, 10, 23, 1, 54, 13, tzinfo=) >>> dt_tz_aware_1.isoformat() '2018-10-23T04:54:13+00:00' - >>> dt_tz_aware_2 = convert_naive_dt_to_tz_aware(dt_naive, TIMEZONE_CL_SANTIAGO) + >>> dt_tz_aware_2 = convert_naive_dt_to_tz_aware(dt_naive, TZ_CL_SANTIAGO) >>> dt_tz_aware_2 datetime.datetime(2018, 10, 23, 1, 54, 13, tzinfo=) diff --git a/cl_sii/rcv/parse.py b/cl_sii/rcv/parse.py index 0e5547d9..8f22786a 100644 --- a/cl_sii/rcv/parse.py +++ b/cl_sii/rcv/parse.py @@ -76,7 +76,7 @@ class _RcvCsvDialect(csv.Dialect): class RcvCsvRowSchema(marshmallow.Schema): EXPECTED_INPUT_FIELDS = tuple(_RCV_CSV_EXPECTED_FIELD_NAMES) + (_CSV_ROW_DICT_EXTRA_FIELDS_KEY, ) # type: ignore # noqa: E501 - FIELD_FECHA_RECEPCION_DATETIME_TZ = tz_utils.TIMEZONE_CL_SANTIAGO + FIELD_FECHA_RECEPCION_DATETIME_TZ = tz_utils.TZ_CL_SANTIAGO class Meta: strict = True From 796d76ed52c6f4a6e196279a6af408886688a2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 May 2019 17:42:02 -0400 Subject: [PATCH 3/9] libs.tz_utils: add aware/naive inspection helpers --- cl_sii/libs/tz_utils.py | 50 +++++++++++++++++++++++++++++++++++++ tests/test_libs_tz_utils.py | 19 +++++++++++--- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/cl_sii/libs/tz_utils.py b/cl_sii/libs/tz_utils.py index c86bbb1f..9d420f07 100644 --- a/cl_sii/libs/tz_utils.py +++ b/cl_sii/libs/tz_utils.py @@ -1,3 +1,15 @@ +""" +Timezone utils +============== + + +Naive and aware +--------------- + +These concept are defined in Python standard library module datetime +`docs `_. + +""" from datetime import datetime from typing import Union @@ -68,3 +80,41 @@ def convert_naive_dt_to_tz_aware(dt: datetime, tz: PytzTimezone) -> datetime: """ dt_tz_aware = tz.localize(dt) # type: datetime return dt_tz_aware + + +def dt_is_aware(value: datetime) -> bool: + """ + Return whether datetime ``value`` is "aware". + + >>> dt_naive = datetime(2018, 10, 23, 1, 54, 13) + >>> dt_is_aware(dt_naive) + False + >>> dt_is_aware(convert_naive_dt_to_tz_aware(dt_naive, TZ_UTC)) + True + >>> dt_is_aware(convert_naive_dt_to_tz_aware(dt_naive, TZ_CL_SANTIAGO)) + True + + """ + if not isinstance(value, datetime): + raise TypeError + # source: 'django.utils.timezone.is_aware' @ Django 2.1.7 + return value.utcoffset() is not None + + +def dt_is_naive(value: datetime) -> bool: + """ + Return whether datetime ``value`` is "naive". + + >>> dt_naive = datetime(2018, 10, 23, 1, 54, 13) + >>> dt_is_naive(dt_naive) + True + >>> dt_is_naive(convert_naive_dt_to_tz_aware(dt_naive, TZ_UTC)) + False + >>> dt_is_naive(convert_naive_dt_to_tz_aware(dt_naive, TZ_CL_SANTIAGO)) + False + + """ + if not isinstance(value, datetime): + raise TypeError + # source: 'django.utils.timezone.is_naive' @ Django 2.1.7 + return value.utcoffset() is None diff --git a/tests/test_libs_tz_utils.py b/tests/test_libs_tz_utils.py index 8d2bcb32..4fe2145b 100644 --- a/tests/test_libs_tz_utils.py +++ b/tests/test_libs_tz_utils.py @@ -1,16 +1,29 @@ import unittest -from cl_sii.libs.tz_utils import convert_naive_dt_to_tz_aware, get_now_tz_aware # noqa: F401 +from cl_sii.libs.tz_utils import ( # noqa: F401 + convert_naive_dt_to_tz_aware, dt_is_aware, dt_is_naive, get_now_tz_aware, + PytzTimezone, TZ_CL_SANTIAGO, TZ_UTC, +) class FunctionsTest(unittest.TestCase): def test_get_now_tz_aware(self) -> None: - # TODO: implement! + # TODO: implement for 'get_now_tz_aware' # Reuse doctests/examples in function docstring. pass def test_convert_naive_dt_to_tz_aware(self) -> None: - # TODO: implement! + # TODO: implement for 'convert_naive_dt_to_tz_aware' + # Reuse doctests/examples in function docstring. + pass + + def test_dt_is_aware(self) -> None: + # TODO: implement for 'dt_is_aware' + # Reuse doctests/examples in function docstring. + pass + + def test_dt_is_naive(self) -> None: + # TODO: implement for 'dt_is_naive' # Reuse doctests/examples in function docstring. pass From fd79fa3f5473d7493f786a17f647e469900f6e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 May 2019 20:14:00 -0400 Subject: [PATCH 4/9] test_data: convert PEM-encoded certs to DER-encoded $ openssl x509 \ -in 'my-cert.pem' \ -outform der \ -out 'my-cert.der' OpenSSL 1.0.2g (2016-03-01) --- .../crypto/wildcard-google-com-cert.der | Bin 0 -> 2065 bytes .../sii-crypto/DTE--76354771-K--33--170-cert.der | Bin 0 -> 1624 bytes .../DTE--76399752-9--33--25568-cert.der | Bin 0 -> 1539 bytes tests/test_data/sii-crypto/prueba-sii-cert.der | Bin 0 -> 1090 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_data/crypto/wildcard-google-com-cert.der create mode 100644 tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.der create mode 100644 tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.der create mode 100644 tests/test_data/sii-crypto/prueba-sii-cert.der diff --git a/tests/test_data/crypto/wildcard-google-com-cert.der b/tests/test_data/crypto/wildcard-google-com-cert.der new file mode 100644 index 0000000000000000000000000000000000000000..0adf2dc59f7a2c6bac16e7d3610cacec3d0048ce GIT binary patch literal 2065 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 P;mvm=*MAO(%PZdjQ7%#` literal 0 HcmV?d00001 From ee0d8039a319127df7ede09913f92533fa548426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 May 2019 21:19:52 -0400 Subject: [PATCH 5/9] libs.crypto_utils: add functions To load an X.509 certificate from DER-encoded certificate data, and convert between DER-encoded and PEM-encoded certificate data. New functions: - `load_der_x509_cert` - `x509_cert_der_to_pem` - `x509_cert_pem_to_der` --- cl_sii/libs/crypto_utils.py | 106 +++++++++++++++++++++++- tests/test_libs_crypto_utils.py | 140 +++++++++++++++++++++++++++++--- 2 files changed, 233 insertions(+), 13 deletions(-) diff --git a/cl_sii/libs/crypto_utils.py b/cl_sii/libs/crypto_utils.py index 00452e1d..e9dee5b6 100644 --- a/cl_sii/libs/crypto_utils.py +++ b/cl_sii/libs/crypto_utils.py @@ -1,3 +1,43 @@ +""" +Crypto utils +============ + + +DER and PEM +----------- + +Best answer to the +`StackOverflow question `_ +"What are the differences between .pem, .cer and .der?". + +Best answer to the +`ServerFault question `_. +"What is a Pem file and how does it differ from other OpenSSL Generated Key File Formats?". + + +DER +-------- + +DER stands for "Distinguished Encoding Rules". + +> A way to encode ASN.1 syntax in binary. + +> The parent format of PEM. It's useful to think of it as a binary version +> of the base64-encoded PEM file. + +PEM +-------- + +PEM stands for "Privacy Enhanced Mail". + +> A failed method for secure email but the container format it used lives on, +> and is a base64 translation of the x509 ASN.1 keys. + +> In the case that it encodes a certificate it would simply contain the +> base64 encoding of the DER certificate [plus the header and footer]. + +""" +import base64 from typing import Union import cryptography.x509 @@ -6,10 +46,35 @@ from cryptography.x509 import Certificate as X509Cert from OpenSSL.crypto import X509 as _X509CertOpenSsl # noqa: F401 +from . import encoding_utils + + +def load_der_x509_cert(der_value: bytes) -> X509Cert: + """ + Load an X.509 certificate from DER-encoded certificate data. + + :raises TypeError: + :raises ValueError: + + """ + if not isinstance(der_value, bytes): + raise TypeError("Value must be bytes.") + + try: + x509_cert = cryptography.x509.load_der_x509_certificate( + data=der_value, + backend=_crypto_x509_backend) + except ValueError: + # e.g. + # "Unable to load certificate" + raise + + return x509_cert + def load_pem_x509_cert(pem_value: Union[str, bytes]) -> X509Cert: """ - Load an X.509 certificate from a PEM-formatted value. + Load an X.509 certificate from PEM-encoded certificate data. .. seealso:: https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file @@ -39,6 +104,45 @@ def load_pem_x509_cert(pem_value: Union[str, bytes]) -> X509Cert: return x509_cert +def x509_cert_der_to_pem(der_value: bytes) -> bytes: + """ + Convert an X.509 certificate DER-encoded data to PEM-encoded. + + .. warning:: + It does not validate that ``der_value`` corresponds to an X.509 cert. + + :raises TypeError: + + """ + if not isinstance(der_value, bytes): + raise TypeError("Value must be bytes.") + + pem_value = base64.standard_b64encode(der_value) + mod_pem_value = add_pem_cert_header_footer(pem_value) + + return mod_pem_value.strip() + + +def x509_cert_pem_to_der(pem_value: bytes) -> bytes: + """ + Convert an X.509 certificate PEM-encoded data to DER-encoded. + + .. warning:: + It does not validate that ``pem_value`` corresponds to an X.509 cert. + + :raises TypeError: + :raises ValueError: + + """ + if not isinstance(pem_value, bytes): + raise TypeError("Value must be bytes.") + + mod_pem_value = remove_pem_cert_header_footer(pem_value) + der_value = encoding_utils.decode_base64_strict(mod_pem_value) + + return der_value.strip() + + def add_pem_cert_header_footer(pem_cert: bytes) -> bytes: """ Add certificate PEM header and footer (if not already present). diff --git a/tests/test_libs_crypto_utils.py b/tests/test_libs_crypto_utils.py index 4fe317f7..304e7b84 100644 --- a/tests/test_libs_crypto_utils.py +++ b/tests/test_libs_crypto_utils.py @@ -6,7 +6,9 @@ from cryptography.x509 import oid from cl_sii.libs.crypto_utils import ( # noqa: F401 - X509Cert, add_pem_cert_header_footer, load_pem_x509_cert, remove_pem_cert_header_footer, + X509Cert, add_pem_cert_header_footer, load_der_x509_cert, load_pem_x509_cert, + remove_pem_cert_header_footer, + x509_cert_der_to_pem, x509_cert_pem_to_der, ) from . import utils @@ -40,11 +42,11 @@ def test_remove_pem_cert_header_footer(self) -> None: class LoadPemX509CertTest(unittest.TestCase): - def test_load_pem_x509_cert_ok(self) -> None: - cert_pem_bytes = utils.read_test_file_bytes( - 'test_data/crypto/wildcard-google-com-cert.pem') + def test_load_der_x509_cert_ok(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/crypto/wildcard-google-com-cert.der') - x509_cert = load_pem_x509_cert(cert_pem_bytes) + x509_cert = load_der_x509_cert(cert_der_bytes) self.assertIsInstance(x509_cert, X509Cert) @@ -217,11 +219,11 @@ def test_load_pem_x509_cert_ok(self) -> None: self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None) self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None) - def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None: - cert_pem_bytes = utils.read_test_file_bytes( - 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem') + def test_load_der_x509_cert_ok_cert_real_dte(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der') - x509_cert = load_pem_x509_cert(cert_pem_bytes) + x509_cert = load_der_x509_cert(cert_der_bytes) self.assertIsInstance(x509_cert, X509Cert) @@ -439,10 +441,11 @@ def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None: self.assertEqual(some_microsoft_ext.critical, False) self.assertTrue(isinstance(some_microsoft_ext.value.value, bytes)) - def test_load_pem_x509_cert_ok_prueba_sii(self) -> None: - cert_pem_bytes = utils.read_test_file_bytes('test_data/sii-crypto/prueba-sii-cert.pem') + def test_load_der_x509_cert_ok_prueba_sii(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/prueba-sii-cert.der') - x509_cert = load_pem_x509_cert(cert_pem_bytes) + x509_cert = load_der_x509_cert(cert_der_bytes) self.assertIsInstance(x509_cert, X509Cert) @@ -615,6 +618,54 @@ def test_load_pem_x509_cert_ok_prueba_sii(self) -> None: self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None) self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None) + def test_load_der_x509_cert_fail_type_error(self) -> None: + with self.assertRaises(TypeError) as cm: + load_der_x509_cert(1) + self.assertEqual(cm.exception.args, ("Value must be bytes.", )) + + def test_load_der_x509_cert_fail_value_error(self) -> None: + with self.assertRaises(ValueError) as cm: + load_der_x509_cert(b'hello') + self.assertEqual( + cm.exception.args, + ("Unable to load certificate", )) + + def test_load_pem_x509_cert_ok(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/crypto/wildcard-google-com-cert.der') + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/crypto/wildcard-google-com-cert.pem') + + x509_cert_from_der = load_der_x509_cert(cert_der_bytes) + x509_cert_from_pem = load_pem_x509_cert(cert_pem_bytes) + + self.assertIsInstance(x509_cert_from_pem, X509Cert) + self.assertEqual(x509_cert_from_der, x509_cert_from_pem) + + def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der') + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem') + + x509_cert_from_der = load_der_x509_cert(cert_der_bytes) + x509_cert_from_pem = load_pem_x509_cert(cert_pem_bytes) + + self.assertIsInstance(x509_cert_from_pem, X509Cert) + self.assertEqual(x509_cert_from_der, x509_cert_from_pem) + + def test_load_pem_x509_cert_ok_prueba_sii(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/prueba-sii-cert.der') + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/prueba-sii-cert.pem') + + x509_cert_from_der = load_der_x509_cert(cert_der_bytes) + x509_cert_from_pem = load_pem_x509_cert(cert_pem_bytes) + + self.assertIsInstance(x509_cert_from_pem, X509Cert) + self.assertEqual(x509_cert_from_der, x509_cert_from_pem) + def test_load_pem_x509_cert_ok_str_ascii(self) -> None: cert_pem_str_ascii = utils.read_test_file_str_ascii( 'test_data/crypto/wildcard-google-com-cert.pem') @@ -642,3 +693,68 @@ def test_load_pem_x509_cert_fail_value_error(self) -> None: ("Unable to load certificate. See " "https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file " "for more details.", )) + + def test_x509_cert_der_to_pem_pem_to_der_ok_1(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/crypto/wildcard-google-com-cert.der') + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/crypto/wildcard-google-com-cert.pem') + + # note: we test the function with a double call because the input PEM data + # may have different line lengths and different line separators. + self.assertEqual( + x509_cert_pem_to_der(x509_cert_der_to_pem(cert_der_bytes)), + x509_cert_pem_to_der(cert_pem_bytes)) + + def test_x509_cert_der_to_pem_pem_to_der_ok_2(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der') + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem') + + # note: we test the function with a double call because the input PEM data + # may have different line lengths and different line separators. + self.assertEqual( + x509_cert_pem_to_der(x509_cert_der_to_pem(cert_der_bytes)), + x509_cert_pem_to_der(cert_pem_bytes)) + + def test_x509_cert_der_to_pem_pem_to_der_ok_3(self) -> None: + cert_der_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/prueba-sii-cert.der') + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/prueba-sii-cert.pem') + + # note: we test the function with a double call because the input PEM data + # may have different line lengths and different line separators. + self.assertEqual( + x509_cert_pem_to_der(x509_cert_der_to_pem(cert_der_bytes)), + x509_cert_pem_to_der(cert_pem_bytes)) + + def test_x509_cert_der_to_pem_type_error(self) -> None: + with self.assertRaises(TypeError) as cm: + x509_cert_der_to_pem(1) + self.assertEqual(cm.exception.args, ("Value must be bytes.", )) + + def test_x509_cert_pem_to_der_type_error(self) -> None: + with self.assertRaises(TypeError) as cm: + x509_cert_pem_to_der(1) + self.assertEqual(cm.exception.args, ("Value must be bytes.", )) + + def test_x509_cert_pem_to_der_valuetype_error(self) -> None: + with self.assertRaises(ValueError) as cm: + x509_cert_pem_to_der(b'hello') + self.assertEqual( + cm.exception.args, + ( + "Input is not a valid base64 value.", + "Invalid base64-encoded string: number of data characters (5) cannot be 1 more " + "than a multiple of 4", + )) + + def test_add_pem_cert_header_footer(self) -> None: + # TODO: implement for 'add_pem_cert_header_footer' + pass + + def test_remove_pem_cert_header_footer(self) -> None: + # TODO: implement for 'remove_pem_cert_header_footer' + pass From 82513168b092756fa5ff8d4cddad0aa8b28b852b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 May 2019 17:48:05 -0400 Subject: [PATCH 6/9] dte.data_models: alter field `DteDataL2.firma_documento_dt_naive` Rename to `firma_documento_dt` and hold the timezone-aware value instead of "naive". --- cl_sii/dte/data_models.py | 27 +++++++++++++++++++++++---- cl_sii/dte/parse.py | 7 +++++-- tests/test_dte_data_models.py | 29 +++++++++++++++++++++++++++-- tests/test_dte_parse.py | 10 ++++++++-- 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 4591cda5..138ea16e 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -23,6 +23,7 @@ import cl_sii.contribuyente.constants import cl_sii.rut.constants from cl_sii.libs import encoding_utils +from cl_sii.libs import tz_utils from cl_sii.rut import Rut from . import constants @@ -95,6 +96,13 @@ def validate_non_empty_bytes(value: bytes) -> None: raise ValueError("Bytes value length (stripped) is 0.") +def validate_correct_tz(value: datetime, tz: tz_utils.PytzTimezone) -> None: + if not tz_utils.dt_is_aware(value): + raise ValueError("Value must be a timezone-aware datetime.", value) + if value.tzinfo.zone != tz.zone: # type: ignore + raise ValueError(f"Timezone of datetime value must be '{tz.zone!s}'.", value) + + @dataclasses.dataclass(frozen=True) class DteNaturalKey: @@ -318,6 +326,16 @@ class DteDataL2(DteDataL1): """ + ########################################################################### + # constants + ########################################################################### + + DATETIME_FIELDS_TZ = tz_utils.TZ_CL_SANTIAGO + + ########################################################################### + # fields + ########################################################################### + emisor_razon_social: str = dc_field() """ "Razón social" (legal name) of the "emisor" of the DTE. @@ -333,7 +351,7 @@ class DteDataL2(DteDataL1): "Fecha de vencimiento (pago)" of the DTE. """ - firma_documento_dt_naive: Optional[datetime] = dc_field(default=None) + firma_documento_dt: Optional[datetime] = dc_field(default=None) """ Datetime on which the "documento" was digitally signed. """ @@ -386,9 +404,10 @@ def __post_init__(self) -> None: if not isinstance(self.fecha_vencimiento_date, date): raise TypeError("Inappropriate type of 'fecha_vencimiento_date'.") - if self.firma_documento_dt_naive is not None: - if not isinstance(self.firma_documento_dt_naive, datetime): - raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.") + if self.firma_documento_dt is not None: + if not isinstance(self.firma_documento_dt, datetime): + raise TypeError("Inappropriate type of 'firma_documento_dt'.") + validate_correct_tz(self.firma_documento_dt, self.DATETIME_FIELDS_TZ) if self.signature_value is not None: if not isinstance(self.signature_value, bytes): diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index 5cd5831f..77b5cc79 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -24,6 +24,7 @@ from typing import Tuple from cl_sii.libs import encoding_utils +from cl_sii.libs import tz_utils from cl_sii.libs import xml_utils from cl_sii.libs.xml_utils import XmlElement, XmlElementTree from cl_sii.rut import Rut @@ -447,7 +448,9 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: monto_total_value = int(monto_total_em.text.strip()) - tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text) + tmst_firma_value = tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime.fromisoformat(tmst_firma_em.text), + tz=data_models.DteDataL2.DATETIME_FIELDS_TZ) signature_signature_value = encoding_utils.decode_base64_strict( signature_signature_value_em.text.strip()) @@ -464,7 +467,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: emisor_razon_social=emisor_razon_social_value, receptor_razon_social=receptor_razon_social_value, fecha_vencimiento_date=fecha_vencimiento_value, - firma_documento_dt_naive=tmst_firma_value, + firma_documento_dt=tmst_firma_value, signature_value=signature_signature_value, signature_x509_cert_pem=signature_key_info_x509_cert_pem, emisor_giro=emisor_giro_value, diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index d0d4acad..03fdcdc0 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -2,6 +2,7 @@ import unittest from datetime import date, datetime +from cl_sii.libs import tz_utils from cl_sii.rut import Rut # noqa: F401 from cl_sii.dte.constants import TipoDteEnum # noqa: F401 @@ -150,7 +151,9 @@ def setUp(self) -> None: emisor_razon_social='INGENIERIA ENACON SPA', receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=None, signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', @@ -175,7 +178,9 @@ def test_as_dict(self) -> None: emisor_razon_social='INGENIERIA ENACON SPA', receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=None, signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', @@ -197,3 +202,23 @@ def test_validate_dte_folio(self) -> None: def test_validate_dte_monto_total(self) -> None: # TODO: implement for 'validate_dte_monto_total' pass + + def test_validate_clean_str(self) -> None: + # TODO: implement for 'validate_clean_str' + pass + + def test_validate_clean_bytes(self) -> None: + # TODO: implement for 'validate_clean_bytes' + pass + + def test_validate_non_empty_str(self) -> None: + # TODO: implement for 'validate_non_empty_str' + pass + + def test_validate_non_empty_bytes(self) -> None: + # TODO: implement for 'validate_non_empty_bytes' + pass + + def test_validate_correct_tz(self) -> None: + # TODO: implement for 'validate_correct_tz' + pass diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index 1944f17d..f18b4396 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -4,8 +4,10 @@ from datetime import date, datetime import cl_sii.dte.constants +from cl_sii.dte.data_models import DteDataL2 from cl_sii.libs import crypto_utils from cl_sii.libs import encoding_utils +from cl_sii.libs import tz_utils from cl_sii.libs import xml_utils from cl_sii.rut import Rut @@ -341,7 +343,9 @@ def test_parse_dte_xml_ok_1(self) -> None: emisor_razon_social='INGENIERIA ENACON SPA', receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 4, 1, 1, 36, 40), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=self._TEST_DTE_1_SIGNATURE_VALUE, signature_x509_cert_pem=self.dte_clean_xml_1_cert_pem_bytes, emisor_giro='Ingenieria y Construccion', @@ -365,7 +369,9 @@ def test_parse_dte_xml_ok_2(self) -> None: emisor_razon_social='COMERCIALIZADORA INNOVA MOBEL SPA', receptor_razon_social='EMPRESAS LA POLAR S.A.', fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 3, 28, 13, 59, 52), + firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( + dt=datetime(2019, 3, 28, 13, 59, 52), + tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=self._TEST_DTE_2_SIGNATURE_VALUE, signature_x509_cert_pem=self.dte_clean_xml_2_cert_pem_bytes, emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', From 99ca4da931f5c720e242543cb2e55ab59312ddae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 7 May 2019 22:18:24 -0400 Subject: [PATCH 7/9] dte.data_models: alter field `DteDataL2.signature_x509_cert_pem` Rename to `signature_x509_cert_der` and hold the X.509 certificate's DER-encoded data instead of PEM-encoded data. **Breaks backwards compatibility**. --- cl_sii/dte/data_models.py | 20 ++++++++++---------- cl_sii/dte/parse.py | 4 ++-- tests/test_dte_data_models.py | 20 ++++++++++++++++---- tests/test_dte_parse.py | 15 +++++++++++++-- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index 138ea16e..a9044fd6 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -22,7 +22,6 @@ import cl_sii.contribuyente.constants import cl_sii.rut.constants -from cl_sii.libs import encoding_utils from cl_sii.libs import tz_utils from cl_sii.rut import Rut @@ -361,11 +360,13 @@ class DteDataL2(DteDataL1): DTE's digital signature's value (raw bytes, without base64 encoding). """ - signature_x509_cert_pem: Optional[bytes] = dc_field(default=None) + signature_x509_cert_der: Optional[bytes] = dc_field(default=None) """ - DTE's digital signature's PEM-encoded X.509 cert. + DTE's digital signature's DER-encoded X.509 cert. - PEM-encoded implies base64-encoded. + .. seealso:: + Functions :func:`cl_sii.libs.crypto_utils.load_der_x509_cert` + and :func:`cl_sii.libs.crypto_utils.x509_cert_der_to_pem`. """ emisor_giro: Optional[str] = dc_field(default=None) @@ -415,12 +416,11 @@ def __post_init__(self) -> None: validate_clean_bytes(self.signature_value) validate_non_empty_bytes(self.signature_value) - if self.signature_x509_cert_pem is not None: - if not isinstance(self.signature_x509_cert_pem, bytes): - raise TypeError("Inappropriate type of 'signature_x509_cert_pem'.") - validate_clean_bytes(self.signature_x509_cert_pem) - validate_non_empty_bytes(self.signature_x509_cert_pem) - encoding_utils.validate_base64(self.signature_x509_cert_pem) + if self.signature_x509_cert_der is not None: + if not isinstance(self.signature_x509_cert_der, bytes): + raise TypeError("Inappropriate type of 'signature_x509_cert_der'.") + validate_clean_bytes(self.signature_x509_cert_der) + validate_non_empty_bytes(self.signature_x509_cert_der) if self.emisor_giro is not None: if not isinstance(self.emisor_giro, str): diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index 77b5cc79..d7a5f0ed 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -454,7 +454,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: signature_signature_value = encoding_utils.decode_base64_strict( signature_signature_value_em.text.strip()) - signature_key_info_x509_cert_pem = encoding_utils.clean_base64( + signature_key_info_x509_cert_der = encoding_utils.decode_base64_strict( signature_key_info_x509_cert_em.text.strip()) return data_models.DteDataL2( @@ -469,7 +469,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: fecha_vencimiento_date=fecha_vencimiento_value, firma_documento_dt=tmst_firma_value, signature_value=signature_signature_value, - signature_x509_cert_pem=signature_key_info_x509_cert_pem, + signature_x509_cert_der=signature_key_info_x509_cert_der, emisor_giro=emisor_giro_value, emisor_email=emisor_email_value, receptor_email=receptor_email_value, diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 03fdcdc0..d4e1d567 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -2,6 +2,7 @@ import unittest from datetime import date, datetime +from cl_sii.libs import encoding_utils from cl_sii.libs import tz_utils from cl_sii.rut import Rut # noqa: F401 @@ -11,6 +12,8 @@ validate_contribuyente_razon_social, validate_dte_folio, validate_dte_monto_total, ) +from .utils import read_test_file_bytes + class DteNaturalKeyTest(unittest.TestCase): @@ -138,6 +141,15 @@ def test_vendedor_rut_deudor_rut(self) -> None: class DteDataL2Test(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_1_xml_signature_value = encoding_utils.decode_base64_strict(read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt')) + cls.dte_1_xml_cert_der = read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der') + def setUp(self) -> None: super().setUp() @@ -154,8 +166,8 @@ def setUp(self) -> None: firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( dt=datetime(2019, 4, 1, 1, 36, 40), tz=DteDataL2.DATETIME_FIELDS_TZ), - signature_value=None, - signature_x509_cert_pem=None, + signature_value=self.dte_1_xml_signature_value, + signature_x509_cert_der=self.dte_1_xml_cert_der, emisor_giro='Ingenieria y Construccion', emisor_email='hello@example.com', receptor_email=None, @@ -181,8 +193,8 @@ def test_as_dict(self) -> None: firma_documento_dt=tz_utils.convert_naive_dt_to_tz_aware( dt=datetime(2019, 4, 1, 1, 36, 40), tz=DteDataL2.DATETIME_FIELDS_TZ), - signature_value=None, - signature_x509_cert_pem=None, + signature_value=self.dte_1_xml_signature_value, + signature_x509_cert_der=self.dte_1_xml_cert_der, emisor_giro='Ingenieria y Construccion', emisor_email='hello@example.com', receptor_email=None, diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index f18b4396..d8f4198e 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -295,9 +295,13 @@ def setUpClass(cls) -> None: cls.dte_clean_xml_1_cert_pem_bytes = encoding_utils.clean_base64( crypto_utils.remove_pem_cert_header_footer( read_test_file_bytes('test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem'))) + cls.dte_clean_xml_1_cert_der = read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.der') cls.dte_clean_xml_2_cert_pem_bytes = encoding_utils.clean_base64( crypto_utils.remove_pem_cert_header_footer( read_test_file_bytes('test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem'))) + cls.dte_clean_xml_2_cert_der = read_test_file_bytes( + 'test_data/sii-crypto/DTE--76399752-9--33--25568-cert.der') cls._TEST_DTE_1_SIGNATURE_VALUE = encoding_utils.decode_base64_strict( read_test_file_bytes( @@ -327,6 +331,13 @@ def test_data(self): b"\xe5]E\xed\x9c\xcb\xc2\x84\x15i\xd0tT]\x8b\x8a\x1f'\xe9\x0b:\x88\x05|\xa0b\xb2" b"\x19{\x1cW\x80\xe4\xa7*\xef\xf2\x1a") + self.assertEqual( + crypto_utils.x509_cert_pem_to_der(self.dte_clean_xml_1_cert_pem_bytes), + self.dte_clean_xml_1_cert_der) + self.assertEqual( + crypto_utils.x509_cert_pem_to_der(self.dte_clean_xml_2_cert_pem_bytes), + self.dte_clean_xml_2_cert_der) + def test_parse_dte_xml_ok_1(self) -> None: xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes) @@ -347,7 +358,7 @@ def test_parse_dte_xml_ok_1(self) -> None: dt=datetime(2019, 4, 1, 1, 36, 40), tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=self._TEST_DTE_1_SIGNATURE_VALUE, - signature_x509_cert_pem=self.dte_clean_xml_1_cert_pem_bytes, + signature_x509_cert_der=self.dte_clean_xml_1_cert_der, emisor_giro='Ingenieria y Construccion', emisor_email='ENACONLTDA@GMAIL.COM', receptor_email=None, @@ -373,7 +384,7 @@ def test_parse_dte_xml_ok_2(self) -> None: dt=datetime(2019, 3, 28, 13, 59, 52), tz=DteDataL2.DATETIME_FIELDS_TZ), signature_value=self._TEST_DTE_2_SIGNATURE_VALUE, - signature_x509_cert_pem=self.dte_clean_xml_2_cert_pem_bytes, + signature_x509_cert_der=self.dte_clean_xml_2_cert_der, emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', emisor_email='ANGEL.PEZO@APCASESORIAS.CL', receptor_email=None, From 7c2f5996ee083c0e20d78b4091f7465ea684c53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 May 2019 12:48:33 -0400 Subject: [PATCH 8/9] HISTORY: update for new version --- HISTORY.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index d0f42b4a..19fbeb1b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,17 @@ History ------- +0.6.0 (2019-05-08) ++++++++++++++++++++++++ + +Includes backwards-incompatible changes to data model ``DteDataL2``. + +* (PR #38, 2019-05-08) dte.data_models: alter field ``DteDataL2.signature_x509_cert_pem`` +* (PR #37, 2019-05-08) dte.data_models: alter field ``DteDataL2.firma_documento_dt_naive`` +* (PR #36, 2019-05-08) libs.crypto_utils: add functions +* (PR #35, 2019-05-07) libs.tz_utils: minor improvements +* (PR #34, 2019-05-06) docs: Fix `bumpversion` command + 0.5.1 (2019-05-03) +++++++++++++++++++++++ From 32695972a63cb73e2bbcb0ce6d76a80491b87150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Wed, 8 May 2019 12:48:49 -0400 Subject: [PATCH 9/9] =?UTF-8?q?Bump=20version:=200.5.1=20=E2=86=92=200.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cl_sii/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c7ce0cb4..c60a93e2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.1 +current_version = 0.6.0 commit = True tag = True diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py index 321f80ef..ffd087f7 100644 --- a/cl_sii/__init__.py +++ b/cl_sii/__init__.py @@ -5,4 +5,4 @@ """ -__version__ = '0.5.1' +__version__ = '0.6.0'