Skip to content

Commit

Permalink
Add support for sp:SignedElements
Browse files Browse the repository at this point in the history
  • Loading branch information
blaggacao committed Sep 21, 2024
1 parent 758dd29 commit 7e385b2
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/zeep/wsdl/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def __init__(self, wsdl, name, port_name):
self._operations = {}
self.signatures = {
"header": [], # Parts of header, that should be signed
"elements": [], # Arbitrary XPath elements that should be signed
"body": False, # If body should be signed
"everything": False, # If every header should be signed
}
Expand Down
33 changes: 33 additions & 0 deletions src/zeep/wsdl/wsdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,39 @@ def parse_binding(
# If we didn't set "everything" to True, update the headers
if not binding.signatures.get("everything", False):
binding.signatures["header"] = [dict(header) for header in all_headers]

# Begin parsing SignedElements assertions
signed_elements = doc.xpath(
'wsp:Policy[@wsu:Id="{}"]//sp:SignedElements'.format(binding_policy),
namespaces=NSMAP,
)

for signed_element in signed_elements:
xpath_version = signed_element.get('XPathVersion', 'http://www.w3.org/TR/1999/REC-xpath-19991116') # Default to XPath 1.0 if not specified

xpath_expressions = signed_element.xpath('sp:XPath', namespaces=NSMAP)

for xpath in xpath_expressions:
xpath_string = xpath.text
if xpath_string:
# Store the XPath expression and its version
binding.signatures.setdefault('elements', []).append({
'xpath': xpath_string,
'xpath_version': xpath_version
})

# If you want to merge multiple SignedElements assertions as per the specification
if 'elements' in binding.signatures:
# Remove duplicates while preserving order
unique_elements = []
seen = set()
for element in binding.signatures['elements']:
element_tuple = (element['xpath'], element['xpath_version'])
if element_tuple not in seen:
seen.add(element_tuple)
unique_elements.append(element)
binding.signatures['elements'] = unique_elements

logger.debug("Adding binding: %s", binding.name.text)
result[binding.name.text] = binding
break
Expand Down
25 changes: 25 additions & 0 deletions src/zeep/wsse/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,17 @@ def _signature_prepare(envelope, key, signature_method, digest_method, signature
header.find(QName(node["Namespace"], node["Name"])),
digest_method,
)
# Sign elements specified by XPath expressions
for element in signatures.get("elements", []):
_sign_node_by_xpath(
ctx,
signature,
envelope,
element["xpath"],
element["xpath_version"],
digest_method
)

ctx.sign(signature)

# Place the X509 data inside a WSSE SecurityTokenReference within
Expand All @@ -281,6 +292,20 @@ def _signature_prepare(envelope, key, signature_method, digest_method, signature
sec_token_ref = etree.SubElement(key_info, QName(ns.WSSE, "SecurityTokenReference"))
return security, sec_token_ref, x509_data

def _sign_node_by_xpath(ctx, signature, envelope, xpath, xpath_version, digest_method):
# Create an XPath evaluator with the appropriate version
if xpath_version == '1.0':
evaluator = etree.XPath(xpath, namespaces=envelope.nsmap)
else:
evaluator = etree.XPath(xpath, namespaces=envelope.nsmap, extension={('http://www.w3.org/TR/1999/REC-xpath-19991116', 'version'): xpath_version})

# Evaluate the XPath expression
nodes = evaluator(envelope)

# Sign each node found by the XPath expression
for node in nodes:
_sign_node(ctx, signature, node, digest_method)


def _sign_envelope_with_key(
envelope, key, signature_method, digest_method, signatures=None
Expand Down
28 changes: 24 additions & 4 deletions tests/test_wsdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,7 @@ def test_parse_bindings_signed_unknown():
document = wsdl.Document(content, None)
assert document.bindings[
"{http://tests.python-zeep.org/xsd-main}TestBinding"
].signatures == {"body": False, "everything": False, "header": []}
].signatures == {"body": False, "everything": False, "header": [], "elements": []}

def test_parse_bindings_signed_body():
policy = """
Expand All @@ -1391,7 +1391,7 @@ def test_parse_bindings_signed_body():
document = wsdl.Document(content, None)
assert document.bindings[
"{http://tests.python-zeep.org/xsd-main}TestBinding"
].signatures == {"body": True, "everything": False, "header": []}
].signatures == {"body": True, "everything": False, "header": [], "elements": []}


def test_parse_bindings_signed_everything():
Expand All @@ -1404,7 +1404,7 @@ def test_parse_bindings_signed_everything():
document = wsdl.Document(content, None)
assert document.bindings[
"{http://tests.python-zeep.org/xsd-main}TestBinding"
].signatures == {"body": True, "everything": True, "header": []}
].signatures == {"body": True, "everything": True, "header": [], "elements": []}


def test_parse_bindings_signed_headers():
Expand All @@ -1423,12 +1423,32 @@ def test_parse_bindings_signed_headers():
"body": False,
"everything": False,
"header": [{"Name": "To", "Namespace": "http://www.w3.org/2005/08/addressing"}],
"elements": []
}

def test_parse_bindings_signed_elements():
policy = """
<wsp:Policy wsu:Id="TestBinding_policy">
<sp:SignedElements">
<sp:XPath>//wsse:Security/wsu:Timestamp</sp:XPath>
</sp:SignedElements>
</wsp:Policy>
"""
content = StringIO(BASE_WSDL.format(policy=policy).strip())
document = wsdl.Document(content, None)
assert document.bindings[
"{http://tests.python-zeep.org/xsd-main}TestBinding"
].signatures == {
"body": False,
"everything": False,
"header": [],
"elements": [{"xpath": "//wsse:Security/wsu:Timestamp", "xpath_version": "http://www.w3.org/TR/1999/REC-xpath-19991116"}]
}


def test_parse_bindings_signed_nothing():
content = StringIO(BASE_WSDL.format(policy="").strip())
document = wsdl.Document(content, None)
assert document.bindings[
"{http://tests.python-zeep.org/xsd-main}TestBinding"
].signatures == {"body": False, "everything": False, "header": []}
].signatures == {"body": False, "everything": False, "header": [], "elements": []}

0 comments on commit 7e385b2

Please sign in to comment.