import base64 import hashlib from copy import deepcopy 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 = base64.b64encode(lib.digest())