1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_se_sie_import/xml_utils.py
2024-12-10 09:04:09 +07:00

174 lines
8.8 KiB
Python

import base64
import binascii
import hashlib
import hmac
from copy import deepcopy
from lxml import etree
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from odoo.tools import consteq
XMLSIG_NSMAP = {"ds": "http://www.w3.org/2000/09/xmldsig#"}
def _c14n_method_constructor(with_comments=False, exclusive=False):
def _c14n_method(node):
return etree.tostring(node, method="c14n", with_comments=with_comments, exclusive=exclusive)
return _c14n_method
def _rsa_method_constructor(digest_method):
def _rsa_method(data, signature, key):
return key.verify(base64.b64decode(signature), data, padding.PKCS1v15(), digest_method())
return _rsa_method
def _hmac_method_constructor(digest_method):
def _hmac_method(data, signature, key):
return consteq(hmac.HMAC(key, data, digest_method()), base64.b64decode(signature))
return _hmac_method
def _hash_method_constructor(digest_method):
def _hash_method(data):
return digest_method(data).digest()
return _hash_method
C14N_TRANSFORMS_METHODS = {
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315": _c14n_method_constructor(),
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments": _c14n_method_constructor(with_comments=True),
"http://www.w3.org/2001/10/xml-exc-c14n#": _c14n_method_constructor(exclusive=True),
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments": _c14n_method_constructor(with_comments=True, exclusive=True),
}
DIGEST_METHODS = {
"http://www.w3.org/2001/04/xmldsig-more#md5": _hash_method_constructor(hashlib.md5),
"http://www.w3.org/2000/09/xmldsig#sha1": _hash_method_constructor(hashlib.sha1),
"http://www.w3.org/2001/04/xmldsig-more#sha224": _hash_method_constructor(hashlib.sha224),
"http://www.w3.org/2001/04/xmlenc#sha256": _hash_method_constructor(hashlib.sha256),
"http://www.w3.org/2001/04/xmldsig-more#sha384": _hash_method_constructor(hashlib.sha384),
"http://www.w3.org/2001/04/xmlenc#sha512": _hash_method_constructor(hashlib.sha384),
}
SIGNATURE_METHODS = {
"http://www.w3.org/2001/04/xmldsig-more#rsa-md5": _rsa_method_constructor(hashes.MD5),
"http://www.w3.org/2000/09/xmldsig#rsa-sha1": _rsa_method_constructor(hashes.SHA1),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha224": _rsa_method_constructor(hashes.SHA224),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": _rsa_method_constructor(hashes.SHA256),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384": _rsa_method_constructor(hashes.SHA384),
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512": _rsa_method_constructor(hashes.SHA512),
"http://www.w3.org/2000/09/xmldsig#hmac-sha1":_hmac_method_constructor(hashes.SHA1),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha224": _hmac_method_constructor(hashes.SHA224),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256": _hmac_method_constructor(hashes.SHA256),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha384": _hmac_method_constructor(hashes.SHA384),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha512": _hmac_method_constructor(hashes.SHA512),
}
def _apply_transform(node, transform_node):
transform_method = transform_node.get("Algorithm")
if transform_method in C14N_TRANSFORMS_METHODS:
return C14N_TRANSFORMS_METHODS[transform_method](node)
if transform_method == "http://www.w3.org/2000/09/xmldsig#enveloped-signature":
root_node = etree.fromstring(node).getroottree().getroot()
signature = root_node.find("{http://www.w3.org/2000/09/xmldsig#}Signature")
previous = signature.getprevious()
if previous is not None and signature.tail: # lxml future requirements, avoids a FutureWarning
previous.tail = "".join([previous.tail or "", signature.tail or ""])
elif signature.tail:
signature.getparent().text = "".join([signature.getparent().text or "", signature.tail or ""])
root_node.remove(signature)
return C14N_TRANSFORMS_METHODS["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"](root_node)
if transform_method == "http://www.w3.org/2000/09/xmldsig#base64":
try:
root_node = etree.fromstring(node)
return base64.b64decode(root_node.text)
except binascii.Error:
return base64.b64decode(node)
raise ValueError("Method not found or not allowed")
def _find_uri(node, uri=""):
uri_node = deepcopy(node.getroottree())
if uri == "":
return etree.tostring(uri_node.getroot(), method="c14n", with_comments=False, exclusive=False)
if uri.startswith("#"):
uri = uri.lstrip("#")
xpath = "//*[@*[local-name() = '{}' ]=$uri]"
for attr in ("ID", "Id", "id"):
result = uri_node.xpath(xpath.format(attr), uri=uri)
if len(result) == 1:
break
if len(result) != 1:
raise ValueError("Cannot reach specified URI")
return etree.tostring(result[0], method="c14n", with_comments=False, exclusive=False)
raise ValueError("Cannot reach specified URI")
def _validate_reference(reference_node):
node = _find_uri(reference_node, reference_node.get("URI", ""))
transforms_node = reference_node.find("ds:Transforms", namespaces=XMLSIG_NSMAP)
digest_value = False
if transforms_node is not None: # lxml future requirements, avoids a FutureWarning
for transform_node in transforms_node.findall("ds:Transform", namespaces=XMLSIG_NSMAP):
node = _apply_transform(node, transform_node)
digest_value = DIGEST_METHODS[reference_node.find("ds:DigestMethod", namespaces=XMLSIG_NSMAP).get("Algorithm")](node)
try:
decoded_file_digest = base64.b64decode(reference_node.find("ds:DigestValue", namespaces=XMLSIG_NSMAP).text, validate=True)
except binascii.Error:
raise ValueError("DigestValue is not a valid base64 string")
return consteq(digest_value, decoded_file_digest)
def validate_xmldsig_signature(signature_node, xmldsig_schema):
""" Validates a xml file signature if that xml is following the xmldsig format
:param lxml._Element signature_node: Signature node
:param lxml.XMLSchema xmldsig_schema: xmldsig schema
:return: Whether the signature is valid
:rtype: bool
"""
def octet_stream_to_integer_primitive(array):
return sum((array[i] * (256 ** len(array) - i - 1) for i, val in enumerate(array)))
xmldsig_schema.assertValid(signature_node)
signed_info_node = signature_node.find("ds:SignedInfo", namespaces=XMLSIG_NSMAP)
for reference_node in signed_info_node.findall("ds:Reference", namespaces=XMLSIG_NSMAP):
if not _validate_reference(reference_node):
return False
canonicalization_method = signed_info_node.find("ds:CanonicalizationMethod", namespaces=XMLSIG_NSMAP).get("Algorithm")
signature_method = signed_info_node.find("ds:SignatureMethod", namespaces=XMLSIG_NSMAP).get("Algorithm")
if signature_method not in SIGNATURE_METHODS:
raise ValueError(f"Method {signature_method} not accepted")
signed_info_node = C14N_TRANSFORMS_METHODS[canonicalization_method](etree.fromstring(etree.tostring(signed_info_node)))
# The weird fromstring(tostring) deals with lxml using XPATH 1.0 when "empty" default prefixes are only possible with XPATH 2.0
# E.G. Here the <SIE> tag has no prefix but corresponds to <{http://www.sie.se/sie5}SIE> in the original file
# which is not valid in XPATH 1.0 and thus lxml fails during the signature validation
signature_method = SIGNATURE_METHODS[signature_method]
rsa_key_node = signature_node.find("ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue", namespaces=XMLSIG_NSMAP)
certificate_node = signature_node.find("ds:KeyInfo/ds:X509Data/ds:X509Certificate", namespaces=XMLSIG_NSMAP)
if rsa_key_node is not None: # lxml future requirements, avoids a FutureWarning
n = octet_stream_to_integer_primitive(base64.b64decode(rsa_key_node.find("ds:Modulus", namespaces=XMLSIG_NSMAP).text))
e = octet_stream_to_integer_primitive(base64.b64decode(rsa_key_node.find("ds:Exponent", namespaces=XMLSIG_NSMAP).text))
public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
elif certificate_node is not None: # lxml future requirements, avoids a FutureWarning
certificate = x509.load_der_x509_certificate(base64.b64decode(certificate_node.text), default_backend())
public_key = certificate.public_key()
else:
raise NotImplementedError("Only x509 and RSA signatures methods are implemented")
try:
signature_method(signed_info_node, signature_node.find("ds:SignatureValue", namespaces=XMLSIG_NSMAP).text, public_key)
return True
except InvalidSignature:
return False