facho: clip.py se adiciona nuevo comand 'sign-xml'.

* facho/cli.py: adiciona nuevo comando 'sign-xml' para firmar
directamente un xml.
* facho/fe/fe.py (DianXMLExtensionSigner.sign_xml_string): Nuevo metodo.

FossilOrigin-Name: 61920c40da14a134de6392845b3e4d98ad2b1b683093038d6161c147669127e9
This commit is contained in:
bit4bit@riseup.net 2020-09-06 16:24:10 +00:00
parent 643191a615
commit 153d577100
4 changed files with 52 additions and 42 deletions

View File

@ -29,6 +29,15 @@ logging.config.dictConfig({
} }
}) })
def disable_ssl():
# MACHETE
import ssl
if getattr(ssl, '_create_unverified_context', None):
ssl._create_default_https_context = ssl._create_unverified_context
warnings.warn("be sure!! ssl disable")
else:
warnings.warn("can't disable ssl")
# MACHETE se corrige # MACHETE se corrige
# lxml.etree.DocumentInvalid: Element '{http://www.w3.org/2000/09/xmldsig#}X509SerialNumber': '34255301462796514282327995225552892834' is not a valid value of the atomic type 'xs:integer'. # lxml.etree.DocumentInvalid: Element '{http://www.w3.org/2000/09/xmldsig#}X509SerialNumber': '34255301462796514282327995225552892834' is not a valid value of the atomic type 'xs:integer'.
@ -200,6 +209,20 @@ def validate_invoice(invoice_path):
XSD.validate(content, XSD.UBLInvoice) XSD.validate(content, XSD.UBLInvoice)
@click.command()
@click.option('--private-key', type=click.Path(exists=True))
@click.option('--passphrase')
@click.option('--ssl/--no-ssl', default=False)
@click.argument('xmlfile', type=click.Path(exists=True), required=True)
def sign_xml(private_key, passphrase, xmlfile, ssl=True):
if not ssl:
disable_ssl()
from facho import fe
signer = fe.DianXMLExtensionSigner(private_key, passphrase=passphrase)
document = open(xmlfile, 'r').read().encode('utf-8')
print(signer.sign_xml_string(document))
@click.command() @click.command()
@click.option('--private-key', type=click.Path(exists=True)) @click.option('--private-key', type=click.Path(exists=True))
@click.option('--generate/--validate', default=False) @click.option('--generate/--validate', default=False)
@ -214,15 +237,9 @@ def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=Tr
def extensions(form.Invoice): -> List[facho.FachoXMLExtension] def extensions(form.Invoice): -> List[facho.FachoXMLExtension]
""" """
# MACHETE
if not ssl: if not ssl:
import ssl disable_ssl()
if getattr(ssl, '_create_unverified_context', None):
ssl._create_default_https_context = ssl._create_unverified_context
warnings.warn("be sure!! ssl disable")
else:
warnings.warn("can't disable ssl")
import importlib.util import importlib.util
spec = importlib.util.spec_from_file_location('invoice', scriptname) spec = importlib.util.spec_from_file_location('invoice', scriptname)
@ -245,11 +262,6 @@ def generate_invoice(private_key, passphrase, scriptname, generate=False, ssl=Tr
extensions = module.extensions(invoice) extensions = module.extensions(invoice)
for extension in extensions: for extension in extensions:
xml.add_extension(extension) xml.add_extension(extension)
if private_key:
signer = fe.DianXMLExtensionSigner(private_key, passphrase=passphrase)
xml.add_extension(signer)
print(xml.tostring(xml_declaration=True)) print(xml.tostring(xml_declaration=True))
@ -267,3 +279,4 @@ main.add_command(soap_get_status_zip)
main.add_command(soap_get_numbering_range) main.add_command(soap_get_numbering_range)
main.add_command(generate_invoice) main.add_command(generate_invoice)
main.add_command(validate_invoice) main.add_command(validate_invoice)
main.add_command(sign_xml)

View File

@ -98,6 +98,7 @@ class LXMLBuilder:
def set_attribute(self, elem, key, value): def set_attribute(self, elem, key, value):
elem.attrib[key] = value elem.attrib[key] = value
@classmethod
def tostring(self, elem, **attrs): def tostring(self, elem, **attrs):
attrs['pretty_print'] = attrs.pop('pretty_print', False) attrs['pretty_print'] = attrs.pop('pretty_print', False)
attrs['encoding'] = attrs.pop('encoding', 'UTF-8') attrs['encoding'] = attrs.pop('encoding', 'UTF-8')

View File

@ -1,7 +1,7 @@
# This file is part of facho. The COPYRIGHT file at the top level of # This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from ..facho import FachoXML, FachoXMLExtension from ..facho import FachoXML, FachoXMLExtension, LXMLBuilder
import xmlsig import xmlsig
import xades import xades
from datetime import datetime from datetime import datetime
@ -187,8 +187,9 @@ class DianXMLExtensionSigner(FachoXMLExtension):
def from_pkcs12(self, filepath, password=None): def from_pkcs12(self, filepath, password=None):
p12 = OpenSSL.crypto.load_pkcs12(open(filepath, 'rb').read(), password) p12 = OpenSSL.crypto.load_pkcs12(open(filepath, 'rb').read(), password)
# return (xpath, xml.Element) def sign_xml_string(self, document):
def build(self, fachoxml): xml = LXMLBuilder.from_string(document)
signature = xmlsig.template.create( signature = xmlsig.template.create(
xmlsig.constants.TransformInclC14N, xmlsig.constants.TransformInclC14N,
xmlsig.constants.TransformRsaSha256, xmlsig.constants.TransformRsaSha256,
@ -217,31 +218,8 @@ class DianXMLExtensionSigner(FachoXMLExtension):
# TODO assert with http://www.sic.gov.co/hora-legal-colombiana # TODO assert with http://www.sic.gov.co/hora-legal-colombiana
props = xades.template.create_signed_properties(qualifying, datetime=datetime.now()) props = xades.template.create_signed_properties(qualifying, datetime=datetime.now())
xades.template.add_claimed_role(props, "supplier") xades.template.add_claimed_role(props, "supplier")
#signed_do = xades.template.ensure_signed_data_object_properties(props)
#xades.template.add_data_object_format(
# signed_do, "#R1",
# identifier=xades.ObjectIdentifier("Idenfitier0", "Description")
#)
#xades.template.add_commitment_type_indication(
# signed_do,
# xades.ObjectIdentifier("Idenfitier0", "Description"),
# qualifiers_type=["Tipo"],
#)
#xades.template.add_commitment_type_indication( xml.append(signature)
# signed_do,
# xades.ObjectIdentifier("Idenfitier1", references=["#R1"]),
# references=["#R1"],
#)
#xades.template.add_data_object_format(
# signed_do,
# "#RKI",
# description="Desc",
# mime_type="application/xml",
# encoding="UTF-8",
#)
fachoxml.root.append(signature)
policy = xades.policy.GenericPolicyId( policy = xades.policy.GenericPolicyId(
self.POLICY_ID, self.POLICY_ID,
@ -254,12 +232,20 @@ class DianXMLExtensionSigner(FachoXMLExtension):
ctx.sign(signature) ctx.sign(signature)
ctx.verify(signature) ctx.verify(signature)
#xmlsig take parent root #xmlsig take parent root
fachoxml.root.remove(signature) xml.remove(signature)
fachoxml = FachoXML(xml,nsmap=NAMESPACES)
ublextension = fachoxml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append_not_exists=True) ublextension = fachoxml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append_not_exists=True)
extcontent = ublextension.find_or_create_element('/ext:UBLExtension:/ext:ExtensionContent') extcontent = ublextension.find_or_create_element('/ext:UBLExtension:/ext:ExtensionContent')
fachoxml.append_element(extcontent, signature) fachoxml.append_element(extcontent, signature)
return fachoxml.tostring()
# return (xpath, xml.Element)
def build(self, fachoxml):
xmlsigned = self.sign_xml_string(fachoxml.tostring())
xml = LXMLBuilder.from_string(xmlsigned)
fachoxml.root = xml
return fachoxml
class DianXMLExtensionAuthorizationProvider(FachoXMLExtension): class DianXMLExtensionAuthorizationProvider(FachoXMLExtension):

View File

@ -94,5 +94,15 @@ def test_dian_invoice_with_fe():
assert "<Invoice" in xml.tostring() assert "<Invoice" in xml.tostring()
def test_xml_sign_dian(monkeypatch):
xml = fe.FeXML('Invoice',
'http://www.dian.gov.co/contratos/facturaelectronica/v1')
xmlstring = xml.tostring()
signer = fe.DianXMLExtensionSigner('./tests/example.p12')
with monkeypatch.context() as m:
helpers.mock_urlopen(m)
xmlsigned = signer.sign_xml_string(xmlstring)
assert "Signature" in xmlsigned