From 524cef7ecee567f364962add635630b587023bc8 Mon Sep 17 00:00:00 2001 From: El Mau Date: Sun, 20 Feb 2022 22:04:25 -0600 Subject: [PATCH 1/3] Actualizar README --- README.md | 2 ++ requirements.txt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7aa67d1..c6c8ea1 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,5 @@ Maneja los certificados de sello y FIEL fácilmente + +## Software libre, NO gratis. diff --git a/requirements.txt b/requirements.txt index 204bb1d..c909f0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ xmlsec -cryptography +cryptography==3.4.8 From 79418bd96a14f6740bf791c0e74a034c9bf231f0 Mon Sep 17 00:00:00 2001 From: El Mau Date: Wed, 3 Apr 2024 18:54:36 -0600 Subject: [PATCH 2/3] Refactorizada --- CHANGELOG.md | 4 + README.md | 1 + VERSION | 1 + cert/finkok.cer | Bin 1471 -> 0 bytes requirements.txt | 3 +- source/__init__.py | 2 +- source/cfdi_cert.py | 297 +++++++++--------- source/conf.py.example | 7 - source/tests/__init_.py | 2 + .../tests/certs/nopareja.cer | Bin .../tests/certs/nopareja.key | Bin source/tests/certs/novigente.cer | Bin 0 -> 1533 bytes .../tests/certs/novigente.key | Bin source/tests/certs/vigente.cer | Bin 0 -> 1460 bytes source/tests/certs/vigente.key | Bin 0 -> 1298 bytes source/tests/test.py | 154 +++++++++ source/tests/test_config.py | 24 ++ 17 files changed, 328 insertions(+), 167 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 VERSION delete mode 100644 cert/finkok.cer delete mode 100644 source/conf.py.example create mode 100644 source/tests/__init_.py rename cert/comercio.cer => source/tests/certs/nopareja.cer (100%) rename cert/finkok.key => source/tests/certs/nopareja.key (100%) create mode 100644 source/tests/certs/novigente.cer rename cert/comercio.key => source/tests/certs/novigente.key (100%) create mode 100644 source/tests/certs/vigente.cer create mode 100644 source/tests/certs/vigente.key create mode 100644 source/tests/test.py create mode 100644 source/tests/test_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f19ac48 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +v 1.0.0 [03-Abr-2024] +--------------------- + - Refactorizada + diff --git a/README.md b/README.md index c6c8ea1..64fc362 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,4 @@ Maneja los certificados de sello y FIEL fácilmente ## Software libre, NO gratis. + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/cert/finkok.cer b/cert/finkok.cer deleted file mode 100644 index 471d7393d808ec3e33fca1d325e1ae6ab2ebf360..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1471 zcmXqLV%=@f#JqR`GZP~dlZdf_fq@|q1Az&MFfuVVG2mt6)N1o+`_9YA$j!=N(8Q>1 z$PW}`4rO8HVRLj=2z3lG)HBcl32^bK2D=7@c{+RgE4a8SIJ)?H`gsP21UUi){1ifh zJe@*A9D_U^4W$euKoTrEhHWJ7;+nMg7|F0Oui9@k_O@+4hN5bvuCJ_qYKErzOE6T&i;mi2K*o?b{-CA z|44sFXGcGnkghO?k)ElZiJpn6p{jv0!kJ9MVnwON1^Ic!iAg!BRtk>JF20VsK))Ht ziSrs-8kiZH8(Nx}7@9m)Y8Df*wEbA*-}AYA;L4z$->gm(9kT< z)x=QFKnA3YRag{7%GW)}H!8@$6eiCtEb3aET$-AbsNkKNTaa3$5L%R;Sdt1%GfK$G zh>?|nxrvb1(e_v9f^w+vo`xb`j=zI-v|F|f(vrY57$DG2V zXJM=_p4{jw;@FqTS1Ki)lj#&JJTd;a_ojG>>>Fw?yR2I(&+!U)@VzRRXlXqo|M#W5 z*Pk;t9G>eG9MWJh5X@Fr*ZZ?uELh)q|LdudZ#-BEQ-$|v+e=RJR=#Z&mo($1vfBRp z>+2R|O_&@m?B@95`K5gImiu}B-#dEdKd3)3Pd}aYeN1AOiBZvt{0)b<|9(@Zm&<&z zXA6Jjooy@46=o_JOmyT7k+{LUvcLXATw2iAgo($EH*~O-egC`X2*2mw$2}Td{X2f` zJd=9;VS#Gq?0w8>ol6D(d+nH=_3ZsgS0-jg2FAs}QEZv-RBVH)Kci zvPMPE5pvOXzff@ZMBRmgP0X$(5g}W2Hr$I|b5*4?=TEXu$u7}jklV79BJ%an0Uu9>c-NS;y2l!A9Mcr zSNUr9yf`l3sX{OG>r6I2d=PxNRnzqAub`XyKVPm|AlBG0u|Q(`#iAz;vuuxYD}E~F zaeEW9W@3rld#T$Y%8ZlNWZekk7Gm zs)4%UQGrDUen;FE%CsE6x5liXu){TH-FpgmR+kO{dbnN zHg7n_w)GnKiA^h?w%pQ^E@$G{S$x?rYC?YOva1(VR73opTz8-I|ID(*>#dkxEu7#h zlhfGTxN3GclS;ux=5QOK@LM-Ugt>fmnJrWGyU)$(b(3@KoWHJck>i?7KgI{PzmCb> li`xG9@vnr4%C!p&B(l#Kdvlpd2F$cNzif*KpM!kqKLCSQ2jTz# diff --git a/requirements.txt b/requirements.txt index c909f0e..7ea8cc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -xmlsec -cryptography==3.4.8 +cryptography==42.0.5 \ No newline at end of file diff --git a/source/__init__.py b/source/__init__.py index 516f2c1..0365c7e 100644 --- a/source/__init__.py +++ b/source/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -from .cfdi-cert import SATCertificate +from .cfdi_cert import CertValidate, CertSign diff --git a/source/cfdi_cert.py b/source/cfdi_cert.py index ec1937f..2dc1f58 100644 --- a/source/cfdi_cert.py +++ b/source/cfdi_cert.py @@ -1,10 +1,24 @@ #!/usr/bin/env python3 -import argparse +# ~ cfdi_cert - Para trabajar con certificados del SAT fácilmente +# ~ Copyright (C) 2020-2024 Mauricio Servin (elmau) + +# ~ This program is free software: you can redistribute it and/or modify it +# ~ under the terms of the GNU General Public License as published by the +# ~ Free Software Foundation, either version 3 of the License, or (at your +# ~ option) any later version. + +# ~ This program is distributed in the hope that it will be useful, but +# ~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# ~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# ~ for more details. + +# ~ You should have received a copy of the GNU General Public License along +# ~ with this program. If not, see . + + import base64 -import datetime -import getpass -from pathlib import Path +from datetime import datetime, timezone from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization @@ -14,21 +28,23 @@ from cryptography.x509.oid import ExtensionOID from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding -from conf import TOKEN + +def get_hash(words: tuple): + digest = hashes.Hash(hashes.SHA512(), default_backend()) + for word in words: + digest.update(word.encode()) + return digest.finalize() -class SATCertificate(object): +class CertValidate(object): - def __init__(self, cer=b'', key=b'', pem=b'', password=''): + def __init__(self): self._error = '' self._init_values() - self._get_data_cer(cer) - self._get_data_key(key, password) - self._get_data_pem(pem) def _init_values(self): self._rfc = '' - self._serial_number = '' + self._serial_number1 = '' self._serial_number2 = '' self._not_before = None self._not_after = None @@ -37,159 +53,40 @@ class SATCertificate(object): self._is_valid_time = False self._cer_pem = '' self._cer_txt = '' + self._key = None self._key_enc = b'' self._key_pem = b'' - self._p12 = b'' - self._cer_modulus = 0 - self._key_modulus = 0 + self._modulus_cer = 0 + self._modulus_key = 0 self._issuer = '' 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) + msg = f'''\tRFC: {self.rfc} + No de Serie: {self.serial_number1} + No de Serie: {self.serial_number2} + Válido desde: {self.not_before} + Válido hasta: {self.not_after} + Es vigente: {self.is_valid_time} + Son pareja: {self.are_couple} + Es FIEL: {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_number2 = obj.serial_number - 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 - self._issuer = ','.join([i.rfc4514_string() for i in obj.issuer]) - - 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 - - def _get_data_pem(self, pem): - if not pem: - return - - self._key_pem = serialization.load_pem_private_key( - pem, None, backend=default_backend()) - return - - # 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_with_pem(self, data, name_hash): - if name_hash == 'sha256': - type_hash = hashes.SHA256() - elif name_hash == 'sha1': - type_hash = hashes.SHA1() - - firma = self._key_pem.sign(data, padding.PKCS1v15(), type_hash) - return base64.b64encode(firma).decode() - - def sign_sha1(self, data, password=''): - if self._key_pem: - return self._sign_with_pem(data, 'sha1') - - private_key = self._get_key(password) - firma = private_key.sign(data, padding.PKCS1v15(), hashes.SHA1()) - return base64.b64encode(firma).decode() - - def sign_xml(self, tree): - import xmlsec - - 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 error(self): + return self._error @property def rfc(self): return self._rfc @property - def serial_number(self): - return self._serial_number + def serial_number1(self): + return self._serial_number1 @property def serial_number2(self): @@ -231,18 +128,104 @@ class SATCertificate(object): def key_pem(self): return self._get_key_pem() - @property - def key_enc(self): - return self._key_enc - @property def issuer(self): return self._issuer - @property - def p12(self): - return self._get_p12() + 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_number1 = obj.serial_number + self._serial_number2 = f'{obj.serial_number:x}'[1::2] + self._not_before = obj.not_valid_before_utc + self._not_after = obj.not_valid_after_utc + self._issuer = ','.join([i.rfc4514_string() for i in obj.issuer]) - @property - def error(self): - return self._error + now = datetime.now(timezone.utc) + 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): + try: + obj = serialization.load_der_private_key( + key, password.encode(), default_backend()) + except ValueError: + msg = 'La contraseña es incorrecta' + self._error = msg + return + + 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 + self._key = obj + return + + def get_key_enc(self, words: tuple): + pw = get_hash(words) + key_enc = self._key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(pw) + ) + return key_enc + + def get_key_pem(self, key_enc: bytes, words: tuple): + pw = get_hash(words) + key = serialization.load_pem_private_key(key_enc, pw, default_backend()) + key_pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + return key_pem + + def validate(self, cer: bytes, key: bytes, password: str) -> bool: + self._get_data_cer(cer) + self._get_data_key(key, password) + return self.is_valid + + +class CertSign(object): + HS = { + 'sha1': hashes.SHA1, + 'sha256': hashes.SHA256, + } + + def __init__(self, key_file: bytes, words: tuple=()): + pw = None + if words: + pw = get_hash(words) + self._key = serialization.load_pem_private_key(key_file, pw, default_backend()) + + def sign(self, data, type_hash: str='sha256'): + if isinstance(data, str): + data = data.encode() + firma = self._key.sign(data, padding.PKCS1v15(), self.HS[type_hash]()) + firma = base64.b64encode(firma).decode() + return firma + + +# ~ def sign_xml(tree): + # ~ import xmlsec + + # ~ 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 \ No newline at end of file diff --git a/source/conf.py.example b/source/conf.py.example deleted file mode 100644 index 4e350fb..0000000 --- a/source/conf.py.example +++ /dev/null @@ -1,7 +0,0 @@ -#!/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/__init_.py b/source/tests/__init_.py new file mode 100644 index 0000000..cf529d7 --- /dev/null +++ b/source/tests/__init_.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python + diff --git a/cert/comercio.cer b/source/tests/certs/nopareja.cer similarity index 100% rename from cert/comercio.cer rename to source/tests/certs/nopareja.cer diff --git a/cert/finkok.key b/source/tests/certs/nopareja.key similarity index 100% rename from cert/finkok.key rename to source/tests/certs/nopareja.key diff --git a/source/tests/certs/novigente.cer b/source/tests/certs/novigente.cer new file mode 100644 index 0000000000000000000000000000000000000000..ebcba8069ef03a00dbf3ebe396bcbda58e887dc1 GIT binary patch literal 1533 zcmXqLV*P2*#QbmpGZP~dlZdf_fq@|q0|AI>WNdC=VZh7AsnzDu_MMlJk(-slpouZf zP{BZsjX9KsnMd4F&sk5wNFgOvp`fTVH7T)J!^FVSOw&-`Ko_Kfi$^UuwWusJIWr%m z#xW%~GcU8aq$n{t^YG?8g^;4mq|%baqRd1?3j;Hd25ugGbVWdY!Kvw`MVTpyDIh~~ z5*0l2((;RPA%+-g8mPlv$jK-sl~|lvoR?bS0CkXFabk&HdVZ2#ZiS(mfeOeHP99mu zGCc(kpoux@`3mOdIttGDIeH52rKv?lsYUsQd^z*#`IY&J$%hx_8HySRgT!=&d4e57 zEX@tf4GjJKj13hHi;EiTB{YLH8eLc1qO|=sfnps zlsK=EArjZ1@hRBvK*O1Zfm)nheHDUTgTg$WJ^h0fTzvzAT!S5hJRN;pgB60EVM;ZL zSIR|%QdI+Gi0@<^9UXyb2MB=-1$_k%SN9NeGhkveHVHN|lrxY4DPt8DMUnD#aSQVE zGq5z2Hjo6#bBhQ$2fG*;#v>aBOmRxciI9<%fw_s1ACwZgn3@VbJya zKvuQe+*S|k`rRkZov+>GS~TtSuS=791v9RAoi9l$w68GqpS+JjiP^EZPF6q9FyY4B zrGi-@9pV@IMAQ@iJp5)?KV_D4MbXRb8)nP3twR;wE0q>o6<=$gKf`bF4(FrAO!pqI zlZ-v;-8_4;$kpGMbM`zCy%j8XdrSLTg%UpL?p<4@(^;SW{`I5m&iDN6?gnu?KApW! zKJGK<-o4;b&*y86Ch5vPbL_tPPdhWCzq~BdiAk=s!TI+Qv14IO%@XNb*FO2KnGmx~ zoU_wpve&F#A3U#_Obq?!*k>e?^IA8PtMXf=f}EUX@eTC@Q$k6Li)54pzfl^MP$zi>2-^Kr|%swSt`2Byy@wbjsejD<7 zuad4imV2Q!%Fw4lsPL_F-^qhM3Ex!t^!%%ss`900HH(LTfAV!v`F{VI3j55ArdilL zoRa-xa^@P9DN7vmLmz)>_i*={sJ&j$drh{K-{ONCE(@&jT$Wbu@#*cfll_6lTPJOs zY|~{cqB-TiSxQ7!?8k`=3^8`8pX_q49<2NBd~mT@$HB>tQ!Wd!T52pgvClzWuHWzR zlvN&cw8AfY*L*PK>Wg!g?UPcw`_00=`fPE~sY=OvzCT9T&vbMJj1*1dY>g@DFyn=-r~-Iq9gp<#$*Uz$PoLmxgbI#MBFIdgbeA~M@WMxbD!jOu&Nf~BS&Hh<$eV^nV Mw&-D;K5M@=0C4p#ZvX%Q literal 0 HcmV?d00001 diff --git a/cert/comercio.key b/source/tests/certs/novigente.key similarity index 100% rename from cert/comercio.key rename to source/tests/certs/novigente.key diff --git a/source/tests/certs/vigente.cer b/source/tests/certs/vigente.cer new file mode 100644 index 0000000000000000000000000000000000000000..e0963d127e6e1adc67ca1e96d8f82e7a55ec643c GIT binary patch literal 1460 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 IE$;$CifF5Kl>h($ literal 0 HcmV?d00001 diff --git a/source/tests/test.py b/source/tests/test.py new file mode 100644 index 0000000..a50b15b --- /dev/null +++ b/source/tests/test.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# coding: utf-8 + +import sys +from pathlib import Path +p = str(Path(__file__).resolve().parent.parent) +sys.path.insert(0, p) + +import unittest +from test_config import * +from cfdi_cert import CertValidate, CertSign + + +# ~ @unittest.SkipTest + + +class BaseTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def setUp(self): + msg = f'In method: {self._testMethodName}' + print(msg) + + def tearDown(self): + pass + + +class TestCertValidate(BaseTest): + + def test_validate_not_vigente(self): + path_cer = path_certs / 'novigente.cer' + path_key = path_certs / 'novigente.key' + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + cert = CertValidate() + result = cert.validate(cer, key, PASSWORD) + self.assertFalse(result) + + expected = 'El certificado no es vigente' + result = cert.error + self.assertEqual(expected, result) + return + + def test_validate_not_couple(self): + path_cer = path_certs / 'nopareja.cer' + path_key = path_certs / 'nopareja.key' + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + cert = CertValidate() + result = cert.validate(cer, key, PASSWORD) + self.assertFalse(result) + + expected = 'El CER y el KEY no son pareja' + result = cert.error + self.assertEqual(expected, result) + return + + def test_validate_wrong_password(self): + cert = CertValidate() + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + result = cert.validate(cer, key, 'letmein') + self.assertFalse(result) + + expected = 'La contraseña es incorrecta' + result = cert.error + self.assertEqual(expected, result) + return + + def test_get_key_enc_pem(self): + cert = CertValidate() + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + result = cert.validate(cer, key, PASSWORD) + self.assertTrue(result) + + words = (cert.rfc, str(cert.serial_number1), MY_TOKEN) + result = cert.get_key_enc(words) + self.assertIsNotNone(result) + + result = cert.get_key_pem(result, words) + self.assertIsNotNone(result) + + return + + def test_validate_cert(self): + cert = CertValidate() + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + result = cert.validate(cer, key, PASSWORD) + # ~ print(cert) + + self.assertTrue(result) + return + + +class TestCertSign(BaseTest): + + def test_sign(self): + cert = CertValidate() + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + result = cert.validate(cer, key, PASSWORD) + self.assertTrue(result) + + words = (cert.rfc, str(cert.serial_number1), MY_TOKEN) + key_enc = cert.get_key_enc(words) + + expected = 'ZSOD/SNUP0YmogV7h94ysXxERPSy8M+EBfWK4oKWkGRIMqSM1DEGLVi0IE0YNDoZTnBSWULsozCxOwt5rJdGE1tr2OTaXaHMubvC88vhqiv62mOeU/vGCv2yPbKcbjOpKDSQ/pEGlgUd69mESwekjpPI0c0NUWlnkO81eHr+Z8v7hTnJxoopvDiMAkg82snPDIFoIBEePcB/VL8oABRLKh9/2UHFMeS0YKQJWApPEaXD1ycxUbBqXgbi2OwQgM4vWMNX0qsiHyuEI82/zUZ8WLj+GHG6m+P/VKs9nYfEurXh68wZZqT1nzUNHudQxGVFdrwgj+uh7kl3O0Swoi160w==' + + cert = CertSign(key_enc, words) + data = 'Ingrid Bergman' + result = cert.sign(data) + + self.assertEqual(expected, result) + return + + def test_sign_with_pem(self): + cert = CertValidate() + cer = path_cer.read_bytes() + key = path_key.read_bytes() + + result = cert.validate(cer, key, PASSWORD) + self.assertTrue(result) + + words = (cert.rfc, str(cert.serial_number1), MY_TOKEN) + key_enc = cert.get_key_enc(words) + key_pem = cert.get_key_pem(key_enc, words) + + expected = 'ZSOD/SNUP0YmogV7h94ysXxERPSy8M+EBfWK4oKWkGRIMqSM1DEGLVi0IE0YNDoZTnBSWULsozCxOwt5rJdGE1tr2OTaXaHMubvC88vhqiv62mOeU/vGCv2yPbKcbjOpKDSQ/pEGlgUd69mESwekjpPI0c0NUWlnkO81eHr+Z8v7hTnJxoopvDiMAkg82snPDIFoIBEePcB/VL8oABRLKh9/2UHFMeS0YKQJWApPEaXD1ycxUbBqXgbi2OwQgM4vWMNX0qsiHyuEI82/zUZ8WLj+GHG6m+P/VKs9nYfEurXh68wZZqT1nzUNHudQxGVFdrwgj+uh7kl3O0Swoi160w==' + + cert = CertSign(key_pem) + data = 'Ingrid Bergman' + result = cert.sign(data) + + self.assertEqual(expected, result) + return + + +if __name__ == '__main__': + unittest.main(exit=False) diff --git a/source/tests/test_config.py b/source/tests/test_config.py new file mode 100644 index 0000000..a280cdd --- /dev/null +++ b/source/tests/test_config.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from pathlib import Path + + +__all__ = [ + 'MY_TOKEN', + 'PASSWORD', + 'path_certs', + 'path_cer', + 'path_key', + ] + + +FOLDER = 'certs' +MY_TOKEN = 'PutoSat' +NAME = 'vigente' +PASSWORD = '12345678a' + + +path_current = Path(__file__).resolve().parent +path_certs = path_current / FOLDER +path_cer = path_certs / f'{NAME}.cer' +path_key = path_certs / f'{NAME}.key' From 570f7ecf10f786dc9f226a74eadb073f36285218 Mon Sep 17 00:00:00 2001 From: El Mau Date: Wed, 3 Apr 2024 18:57:31 -0600 Subject: [PATCH 3/3] Actualizar README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 64fc362..c263956 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,7 @@ Maneja los certificados de sello y FIEL fácilmente ## Software libre, NO gratis. + +* G1: `A5DdXxCKPw3QKWVdDVs7CzkNugNUW1sHu5zDJFWxCU2h` + +[Apoya](https://cuates.net/acerca-de/)