Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions cl_sii/libs/xml_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
"""
XML utils
=========


XML (Digital) Signature
-----------------------

a.k.a. 'XMLDSig', 'XML-DSig', XML-Sig'

XML Signature [..] defines an XML syntax for digital signatures and is
defined in the W3C recommendation "XML Signature Syntax and Processing"
(``xmldsig-core``). Functionally, it has much in common with ``PKCS#7 ``
but is more extensible and geared towards signing XML documents.
It is used by various Web technologies such as SOAP, SAML, and others.

.. seealso::
https://en.wikipedia.org/wiki/XML_Signature


"""
import logging
import os
from typing import IO
Expand All @@ -7,11 +28,32 @@
import lxml.etree
import xml.parsers.expat
import xml.parsers.expat.errors
from lxml.etree import ElementBase as XmlElement # noqa: F401
from lxml.etree import ElementTree as XmlElementTree # noqa: F401
from lxml.etree import XMLSchema as XmlSchema # noqa: F401


logger = logging.getLogger(__name__)


XML_DSIG_NS_MAP = dict(
ds='http://www.w3.org/2000/09/xmldsig#',
dsig11='http://www.w3.org/2009/xmldsig11#',
dsig2='http://www.w3.org/2010/xmldsig2#',
ec='http://www.w3.org/2001/10/xml-exc-c14n#',
dsig_more='http://www.w3.org/2001/04/xmldsig-more#',
xenc='http://www.w3.org/2001/04/xmlenc#',
xenc11='http://www.w3.org/2009/xmlenc11#',
)
"""
Mapping from XML namespace prefix to full name, for XML Signature.

Source:
``signxml.namespaces`` @ 16503242 (~ v2.6.0)
https://github.com/XML-Security/signxml/blob/16503242/signxml/__init__.py#L23-L31
"""


###############################################################################
# exceptions
###############################################################################
Expand Down Expand Up @@ -72,7 +114,7 @@ class XmlSchemaDocValidationError(Exception):
# functions
###############################################################################

def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase:
def parse_untrusted_xml(value: bytes) -> XmlElement:
"""
Parse XML-encoded content in value.

Expand Down Expand Up @@ -115,7 +157,7 @@ def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase:
base_url=None, # default: None
forbid_dtd=False, # default: False (allow Document Type Definition)
forbid_entities=True, # default: True (forbid Entity definitions/declarations)
) # type: lxml.etree.ElementBase
) # type: XmlElement

except (defusedxml.DTDForbidden,
defusedxml.EntitiesForbidden,
Expand Down Expand Up @@ -192,19 +234,19 @@ def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase:
return xml_root_em


def read_xml_schema(filename: str) -> lxml.etree.XMLSchema:
def read_xml_schema(filename: str) -> XmlSchema:
"""
Instantiate an XML schema object from a file.

:raises ValueError: if there is no file at ``filename``

"""
if os.path.exists(filename) and os.path.isfile(filename):
return lxml.etree.XMLSchema(file=filename)
return XmlSchema(file=filename)
raise ValueError("XML schema file not found.", filename)


def validate_xml_doc(xml_schema: lxml.etree.XMLSchema, xml_doc: lxml.etree.ElementBase) -> None:
def validate_xml_doc(xml_schema: XmlSchema, xml_doc: XmlElement) -> None:
"""
Validate ``xml_doc`` against XML schema ``xml_schema``.

Expand Down Expand Up @@ -240,7 +282,7 @@ def validate_xml_doc(xml_schema: lxml.etree.XMLSchema, xml_doc: lxml.etree.Eleme
raise XmlSchemaDocValidationError(validation_error_msg) from exc


def write_xml_doc(xml_doc: lxml.etree.ElementBase, output: IO[bytes]) -> None:
def write_xml_doc(xml_doc: XmlElement, output: IO[bytes]) -> None:
"""
Write ``xml_doc`` to bytes stream ``output``.

Expand All @@ -264,7 +306,7 @@ def write_xml_doc(xml_doc: lxml.etree.ElementBase, output: IO[bytes]) -> None:
# note: use `IO[X]` for arguments and `TextIO`/`BinaryIO` for return types (says GVR).
# https://github.com/python/typing/issues/518#issuecomment-350903120

xml_etree: lxml.etree.ElementTree = xml_doc.getroottree()
xml_etree: XmlElementTree = xml_doc.getroottree()

# See:
# https://lxml.de/api/lxml.etree._ElementTree-class.html#write
Expand Down
3 changes: 2 additions & 1 deletion tests/test_libs_xml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lxml.etree

from cl_sii.libs.xml_utils import XmlElement
from cl_sii.libs.xml_utils import ( # noqa: F401
XmlSyntaxError, XmlFeatureForbidden,
parse_untrusted_xml, read_xml_schema, validate_xml_doc, write_xml_doc,
Expand All @@ -20,7 +21,7 @@ def test_parse_untrusted_xml_valid(self) -> None:
b' <empty-element/>\n'
b'</root>')
xml = parse_untrusted_xml(value)
self.assertIsInstance(xml, lxml.etree.ElementBase)
self.assertIsInstance(xml, XmlElement)
# print(xml)
self.assertEqual(
lxml.etree.tostring(xml, pretty_print=False),
Expand Down