From 264ea14bede3e258b5be9ee118596302bea190c3 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 29 May 2017 10:10:53 -0300 Subject: [PATCH 1/3] Nova forma de assinatura usando o signXML --- pynfe/entidades/certificado.py | 17 +++++++------ pynfe/processamento/assinatura.py | 50 +++++++++++++++++++++++++++++++++++++++ pynfe/utils/__init__.py | 7 ++---- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/pynfe/entidades/certificado.py b/pynfe/entidades/certificado.py index 461bd3b..61eac5d 100644 --- a/pynfe/entidades/certificado.py +++ b/pynfe/entidades/certificado.py @@ -9,7 +9,7 @@ import os class Certificado(Entidade): """Classe abstrata responsavel por definir o modelo padrao para as demais classes de certificados digitais. - + Caso va implementar um novo formato de certificado, crie uma classe que herde desta.""" @@ -29,15 +29,15 @@ class CertificadoA1(Certificado): def __init__(self, caminho_arquivo=None): self.caminho_arquivo = caminho_arquivo self.arquivos_temp = [] - + def separar_arquivo(self, senha, caminho=False): """Separa o arquivo de certificado em dois: de chave e de certificado, e retorna a string. Se caminho for True grava na pasta temporaria e retorna - o caminho dos arquivos, apos o uso devem ser excluidos com o metodo excluir.""" - + o caminho dos arquivos, senao retorna o objeto. Apos o uso devem ser excluidos com o metodo excluir.""" + # Carrega o arquivo .pfx, erro pode ocorrer se a senha estiver errada ou formato invalido. pkcs12 = crypto.load_pkcs12(open(self.caminho_arquivo, "rb").read(), senha) - + if caminho: cert = crypto.dump_certificate(crypto.FILETYPE_PEM, pkcs12.get_certificate()) chave = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkcs12.get_privatekey()) @@ -55,12 +55,12 @@ class CertificadoA1(Certificado): cert = cert.replace('\n', '') cert = cert.replace('-----BEGIN CERTIFICATE-----', '') cert = cert.replace('-----END CERTIFICATE-----', '') - + # Chave, string decodificada da chave privada chave = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkcs12.get_privatekey()) - + return chave, cert - + def excluir(self): """Exclui os arquivos temporarios utilizados para o request.""" try: @@ -69,4 +69,3 @@ class CertificadoA1(Certificado): self.arquivos_temp.clear() except: pass - diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index 2e0472e..4ec6261 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- from pynfe.utils import etree, remover_acentos +from pynfe.utils.flags import NAMESPACE_SIG import subprocess +import signxml +from signxml import XMLSigner +from pynfe.entidades import CertificadoA1 class Assinatura(object): @@ -323,3 +327,49 @@ class AssinaturaA1(Assinatura): return xml except Exception as e: raise e + + +class AssinaturaA1SignXML(Assinatura): + + def __init__(self, certificado, senha): + self.key, self.cert = CertificadoA1(certificado).separar_arquivo(senha) + + def assinar(self, xml, retorna_string=False): + if len(xml.nsmap.items()) == 0: # não tem namespace + reference = xml.find('infNFe').attrib['Id'] + else: + ns = {'ns': 'http://www.portalfiscal.inf.br/nfe'} + reference = xml.find('ns:infNFe', namespaces=ns).attrib['Id'] + + # retira acentos + xml_str = remover_acentos(etree.tostring(xml, encoding="unicode", pretty_print=False)) + xml = etree.fromstring(xml_str) + + signer = XMLSigner( + method=signxml.methods.enveloped, signature_algorithm="rsa-sha1", + digest_algorithm='sha1', + c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315') + + ns = {None: signer.namespaces['ds']} + signer.namespaces = ns + + ref_uri = ('#%s' % reference) if reference else None + signed_root = signer.sign( + xml, key=self.key, cert=self.cert, reference_uri=ref_uri) + + ns = {'ns': NAMESPACE_SIG} + if reference: + element_signed = signed_root.find(".//*[@Id='%s']" % reference) + signature = signed_root.find(".//ns:Signature", namespaces=ns) + + if element_signed is not None and signature is not None: + parent = element_signed.getparent() + parent.append(signature) + + # coloca o certificado na tag X509Data/X509Certificate + tagX509Data = signed_root.find('.//ns:X509Data', namespaces=ns) + etree.SubElement(tagX509Data, 'X509Certificate').text = self.cert + if retorna_string: + return etree.tostring(signed_root, encoding="unicode", pretty_print=False) + else: + return signed_root diff --git a/pynfe/utils/__init__.py b/pynfe/utils/__init__.py index 7259a5c..2aeeab6 100644 --- a/pynfe/utils/__init__.py +++ b/pynfe/utils/__init__.py @@ -9,10 +9,7 @@ try: except ImportError: raise Exception('Falhou ao importar lxml/ElementTree') -try: - from StringIO import StringIO -except ImportError: - from io import StringIO +from io import StringIO try: from . import flags @@ -148,4 +145,4 @@ def obter_uf_por_codigo(codigo_uf): def remover_acentos(txt): - return normalize('NFKD', txt).encode('ASCII','ignore').decode('ASCII') \ No newline at end of file + return normalize('NFKD', txt).encode('ASCII','ignore').decode('ASCII') From 0615b5ce57a32e14a8d2d8c5b413233ad67be5c1 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 31 May 2017 16:03:29 -0300 Subject: [PATCH 2/3] assinatura SignXML busca tag atributo Id --- pynfe/processamento/assinatura.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index 4ec6261..a2f74a5 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -335,11 +335,8 @@ class AssinaturaA1SignXML(Assinatura): self.key, self.cert = CertificadoA1(certificado).separar_arquivo(senha) def assinar(self, xml, retorna_string=False): - if len(xml.nsmap.items()) == 0: # não tem namespace - reference = xml.find('infNFe').attrib['Id'] - else: - ns = {'ns': 'http://www.portalfiscal.inf.br/nfe'} - reference = xml.find('ns:infNFe', namespaces=ns).attrib['Id'] + # busca tag que tem id(reference_uri), logo nao importa se tem namespace + reference = xml.find(".//*[@Id]").attrib['Id'] # retira acentos xml_str = remover_acentos(etree.tostring(xml, encoding="unicode", pretty_print=False)) @@ -358,14 +355,6 @@ class AssinaturaA1SignXML(Assinatura): xml, key=self.key, cert=self.cert, reference_uri=ref_uri) ns = {'ns': NAMESPACE_SIG} - if reference: - element_signed = signed_root.find(".//*[@Id='%s']" % reference) - signature = signed_root.find(".//ns:Signature", namespaces=ns) - - if element_signed is not None and signature is not None: - parent = element_signed.getparent() - parent.append(signature) - # coloca o certificado na tag X509Data/X509Certificate tagX509Data = signed_root.find('.//ns:X509Data', namespaces=ns) etree.SubElement(tagX509Data, 'X509Certificate').text = self.cert From 948ab3798242b7ca9dd6679a2bfa5a854639ba82 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 31 May 2017 16:30:21 -0300 Subject: [PATCH 3/3] =?UTF-8?q?inutiliza=C3=A7=C3=A3o=20agora=20assina=20c?= =?UTF-8?q?om=20SignXML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/comunicacao.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pynfe/processamento/comunicacao.py b/pynfe/processamento/comunicacao.py index 20870bf..485de1d 100644 --- a/pynfe/processamento/comunicacao.py +++ b/pynfe/processamento/comunicacao.py @@ -6,7 +6,7 @@ from pynfe.utils import etree, so_numeros from pynfe.utils.flags import NAMESPACE_NFE, NAMESPACE_SOAP, NAMESPACE_XSI, NAMESPACE_XSD, NAMESPACE_METODO, \ VERSAO_PADRAO, CODIGOS_ESTADOS, NAMESPACE_BETHA from pynfe.utils.webservices import NFCE, NFE, NFSE -from .assinatura import AssinaturaA1 +from .assinatura import AssinaturaA1, AssinaturaA1SignXML from pynfe.entidades.certificado import CertificadoA1 @@ -205,7 +205,7 @@ class ComunicacaoSefaz(Comunicacao): return self._post(url, xml) def download(self, cnpj, chave): - """ + """ Metodo para download de NFe por parte de destinatário. O certificado digital deve ser o mesmo do destinatário da Nfe. NT 2012/002 @@ -221,7 +221,7 @@ class ComunicacaoSefaz(Comunicacao): # Monta XML para envio da requisição xml = self._construir_xml_status_pr(cabecalho=self._cabecalho_soap(metodo='NfeDownloadNF'), metodo='NfeDownloadNF', dados=raiz) - + return self._post(url, xml) def inutilizacao(self, modelo, cnpj, numero_inicial, numero_final, justificativa='', ano=None, serie='1'): @@ -260,7 +260,7 @@ class ComunicacaoSefaz(Comunicacao): etree.SubElement(inf_inut, 'xJust').text = justificativa # assinatura - a1 = AssinaturaA1(self.certificado, self.certificado_senha) + a1 = AssinaturaA1SignXML(self.certificado, self.certificado_senha) xml = a1.assinar(raiz) # Monta XML para envio da requisição @@ -355,7 +355,7 @@ class ComunicacaoSefaz(Comunicacao): etree.SubElement(raiz, 'versaoDados').text = '1.01' elif metodo == 'NfeDownloadNF': etree.SubElement(raiz, 'versaoDados').text = '1.00' - elif metodo == 'CadConsultaCadastro2': + elif metodo == 'CadConsultaCadastro2': etree.SubElement(raiz, 'versaoDados').text = '2.00' else: etree.SubElement(raiz, 'versaoDados').text = VERSAO_PADRAO @@ -399,7 +399,7 @@ class ComunicacaoSefaz(Comunicacao): try: xml_declaration='' # limpa xml com caracteres bugados para infNFeSupl em NFC-e - xml = re.sub('(.*?)', + xml = re.sub('(.*?)', lambda x:x.group(0).replace('<','<').replace('>','>').replace('amp;',''), etree.tostring(xml, encoding='unicode').replace('\n','')) xml = xml_declaration + xml @@ -517,7 +517,7 @@ class ComunicacaoNfse(Comunicacao): return self._post(url, xml, 'cancelar') # Ginfes elif self.autorizador == 'GINFES': - # comunica via wsdl com certificado + # comunica via wsdl com certificado return self._post_https(url, xml, 'cancelar') # TODO outros autorizadres else: