Odoo18-Base/addons/l10n_es_edi_facturae/xml_utils.py
2025-03-10 10:52:11 +07:00

88 lines
3.6 KiB
Python

import base64
import hashlib
from base64 import b64encode
from copy import deepcopy
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from lxml import etree
from odoo.exceptions import UserError
NS_MAP = {'ds': "http://www.w3.org/2000/09/xmldsig#"}
def _canonicalize_node(node):
"""
Returns the canonical (C14N 1.0, without comments, non exclusive) representation of node.
Speficied in: https://www.w3.org/TR/2001/REC-xml-c14n-20010315
Required for computing digests and signatures.
Returns an UTF-8 encoded bytes string.
"""
return etree.tostring(node, method="c14n", with_comments=False, exclusive=False)
def _get_uri(uri, reference, base_uri=""):
"""
Returns the content within `reference` that is identified by `uri`.
Canonicalization is used to convert node reference to an octet stream.
- URIs starting with # are same-document references
https://www.w3.org/TR/xmldsig-core/#sec-URI
- Empty URIs point to the whole document tree, without the signature
https://www.w3.org/TR/xmldsig-core/#sec-EnvelopedSignature
Returns an UTF-8 encoded bytes string.
"""
node = deepcopy(reference.getroottree().getroot())
if uri == base_uri:
# Base URI: whole document, without signature (default is empty URI)
for signature in node.xpath('ds:Signature', namespaces=NS_MAP):
if signature.tail:
# move the tail to the previous node or to the parent
if (previous := signature.getprevious()) is not None:
previous.tail = "".join([previous.tail or "", signature.tail or ""])
else:
signature.getparent().text = "".join([signature.getparent().text or "", signature.tail or ""])
node.remove(signature)
return _canonicalize_node(node)
if uri.startswith("#"):
path = "//*[@*[local-name() = '{}' ]=$uri]"
results = node.xpath(path.format("Id"), uri=uri.lstrip("#")) # case-sensitive 'Id'
if len(results) == 1:
return _canonicalize_node(results[0])
if len(results) > 1:
raise UserError(f"Ambiguous reference URI {uri} resolved to {len(results)} nodes")
raise UserError(f'URI {uri} not found')
def _reference_digests(node, base_uri=""):
"""
Processes the references from node and computes their digest values as specified in
https://www.w3.org/TR/xmldsig-core/#sec-DigestMethod
https://www.w3.org/TR/xmldsig-core/#sec-DigestValue
"""
for reference in node.findall("ds:Reference", namespaces=NS_MAP):
ref_node = _get_uri(reference.get("URI", ""), reference, base_uri=base_uri)
lib = hashlib.new("sha256", ref_node)
reference.find("ds:DigestValue", namespaces=NS_MAP).text = b64encode(lib.digest())
def _fill_signature(node, private_key):
"""
Uses private_key to sign the SignedInfo sub-node of `node`, as specified in:
https://www.w3.org/TR/xmldsig-core/#sec-SignatureValue
https://www.w3.org/TR/xmldsig-core/#sec-SignedInfo
"""
signed_info_xml = node.find("ds:SignedInfo", namespaces=NS_MAP)
# During signature generation, the digest is computed over the canonical form of the document
signature = private_key.sign(_canonicalize_node(signed_info_xml), padding.PKCS1v15(), hashes.SHA256())
node.find("ds:SignatureValue", namespaces=NS_MAP).text = base64.encodebytes(signature)
def _int_to_bytes(number):
""" Converts an integer to a byte string (in smallest big-endian form). """
return number.to_bytes((number.bit_length() + 7) // 8, byteorder='big')