From 602fda7521634ff2b7161b20dc627c919622d548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marinho=20Brand=C3=A3o?= Date: Sun, 17 Jan 2010 09:59:10 -0200 Subject: [PATCH] Trabalhando na assinatura --- pynfe/processamento/assinatura.py | 142 ++++++++++++++++++++++++- tests/01-basico.txt | 4 +- tests/03-processamento-01-serializacao-xml.txt | 6 ++ tests/03-processamento-03-assinatura.txt | 18 ++-- 4 files changed, 158 insertions(+), 12 deletions(-) diff --git a/pynfe/processamento/assinatura.py b/pynfe/processamento/assinatura.py index eae7519..74fc3b6 100644 --- a/pynfe/processamento/assinatura.py +++ b/pynfe/processamento/assinatura.py @@ -1,5 +1,38 @@ # -*- coding: utf-8 -*- +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +try: + from lxml import etree +except ImportError: + try: + # Python 2.5 - cElementTree + import xml.etree.cElementTree as etree + except ImportError: + try: + # Python 2.5 - ElementTree + import xml.etree.ElementTree as etree + except ImportError: + try: + # Instalacao normal do cElementTree + import cElementTree as etree + except ImportError: + try: + # Instalacao normal do ElementTree + import elementtree.ElementTree as etree + except ImportError: + raise Exception('Falhou ao importar lxml/ElementTree') + +import xmlsec, libxml2 # FIXME: verificar ambiguidade de dependencias: lxml e libxml2 + +from geraldo.utils import memoize + +NAMESPACE_NFE = u'http://www.portalfiscal.inf.br/nfe' +NAMESPACE_SIG = u'http://www.w3.org/2000/09/xmldsig#' + class Assinatura(object): """Classe abstrata responsavel por definir os metodos e logica das classes de assinatura digital.""" @@ -11,7 +44,7 @@ class Assinatura(object): self.certificado = certificado self.senha = senha - def assinar_arquivos(self, caminho_raiz): + def assinar_arquivo(self, caminho_arquivo): """Efetua a assinatura dos arquivos XML informados""" pass @@ -33,7 +66,7 @@ class Assinatura(object): """Efetua a assinatura em instancias do PyNFe""" pass - def verificar_arquivos(self, caminho_raiz): + def verificar_arquivo(self, caminho_arquivo): pass def verificar_xml(self, xml): @@ -45,9 +78,112 @@ class Assinatura(object): def verificar_objetos(self, objetos): pass + +@memoize +def extrair_tag(root): + return root.tag.split('}')[-1] + class AssinaturaA1(Assinatura): """Classe abstrata responsavel por efetuar a assinatura do certificado digital no XML informado.""" - pass + def assinar_arquivo(self, caminho_arquivo): + # Carrega o XML do arquivo + raiz = etree.parse(caminho_arquivo) + return self.assinar_etree(raiz) + + def assinar_xml(self, xml): + raiz = etree.parse(StringIO(xml)) + return self.assinar_etree(raiz) + + def assinar_etree(self, raiz): + # Extrai a tag do elemento raiz + tipo = extrair_tag(raiz.getroot()) + + # doctype compatível com o tipo da tag raiz + if tipo == u'NFe': + doctype = u']>' + elif tipo == u'inutNFe': + doctype = u']>' + elif tipo == u'cancNFe': + doctype = u']>' + elif tipo == u'DPEC': + doctype = u']>' + + # Tag de assinatura + if raiz.getroot().find('Signature') is None: + signature = etree.Element( + 'Signature', + URI=raiz.getroot().getchildren()[0].attrib['Id'], + xmlns=NAMESPACE_SIG, + ) + signature.text = '' + raiz.getroot().insert(0, signature) + + # Acrescenta a tag de doctype (como o lxml nao suporta alteracao do doctype, + # converte para string para faze-lo) + xml = etree.tostring(raiz, xml_declaration=True, encoding='utf-8') + pos = xml.find('>') + 1 + xml = xml[:pos] + doctype + xml[pos:] + raiz = etree.parse(StringIO(xml)) + + # Ativa funções criptográficas + self._ativa_funcoes_criptograficas() + + # Colocamos o texto no avaliador XML + #doc_xml = libxml2.parseMemory(xml, len(xml)) + + # Cria o contexto para manipulação do XML via sintaxe XPATH + #ctxt = doc_xml.xpathNewContext() + #ctxt.xpathRegisterNs(u'sig', NAMESPACE_SIG) + + # Separa o nó da assinatura + #noh_assinatura = ctxt.xpathEval(u'//*/sig:Signature')[0] + + # Buscamos a chave no arquivo do certificado + chave = xmlsec.cryptoAppKeyLoad( + filename=str(self.certificado.caminho_arquivo), + format=xmlsec.KeyDataFormatPkcs12, + pwd=str(self.senha), + pwdCallback=None, + pwdCallbackCtx=None, + ) + + # Cria a variável de chamada (callable) da função de assinatura + assinador = xmlsec.DSigCtx() + + # Atribui a chave ao assinador + assinador.signKey = chave + + # Desativa funções criptográficas + self._desativa_funcoes_criptograficas() + + #print etree.tostring(raiz, pretty_print=True, xml_declaration=True, encoding='utf-8') + + def _ativa_funcoes_criptograficas(self): + # Ativa as funções de análise de arquivos XML FIXME + libxml2.initParser() + libxml2.substituteEntitiesDefault(1) + + # Ativa as funções da API de criptografia + xmlsec.init() + xmlsec.cryptoAppInit(None) + xmlsec.cryptoInit() + + def _desativa_funcoes_criptograficas(self): + ''' Desativa as funções criptográficas e de análise XML + As funções devem ser chamadas aproximadamente na ordem inversa da ativação + ''' + + # Shutdown xmlsec-crypto library + xmlsec.cryptoShutdown() + + # Shutdown crypto library + xmlsec.cryptoAppShutdown() + + # Shutdown xmlsec library + xmlsec.shutdown() + + # Shutdown LibXML2 FIXME + libxml2.cleanupParser() diff --git a/tests/01-basico.txt b/tests/01-basico.txt index e2aee8f..d597861 100644 --- a/tests/01-basico.txt +++ b/tests/01-basico.txt @@ -55,12 +55,12 @@ modelo: | ---------------------- | consultar_cadastro() | | | | Validacao | | inutilizar_faixa_numeracao() | | | ---------------------- -------------------------------- | - | | validar_arquivos() | | + | | validar_arquivo() | | | | validar_xml() | | | | validar_etree() | ---------------------- | | | validar_objetos() | | Assinatura | | | ---------------------- ---------------------- | - | | assinar_arquivos() | | + | | assinar_arquivo() | | | | assinar_xml() | | | | assinar_etree() | | | | assinar_objetos() | | diff --git a/tests/03-processamento-01-serializacao-xml.txt b/tests/03-processamento-01-serializacao-xml.txt index 6665c44..dcd4a02 100644 --- a/tests/03-processamento-01-serializacao-xml.txt +++ b/tests/03-processamento-01-serializacao-xml.txt @@ -497,6 +497,12 @@ Exportacao completa >>> xml = serializador.exportar(modelo=55) + >>> from lxml import etree + + >>> fp = file('tests/saida/nfe-1.xml', 'w') + >>> fp.write(etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding='utf-8')) + >>> fp.close() + - Quando gerados me lote, apenas o primeiro arquivo deve ter o cabecalho padrao do XML 1.0 - diff --git a/tests/03-processamento-03-assinatura.txt b/tests/03-processamento-03-assinatura.txt index 1112936..510f2d5 100644 --- a/tests/03-processamento-03-assinatura.txt +++ b/tests/03-processamento-03-assinatura.txt @@ -6,7 +6,7 @@ Carregando Certificado Digital tipo A1 >>> from pynfe.entidades import CertificadoA1 - >>> certificado = CertificadoA1(caminho_arquivo='tests/certificado.pfx') + >>> certificado = CertificadoA1(caminho_arquivo='tests/certificado.pem') Assinando NF-e -------------- @@ -24,24 +24,28 @@ A assinatura deve ser feita em quatro tipos diferentes de origem do XML: - Arquivos - >>> hasattr(AssinaturaA1, 'assinar_arquivos') + >>> assinatura.assinar_arquivo('tests/saida/nfe-1.xml') True - String de XML - >>> hasattr(AssinaturaA1, 'assinar_xml') + >>> hasattr(assinatura, 'assinar_xml') True -- Instancia de lxml.etree +- Instancias do PyNFe - >>> hasattr(AssinaturaA1, 'assinar_etree') + >>> hasattr(assinatura, 'assinar_objetos') True -- Instancias do PyNFe +- Instancia de lxml.etree - >>> hasattr(AssinaturaA1, 'assinar_objetos') + >>> hasattr(assinatura, 'assinar_etree') True - Utilizar pyXMLSec para isso - verificar qual eh a integracao do PyXMLSec com o lxml.etree +Validando assinatura +-------------------- + +