Skip to content

Commit 3ec6ccb

Browse files
committed
Add support for sp:SignedElements
1 parent 758dd29 commit 3ec6ccb

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

src/zeep/wsdl/definitions.py

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def __init__(self, wsdl, name, port_name):
137137
self._operations = {}
138138
self.signatures = {
139139
"header": [], # Parts of header, that should be signed
140+
"elements": [], # Arbitrary XPath elements that should be signed
140141
"body": False, # If body should be signed
141142
"everything": False, # If every header should be signed
142143
}

src/zeep/wsdl/wsdl.py

+33
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,39 @@ def parse_binding(
476476
# If we didn't set "everything" to True, update the headers
477477
if not binding.signatures.get("everything", False):
478478
binding.signatures["header"] = [dict(header) for header in all_headers]
479+
480+
# Begin parsing SignedElements assertions
481+
signed_elements = doc.xpath(
482+
'wsp:Policy[@wsu:Id="{}"]//sp:SignedElements'.format(binding_policy),
483+
namespaces=NSMAP,
484+
)
485+
486+
for signed_element in signed_elements:
487+
xpath_version = signed_element.get('XPathVersion', '1.0') # Default to XPath 1.0 if not specified
488+
489+
xpath_expressions = signed_element.xpath('sp:XPath', namespaces=NSMAP)
490+
491+
for xpath in xpath_expressions:
492+
xpath_string = xpath.text
493+
if xpath_string:
494+
# Store the XPath expression and its version
495+
binding.signatures.setdefault('elements', []).append({
496+
'xpath': xpath_string,
497+
'xpath_version': xpath_version
498+
})
499+
500+
# If you want to merge multiple SignedElements assertions as per the specification
501+
if 'elements' in binding.signatures:
502+
# Remove duplicates while preserving order
503+
unique_elements = []
504+
seen = set()
505+
for element in binding.signatures['elements']:
506+
element_tuple = (element['xpath'], element['xpath_version'])
507+
if element_tuple not in seen:
508+
seen.add(element_tuple)
509+
unique_elements.append(element)
510+
binding.signatures['elements'] = unique_elements
511+
479512
logger.debug("Adding binding: %s", binding.name.text)
480513
result[binding.name.text] = binding
481514
break

src/zeep/wsse/signature.py

+25
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,17 @@ def _signature_prepare(envelope, key, signature_method, digest_method, signature
272272
header.find(QName(node["Namespace"], node["Name"])),
273273
digest_method,
274274
)
275+
# Sign elements specified by XPath expressions
276+
for element in signatures.get("elements", []):
277+
_sign_node_by_xpath(
278+
ctx,
279+
signature,
280+
envelope,
281+
element["xpath"],
282+
element["xpath_version"],
283+
digest_method
284+
)
285+
275286
ctx.sign(signature)
276287

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

295+
def _sign_node_by_xpath(ctx, signature, envelope, xpath, xpath_version, digest_method):
296+
# Create an XPath evaluator with the appropriate version
297+
if xpath_version == '1.0':
298+
evaluator = etree.XPath(xpath, namespaces=envelope.nsmap)
299+
else:
300+
evaluator = etree.XPath(xpath, namespaces=envelope.nsmap, extension={('http://www.w3.org/TR/1999/REC-xpath-19991116', 'version'): xpath_version})
301+
302+
# Evaluate the XPath expression
303+
nodes = evaluator(envelope)
304+
305+
# Sign each node found by the XPath expression
306+
for node in nodes:
307+
_sign_node(ctx, signature, node, digest_method)
308+
284309

285310
def _sign_envelope_with_key(
286311
envelope, key, signature_method, digest_method, signatures=None

0 commit comments

Comments
 (0)