diff --git a/source/tests/certificados/comercio.cer b/source/tests/certificados/comercio.cer new file mode 100644 index 0000000..471d739 Binary files /dev/null and b/source/tests/certificados/comercio.cer differ diff --git a/source/tests/certificados/comercio.enc b/source/tests/certificados/comercio.enc new file mode 100644 index 0000000..e2e9aa4 --- /dev/null +++ b/source/tests/certificados/comercio.enc @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI6siSTwKyr/QCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCI0ZtyIVL+Lutw1HiX9yiABIIE +0NVK6M/HDydtFkytzQO0x5iZGKZ4rQQOvSaPItX0k/kOUX/fcMEmHvqGs3jE+HgL +I57mW7lxC4cM5mc746p9RQI/FYGZH7t9u3KVdL5+muJ8Khs7GRu7hWK3qFOtfdxY +8VE+1bq+3rd2Cj464An3OVKryUC16szIfTZNXJU6Ftxaq3uSDSJRct8zG4JgTMhj +I+W1XJxUWb+56FI2dJK3MMI8IDR7ctED9XoGBP7lksqqcUmzqolgrWppfcJNoGAF +t5Cz6ZAZShPzhbrkZ7xweMWgHYJlghMcm40D6OTr51l4mxvIOU7vKIIswYZwn1jz +SGfo8icrhsDRpigL5vB4y0asPHFt5AvYWx0hx0CDcxDj71s09svwuSQtTEtQdNDQ +acrffuK2miv6sxXD8DL7+cQ7Q/fdWRhELJ8HRU98YOHVJchutF7LCcAbA/iti195 +YFr10krT5xZyCb/6J33B6UY1LXu94GfmLQA2mdTwucQd0zN5mvmrlG/SXnq5LG36 +dxyXwY4uehLunKWEJu9lX/pdVB7TuNU1sA7411x0hlHi4OepTVgCOsx5nYDWviOa +v02pUPD/ddDtJ45u6cTDGBIeeeo9ZebVUAYz7C4FS1wEspJ/Ux10gC3NPYai7m8T +0HjE5fUXXgeeHI6AGDwzSTFxvLgVYUHGNfvf8Xuz/6/B1JQAxUWslrcI8px2WD0j +xgot2omFbNTZkvKAfn13lZcx8dR0K5qittVFxBwOZkGg+honwzn4TzQp8ZL3E1mQ +2DOgquSlxgT0McUivFTera6OMXMrQSmEbwkuFZh/YN4FE7uhpqB0XKM79v4cmYkD +ze2eX46hG9nVwFXo1jPcVbMuxzwLXmVibvSsTWidqV4CKxyZB9o+B2N00dRe1bh6 +keshRlNWz3drxDCiglKz+BV84OxizWAeCQmBKNqF9umzIM/5/Cg/KnUinG5Vcw52 +5UUiIBGN4a7NQZHtTqTLRYE9vpb/7AkniTQXeQ9ZKUAF1Z+8bkxCTW7Gdmpfku2t ++5SL1qeD3bvUlShBoONJBa21KIsjZsNxEI35n9898xufs4VoNIFzsyhMlcnkoNar +eQ+1rYigmOeKTq3T1grHujFTjVGUe8yYvgoUQjn29LH8zJkypBkL9wmY7/KBb8xK +ZwUguw1y0hYSReMSxKqWThrQwmtkARFu2F0lUbuEXMvDVVpSfNd7w2ij1OP1nwwL +g0q3Sd5d+cYhAGC/FioUL4vFb38DBEjCbUeezc5l8eYNfKrU8JCGSENAv1e756Vu +xb+7mkw/YqL6vMeqq6/S58Naz2IXCM+1iH9KuRS2xqayMCuISbyb/QtzWNnDrTPN +/8qCHO9yijP2/1mQURCnj8lqUbr7nbp9mzuaC+eAaVFEGSfDLx1wV8mFH0rko5Lr +/o5bs/15uCCXxfPWsbViDc+5nCnm0qvLBzUWZfps10CklvcU5Gq5f2VyBrMKyP1X +cQnS6RogrJ6kEYTBc1/VGTTw4RKR6CnCmYNi921stLu9ayMfDroegmEPuM+59+Tn +2Jw2mw/jL0m0stIoAgBEfc4i8a+uabtbD01yJEBnaTomfUUJNckJf3RPZrnDYmGr +lOaDNKIu+f1GcLrahHqbXXVTGVVGqezOUn/IdYubufYw +-----END ENCRYPTED PRIVATE KEY----- diff --git a/source/tests/certificados/comercio.key b/source/tests/certificados/comercio.key new file mode 100644 index 0000000..0155f0a Binary files /dev/null and b/source/tests/certificados/comercio.key differ diff --git a/source/tests/pycert/__init__.py b/source/tests/pycert/__init__.py new file mode 100644 index 0000000..dbe4529 --- /dev/null +++ b/source/tests/pycert/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +from .cfdi_cert import SATCertificate diff --git a/source/tests/pycert/cfdi_cert.py b/source/tests/pycert/cfdi_cert.py new file mode 100644 index 0000000..c493b88 --- /dev/null +++ b/source/tests/pycert/cfdi_cert.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 + +import argparse +import base64 +import datetime +import getpass +from pathlib import Path + +import xmlsec +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.x509.oid import ExtensionOID +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding + +from .conf import TOKEN + + +class SATCertificate(object): + + def __init__(self, cer=b'', key=b'', password=''): + self._error = '' + self._init_values() + self._get_data_cer(cer) + self._get_data_key(key, password) + + def _init_values(self): + self._rfc = '' + self._serial_number = '' + self._not_before = None + self._not_after = None + self._is_fiel = False + self._are_couple = False + self._is_valid_time = False + self._cer_pem = '' + self._cer_txt = '' + self._key_enc = b'' + self._p12 = b'' + self._cer_modulus = 0 + self._key_modulus = 0 + return + + def __str__(self): + msg = '\tRFC: {}\n'.format(self.rfc) + msg += '\tNo de Serie: {}\n'.format(self.serial_number) + msg += '\tVálido desde: {}\n'.format(self.not_before) + msg += '\tVálido hasta: {}\n'.format(self.not_after) + msg += '\tEs vigente: {}\n'.format(self.is_valid_time) + msg += '\tSon pareja: {}\n'.format(self.are_couple) + msg += '\tEs FIEL: {}\n'.format(self.is_fiel) + return msg + + def __bool__(self): + return self.is_valid + + def _get_hash(self): + digest = hashes.Hash(hashes.SHA512(), default_backend()) + digest.update(self._rfc.encode()) + digest.update(self._serial_number.encode()) + digest.update(TOKEN.encode()) + return digest.finalize() + + def _get_data_cer(self, cer): + obj = x509.load_der_x509_certificate(cer, default_backend()) + self._rfc = obj.subject.get_attributes_for_oid( + NameOID.X500_UNIQUE_IDENTIFIER)[0].value.split(' ')[0] + self._serial_number = '{0:x}'.format(obj.serial_number)[1::2] + self._not_before = obj.not_valid_before + self._not_after = obj.not_valid_after + now = datetime.datetime.utcnow() + self._is_valid_time = (now > self.not_before) and (now < self.not_after) + if not self._is_valid_time: + msg = 'El certificado no es vigente' + self._error = msg + + self._is_fiel = obj.extensions.get_extension_for_oid( + ExtensionOID.KEY_USAGE).value.key_agreement + + self._cer_pem = obj.public_bytes(serialization.Encoding.PEM).decode() + self._cer_txt = ''.join(self._cer_pem.split('\n')[1:-2]) + self._cer_modulus = obj.public_key().public_numbers().n + return + + def _get_data_key(self, key, password): + self._key_enc = key + if not key or not password: + return + + try: + obj = serialization.load_der_private_key( + key, password.encode(), default_backend()) + except ValueError: + msg = 'La contraseña es incorrecta' + self._error = msg + return + + p = self._get_hash() + self._key_enc = obj.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(p) + ) + + self._key_modulus = obj.public_key().public_numbers().n + self._are_couple = self._cer_modulus == self._key_modulus + if not self._are_couple: + msg = 'El CER y el KEY no son pareja' + self._error = msg + return + + def _get_key(self, password): + if not password: + password = self._get_hash() + private_key = serialization.load_pem_private_key( + self._key_enc, password=password, backend=default_backend()) + return private_key + + def _get_key_pem(self): + obj = self._get_key('') + key_pem = obj.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + return key_pem + + # Not work + def _get_p12(self): + obj = serialization.pkcs12.serialize_key_and_certificates('test', + self.key_pem, self.cer_pem, None, + encryption_algorithm=serialization.NoEncryption() + ) + return obj + + def sign(self, data, password=''): + private_key = self._get_key(password) + firma = private_key.sign(data, padding.PKCS1v15(), hashes.SHA256()) + return base64.b64encode(firma).decode() + + def sign_xml(self, tree): + node = xmlsec.tree.find_node(tree, xmlsec.constants.NodeSignature) + ctx = xmlsec.SignatureContext() + key = xmlsec.Key.from_memory(self.key_pem, xmlsec.constants.KeyDataFormatPem) + ctx.key = key + ctx.sign(node) + node = xmlsec.tree.find_node(tree, 'X509Certificate') + node.text = self.cer_txt + return tree + + @property + def rfc(self): + return self._rfc + + @property + def serial_number(self): + return self._serial_number + + @property + def not_before(self): + return self._not_before + + @property + def not_after(self): + return self._not_after + + @property + def is_fiel(self): + return self._is_fiel + + @property + def are_couple(self): + return self._are_couple + + @property + def is_valid(self): + return not bool(self.error) + + @property + def is_valid_time(self): + return self._is_valid_time + + @property + def cer_pem(self): + return self._cer_pem.encode() + + @property + def cer_txt(self): + return self._cer_txt + + @property + def key_pem(self): + return self._get_key_pem() + + @property + def key_enc(self): + return self._key_enc + + @property + def p12(self): + return self._get_p12() + + @property + def error(self): + return self._error + + +def main(args): + # ~ contra = getpass.getpass('Introduce la contraseña del archivo KEY: ') + contra = '12345678a' + if not contra.strip(): + msg = 'La contraseña es requerida' + print(msg) + return + + path_cer = Path(args.cer) + path_key = Path(args.key) + + if not path_cer.is_file(): + msg = 'El archivo CER es necesario' + print(msg) + return + + if not path_key.is_file(): + msg = 'El archivo KEY es necesario' + print(msg) + return + + cer = path_cer.read_bytes() + key = path_key.read_bytes() + cert = SATCertificate(cer, key, contra) + + if cert.error: + print(cert.error) + else: + print(cert) + return + + +def _process_command_line_arguments(): + parser = argparse.ArgumentParser(description='CFDI Certificados') + + help = 'Archivo CER' + parser.add_argument('-c', '--cer', help=help, default='') + help = 'Archivo KEY' + parser.add_argument('-k', '--key', help=help, default='') + + args = parser.parse_args() + return args + + +if __name__ == '__main__': + args = _process_command_line_arguments() + main(args) diff --git a/source/tests/pycert/conf.py.example b/source/tests/pycert/conf.py.example new file mode 100644 index 0000000..4e350fb --- /dev/null +++ b/source/tests/pycert/conf.py.example @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +# ~ Establece un token personalizado para encriptar las claves +# ~ from secrets import token_hex +# ~ token_hex(32) + +TOKEN = '' diff --git a/source/tests/tests_comercio.py b/source/tests/tests_comercio.py new file mode 100644 index 0000000..831d9ee --- /dev/null +++ b/source/tests/tests_comercio.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import datetime +import sys +import unittest +import uuid +import lxml.etree as ET +from io import BytesIO +from pathlib import Path + +sys.path.append('..') +from pycert import SATCertificate +from comerciodigital import PACComercioDigital + + +NAME = 'comercio' + +TEMPLATE_CFDI = """ + + + + + + + + + + + + + + + + + +""" + +class TestCfdi(object): + + def __init__(self): + self._xml = '' + self._make_cfdi() + + @property + def xml(self): + return self._xml.decode() + + def _make_cfdi(self): + path = Path(__file__) + path_cer = Path(path.parent).joinpath('certificados', f'{NAME}.cer') + path_key = Path(path.parent).joinpath('certificados', f'{NAME}.enc') + path_xslt = Path(path.parent).joinpath('xslt', 'cadena.xslt') + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + self._cert = SATCertificate(cer, key) + self._doc = ET.parse(BytesIO(TEMPLATE_CFDI.encode())) + self._root = self._doc.getroot() + self._root.attrib['Fecha'] = datetime.datetime.now().isoformat()[:19] + self._root.attrib['NoCertificado'] = self._cert.serial_number + self._root.attrib['Certificado'] = self._cert.cer_txt + + self._add_stamp(path_xslt) + + self._xml = ET.tostring(self._root, + pretty_print=True, doctype='') + return + + def _add_stamp(self, path_xslt): + xslt = open(path_xslt, 'rb') + transfor = ET.XSLT(ET.parse(xslt)) + cadena = str(transfor(self._doc)).encode() + stamp = self._cert.sign(cadena) + self._root.attrib['Sello'] = stamp + xslt.close() + return + + +class TestStamp(unittest.TestCase): + + def setUp(self): + print(f'In method: {self._testMethodName}') + self.pac = PACComercioDigital() + + def test_cfdi_stamp(self): + cfdi = TestCfdi().xml + result = self.pac.stamp(cfdi) + cfdi_uuid = self.pac.cfdi_uuid + + self.assertFalse(bool(self.pac.error)) + self.assertTrue(bool(uuid.UUID(cfdi_uuid))) + + +if __name__ == '__main__': + unittest.main() diff --git a/source/tests/xslt/cadena.xslt b/source/tests/xslt/cadena.xslt new file mode 100644 index 0000000..4c6e2d5 --- /dev/null +++ b/source/tests/xslt/cadena.xslt @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + ||| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/tests/xslt/comercioexterior11.xslt b/source/tests/xslt/comercioexterior11.xslt new file mode 100644 index 0000000..fd71841 --- /dev/null +++ b/source/tests/xslt/comercioexterior11.xslt @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/tests/xslt/divisas.xslt b/source/tests/xslt/divisas.xslt new file mode 100644 index 0000000..dc8988e --- /dev/null +++ b/source/tests/xslt/divisas.xslt @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/tests/xslt/donat11.xslt b/source/tests/xslt/donat11.xslt new file mode 100644 index 0000000..24d4363 --- /dev/null +++ b/source/tests/xslt/donat11.xslt @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/tests/xslt/iedu.xslt b/source/tests/xslt/iedu.xslt new file mode 100644 index 0000000..eb285cb --- /dev/null +++ b/source/tests/xslt/iedu.xslt @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/tests/xslt/implocal.xslt b/source/tests/xslt/implocal.xslt new file mode 100644 index 0000000..80b8d23 --- /dev/null +++ b/source/tests/xslt/implocal.xslt @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/tests/xslt/ine11.xslt b/source/tests/xslt/ine11.xslt new file mode 100644 index 0000000..05c1e56 --- /dev/null +++ b/source/tests/xslt/ine11.xslt @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/tests/xslt/leyendasFisc.xslt b/source/tests/xslt/leyendasFisc.xslt new file mode 100644 index 0000000..e0587a2 --- /dev/null +++ b/source/tests/xslt/leyendasFisc.xslt @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/tests/xslt/nomina12.xslt b/source/tests/xslt/nomina12.xslt new file mode 100644 index 0000000..2570170 --- /dev/null +++ b/source/tests/xslt/nomina12.xslt @@ -0,0 +1,412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/tests/xslt/pagos10.xslt b/source/tests/xslt/pagos10.xslt new file mode 100644 index 0000000..98b41f2 --- /dev/null +++ b/source/tests/xslt/pagos10.xslt @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/tests/xslt/utilerias.xslt b/source/tests/xslt/utilerias.xslt new file mode 100644 index 0000000..d5dd14e --- /dev/null +++ b/source/tests/xslt/utilerias.xslt @@ -0,0 +1,22 @@ + + + + + + | + + + + + + + + | + + + + + + + +