# -*- coding: utf-8 -*- # Copyright 2021 Joan Marín # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import hashlib from os import path from uuid import uuid4 from base64 import b64encode, b64decode from io import BytesIO from datetime import datetime, timedelta import OpenSSL.crypto as crypto from lxml import etree from jinja2 import Environment, FileSystemLoader from pgxades import XAdESContext, PolicyId, template import xmlsig from qrcode import QRCode, constants from odoo import _ from odoo.exceptions import ValidationError def get_software_security_code(IdSoftware, Pin, NroDocumentos): uncoded_value = (IdSoftware + ' + ' + Pin + ' + ' + NroDocumentos) software_security_code = IdSoftware + Pin + NroDocumentos software_security_code = hashlib.sha384(software_security_code.encode('utf-8')) return { 'SoftwareSecurityCodeUncoded': uncoded_value, 'SoftwareSecurityCode': software_security_code.hexdigest()} def get_cufe_cude( NumFac, FecFac, HorFac, ValFac, CodImp1, ValImp1, CodImp2, ValImp2, CodImp3, ValImp3, ValTot, NitOFE, DocAdq, ClTec, SoftwarePIN, TipoAmbie): # CUFE = SHA-384(NumFac + FecFac + HorFac + ValFac + CodImp1 + ValImp1 + # CodImp2 + ValImp2 + CodImp3 + ValImp3 + ValTot + NitOFE + DocAdq + # ClTec + TipoAmbie) # CUDE = SHA-384(NumFac + FecFac + HorFac + ValFac + CodImp1 + ValImp1 + # CodImp2 + ValImp2 + CodImp3 + ValImp3 + ValTot + NitOFE + DocAdq + # Software-PIN + TipoAmbie) uncoded_value = (NumFac + ' + ' + FecFac + ' + ' + HorFac + ' + ' + ValFac + ' + ' + CodImp1 + ' + ' + ValImp1 + ' + ' + CodImp2 + ' + ' + ValImp2 + ' + ' + CodImp3 + ' + ' + ValImp3 + ' + ' + ValTot + ' + ' + NitOFE + ' + ' + DocAdq + ' + ' + (ClTec if ClTec else SoftwarePIN) + ' + ' + TipoAmbie) CUFE_CUDE = ( NumFac + FecFac + HorFac + ValFac + CodImp1 + ValImp1 + CodImp2 + ValImp2 + CodImp3 + ValImp3 + ValTot + NitOFE + DocAdq + (ClTec if ClTec else SoftwarePIN) + TipoAmbie) CUFE_CUDE = hashlib.sha384(CUFE_CUDE.encode('utf-8')) return { 'CUFE/CUDEUncoded': uncoded_value, 'CUFE/CUDE': CUFE_CUDE.hexdigest()} # https://stackoverflow.com/questions/38432809/dynamic-xml-template-generation-using-get-template-jinja2 def get_template_xml(values, template_name): base_path = path.dirname(path.dirname(__file__)) env = Environment(loader=FileSystemLoader(path.join(base_path, 'templates'))) template_xml = env.get_template('{}.xml'.format(template_name)) xml = template_xml.render(values) return xml.replace('&', '&').encode('utf-8') def get_pkcs12(certificate_file, certificate_password): msg = _("The certificate password or certificate file is not valid.\n\n" "Exception: %s") try: return crypto.load_pkcs12(b64decode(certificate_file), certificate_password) except Exception as e: raise ValidationError(msg % e) # https://www.decalage.info/en/python/lxml-c14n def get_xml_with_c14n(xml): if not isinstance(xml, etree._Element): xml = etree.fromstring(xml.encode('utf-8')) out = BytesIO() xml.getroottree().write_c14n(out) value = b64encode(out.getvalue()).decode("utf-8") out.close() return value # https://github.com/etobella/python-xades def get_xml_with_signature( xml_without_signature, signature_policy_url, signature_policy_file, signature_policy_description, certificate_file, certificate_password): # https://lxml.de/tutorial.html # root = etree.fromstring(response.content) # root = etree.tostring(root, encoding='utf-8') # parser = etree.XMLParser(encoding='utf-8', remove_blank_text=True) ds = "http://www.w3.org/2000/09/xmldsig#" ext = "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" parser = etree.XMLParser(remove_comments=True, remove_blank_text=True) root = etree.fromstring(xml_without_signature, parser=parser) signature_id = "xmldsig-{}".format(uuid4()) signature = xmlsig.template.create( xmlsig.constants.TransformInclC14N, xmlsig.constants.TransformRsaSha512, signature_id) # Complememto para añadir atributo faltante for element in root.iter("{%s}SignatureValue" % ds): element.attrib['Id'] = signature_id + "-sigvalue" ref = xmlsig.template.add_reference( signature, xmlsig.constants.TransformSha512, uri="", name=signature_id + "-ref0") xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped) sp = xmlsig.template.add_reference( signature, xmlsig.constants.TransformSha512, uri="#" + signature_id + "-signedprops", uri_type="http://uri.etsi.org/01903#SignedProperties") xmlsig.template.add_transform(sp, xmlsig.constants.TransformInclC14N) xmlsig.template.add_reference( signature, xmlsig.constants.TransformSha512, uri="#" + signature_id + "-keyinfo") ki = xmlsig.template.ensure_key_info(signature, name=signature_id + "-keyinfo") data = xmlsig.template.add_x509_data(ki) xmlsig.template.x509_data_add_certificate(data) xmlsig.template.x509_data_add_subject_name(data) serial = xmlsig.template.x509_data_add_issuer_serial(data) xmlsig.template.x509_issuer_serial_add_issuer_name(serial) xmlsig.template.x509_issuer_serial_add_serial_number(serial) xmlsig.template.add_key_value(ki) qualifying = template.create_qualifying_properties(signature) props = template.create_signed_properties( qualifying, name=signature_id + "-signedprops") template.add_claimed_role(props, "supplier") policy = PolicyId() policy.id = signature_policy_url policy.name = signature_policy_description policy.remote = b64decode(signature_policy_file) policy.hash_method = xmlsig.constants.TransformSha512 ctx = XAdESContext(policy) ctx.load_pkcs12(get_pkcs12(certificate_file, certificate_password)) root.append(signature) ctx.sign(signature) ctx.verify(signature) root.remove(signature) position = 0 for element in root.iter("{%s}ExtensionContent" % ext): if position == 1: element.append(signature) position += 1 return get_xml_with_c14n(root) def get_xml_soap_values(certificate_file, certificate_password): Created = datetime.utcnow() Expires = Created + timedelta(seconds=60000) Created = Created.strftime("%Y-%m-%dT%H:%M:%S.001Z") Expires = Expires.strftime("%Y-%m-%dT%H:%M:%S.001Z") # https://github.com/mit-dig/idm/blob/master/idm_query_functions.py#L151 pkcs12 = get_pkcs12(certificate_file, certificate_password) cert = pkcs12.get_certificate() der = b64encode(crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)).decode('utf-8') return { 'Created': Created, 'Expires': Expires, 'Id': uuid4(), 'BinarySecurityToken': der} def get_xml_soap_with_signature( xml_soap_without_signature, Id, certificate_file, certificate_password): wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" X509v3 = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" parser = etree.XMLParser(remove_comments=True, remove_blank_text=True) root = etree.fromstring(xml_soap_without_signature, parser=parser) signature_id = "{}".format(Id) signature = xmlsig.template.create( xmlsig.constants.TransformExclC14N, xmlsig.constants.TransformRsaSha256, # solo me ha funcionado con esta "SIG-" + signature_id) ref = xmlsig.template.add_reference( signature, xmlsig.constants.TransformSha256, uri="#id-" + signature_id) xmlsig.template.add_transform(ref, xmlsig.constants.TransformExclC14N) ki = xmlsig.template.ensure_key_info(signature, name="KI-" + signature_id) ctx = xmlsig.SignatureContext() ctx.load_pkcs12(get_pkcs12(certificate_file, certificate_password)) for element in root.iter("{%s}Security" % wsse): element.append(signature) ki_str = etree.SubElement(ki, "{%s}SecurityTokenReference" % wsse) ki_str.attrib["{%s}Id" % wsu] = "STR-" + signature_id ki_str_reference = etree.SubElement(ki_str, "{%s}Reference" % wsse) ki_str_reference.attrib['URI'] = "#X509-" + signature_id ki_str_reference.attrib['ValueType'] = X509v3 ctx.sign(signature) ctx.verify(signature) return root def get_qr_image(data): qr_code = QRCode( version=1, error_correction=constants.ERROR_CORRECT_L, box_size=20, border=4) qr_code.add_data(data) qr_code.make(fit=True) image = qr_code.make_image() temp = BytesIO() image.save(temp, format="PNG") qr_image = b64encode(temp.getvalue()).decode('utf-8') temp.close() return qr_image