From 93fa2fb624141f727e48a1c459e522445d054f82 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 3 Dec 2020 15:11:50 -0600 Subject: [PATCH] =?UTF-8?q?Versi=C3=B3n=20inicial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + README.md | 3 +- cert/comercio.cer | Bin 0 -> 1533 bytes cert/comercio.key | Bin 0 -> 1298 bytes cert/finkok.cer | Bin 0 -> 1471 bytes cert/finkok.key | Bin 0 -> 1298 bytes requirements.txt | 2 + source/__init__.py | 3 + source/cfdi-cert.py | 257 +++++++++++++++++++++++++++++++++++++++++ source/conf.py.example | 7 ++ 10 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 cert/comercio.cer create mode 100644 cert/comercio.key create mode 100644 cert/finkok.cer create mode 100644 cert/finkok.key create mode 100644 requirements.txt create mode 100644 source/__init__.py create mode 100644 source/cfdi-cert.py create mode 100644 source/conf.py.example diff --git a/.gitignore b/.gitignore index 13d1490..d684f1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ # ---> Python + +conf.py + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index f9affbb..7aa67d1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # cfdi-cert -Maneja los certificados de sello y FIEL fácilmente \ No newline at end of file +Maneja los certificados de sello y FIEL fácilmente + diff --git a/cert/comercio.cer b/cert/comercio.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/cert/comercio.key new file mode 100644 index 0000000000000000000000000000000000000000..ecaa7a25413efab7ea60162ebebeeb344d6270db GIT binary patch literal 1298 zcmV+t1?~DUf&~sRKn4jahDe6@4FLrWFf%Y41_>&LNQU&4g?4S0RRGm0RaF4 z0tf&w6b1+?hDe6@4Fd-R2rz;Kz5)RNFa&}G$aoNWduY^xXu!bLSy7>IndL<$s7kl>Kt-j51mwWLZ1~vHK zNb5+MP;au0@g4f<;4g-yU2`oe27C%c7is1Qc?HFHCV7oTH0&(CE;B12;33v;)ttVR z{-w8dkEkYM2vXZt46zMQCxP{*Dov~qQ`F;NdBm#FP&y^x@tEByx1qz&+U$$RN+NQD;K%iz_;Ee zAF!y6NZQO{!Lp{Hu|_=Q4#Lkw)4qA{SW`s;Kk;n|x}7~$FTrGVnY@HX`Czt=sRn$@ zPz1lr0;}^m)uG^(>~73qNFhYU`}NQn<^u$TX;1v}~U9Sb58Yx984F>nrzg(4zz@U5J+w{NoAa@7Bx4gLDPLAF7&6ux!AjQ0}c67~A*)bA2 zsrf~11=g{%2D-C~t1Yu$54BDEQi%}5djB?h`(-28piIr6z|%Yc9KdyhBjka7FXOkm z)+gU-P!vNj#$aZ{5fQe%KQ`ZkQVrND%<61^xTVn&Jr_TeQ~*` z!jZYwGHFx1H@K5^6l44k@(&2+O0QiECPZXS91oyGXb2{b`mh(0FB$?yon{J#vxWrw z{#3rR80F~vzjJzgSiqxGA2V2%`=I{bAYBQ7uDU7%W(ovqSZ~k8WBRwZc|N;V*IM&S zJ2j&7QYXI=QVBb{m(hU=*m7G3q*>Yv>dO+$>9an+-P%b~MI`qJB4z-b%NUVtkDv?F zP+Cz{ghYKT_-Us}W0did5L97%XyF!FyF=3aqa_?rpA+$)DyHvKO=<9h&1lv; zi=N9^sOm(~Hym*Byfh0<4K`sZAt*EA;r7h!2TZ;|Fk1=efjuMBjs I$h^$0ybmF5RR910 literal 0 HcmV?d00001 diff --git a/cert/finkok.cer b/cert/finkok.cer new file mode 100644 index 0000000000000000000000000000000000000000..471d7393d808ec3e33fca1d325e1ae6ab2ebf360 GIT binary patch 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# literal 0 HcmV?d00001 diff --git a/cert/finkok.key b/cert/finkok.key new file mode 100644 index 0000000000000000000000000000000000000000..0155f0af4f867893e8a6a85e2be2fc5eec37ed25 GIT binary patch literal 1298 zcmV+t1?~DUf&~sRKn4jahDe6@4FLrWFf%Y41_>&LNQU&4g?4S0RRGm0RaF4 z0tf&w6b1+?hDe6@4Fd-R2rz;KyaE9LFa&}G$aoNWduY^xXu!bLSy7>IndL<$s7kEb=uTxQCw~xbt?E#!>!F0+xGu-}#hK(8zR1g$GxCM)qxpX| zF0rn3gwu>SU@VOUb+=XYB?-Z1;%_?D#yfyjC{c$_*F3+&h)g$HO}>%bxN-HSGWc{; zo@vn%nWB?qTIE$r`uA!s*)%K%Wnk5+MFXb*!x`MI(nb*OstRgyF~9IV!v3C;tqVi^ zi5nb=3tsCnBEH!u#5Z-IeDw*KGUoW;?sES@SJ~7ldpbIdYO{Xjz zkrY@uPCL_lrC5y9eb8mCo|Tv|V{|Ti+i|rV40lKTexvO?`{YfCR*lo5^5q8EX87lM zsY87tv(-|O9cPz<2A1=><>Gx3IOV6krpmL1Rz-Bu(b!fi;)DMt7(*&kvQW}p3g*bk zDRWj_Z;E|IIOuhJMhRzZN0_3|n65Y>$TH8Q1vVdv+}NANQnneKY(W^lH*wc^rY)I>h z4A^`+Ed$cUf?-O7*wu#D9MxUxww8(J#+&ljK}_@VH}%u;@H874NMGnX|Bi9yU8o?`>W!*@td}8)XND)A5`T_eml`w ze-09;Y^2FChKEb_fHAy!!l_MeOeonsbsPgUaV8*MtrMeOTsd>1_xx3Ev-FTU0aYY| zA!82m)MJw%$vTe*)JZhNfMgu&k(d|Ad1e3F@W` zVFvpk4M+u_P4m%BlD)k>;SN_E$B^KoKb*yr5kS82%$s5y6_Qd^>`-O)eC+G2Br%#* zh>F!)1cBO5QP#iNLCKyk;?enRAPUrM;EQ#v)nvtd7#=$XGhm+Mq}GMCOc(fj0-hx~ zG_~YG!too(1Q!w84Pqzl>0ToqiV!PILe-rM65mz#lNQ0JcSgLs*6jR4mkb?I1DBl2 zBAGyl{v9qo)R9oLz(9Bsr{9ypqBK&>8|}d&UUPguB#q@cHPg_TCNioE8*X0js5#^n z*pKv*bPIQE=$Gt>%+llKugAY5evs%$8ZMKpRN=ZMTW;CYwG%Z9GOZMAu;x}P!pS6( z_otyE&0|A+-O$WFB_! literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..204bb1d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +xmlsec +cryptography diff --git a/source/__init__.py b/source/__init__.py new file mode 100644 index 0000000..516f2c1 --- /dev/null +++ b/source/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +from .cfdi-cert import SATCertificate diff --git a/source/cfdi-cert.py b/source/cfdi-cert.py new file mode 100644 index 0000000..d017542 --- /dev/null +++ b/source/cfdi-cert.py @@ -0,0 +1,257 @@ +#!/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 + + def get_encrypt(self, password): + key_enc = self._key_enc + if password: + p = password.encode() + obj = self._get_key('') + key_enc = obj.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(p) + ) + return key_enc + + 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 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/conf.py.example b/source/conf.py.example new file mode 100644 index 0000000..4e350fb --- /dev/null +++ b/source/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 = ''