Skip to content

Commit ef15da8

Browse files
committed
Handle ambiguous KeyValue situations
- XMLSigner.sign(): add always_add_key_value kwarg to include both X509Data and KeyValue for ill-defined signing applications - XMLVerifier.verify(): reject signatures that contain both X509Data and KeyValue by default; add ignore_ambiguous_key_info kwarg to bypass - Add security warnings in docs Fixes #52 Fixes #65 Fixes #64
1 parent 995fe61 commit ef15da8

File tree

2 files changed

+29
-3
lines changed

2 files changed

+29
-3
lines changed

signxml/__init__.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,12 @@ def sign(self, data, key=None, passphrase=None, cert=None, reference_uri=None, k
320320
:param id_attribute:
321321
Name of the attribute whose value ``URI`` refers to. By default, SignXML will search for "Id", then "ID".
322322
:type id_attribute: string
323-
:param always_add_key_value: Write the key value to the KeyInfo element even if an X509 certificate is present.
323+
:param always_add_key_value:
324+
Write the key value to the KeyInfo element even if a X509 certificate is present. Use of this parameter
325+
is discouraged, as it introduces an ambiguity and a security hazard. The public key used to sign the
326+
document is already encoded in the certificate (which is in X509Data), so the verifier must either ignore
327+
KeyValue or make sure it matches what's in the certificate. This parameter is provided for compatibility
328+
purposes only.
324329
:type always_add_key_value: boolean
325330
326331
:returns:
@@ -610,7 +615,7 @@ def _apply_transforms(self, payload, transforms_node, signature, c14n_algorithm)
610615

611616
def verify(self, data, require_x509=True, x509_cert=None, cert_subject_name=None, ca_pem_file=None, ca_path=None,
612617
hmac_key=None, validate_schema=True, parser=None, uri_resolver=None, id_attribute=None,
613-
expect_references=1):
618+
expect_references=1, ignore_ambiguous_key_info=False):
614619
"""
615620
Verify the XML signature supplied in the data and return the XML node signed by the signature, or raise an
616621
exception if the signature is not valid. By default, this requires the signature to be generated using a valid
@@ -681,6 +686,14 @@ def verify(self, data, require_x509=True, x509_cert=None, cert_subject_name=None
681686
Number of references to expect in the signature. If this is not 1, an array of VerifyResults is returned.
682687
If set to a non-integer, any number of references is accepted (otherwise a mismatch raises an error).
683688
:type expect_references: int or boolean
689+
:param ignore_ambiguous_key_info:
690+
Ignore the presence of a KeyValue element when X509Data is present in the signature and used for verifying.
691+
The presence of both elements is an ambiguity and a security hazard. The public key used to sign the
692+
document is already encoded in the certificate (which is in X509Data), so the verifier must either ignore
693+
KeyValue or make sure it matches what's in the certificate. SignXML does not implement the functionality
694+
necessary to match the keys, and throws an InvalidInput error instead. Set this to True to bypass the error
695+
and validate the signature using X509Data only.
696+
:type ignore_ambiguous_key_info: boolean
684697
685698
:raises: :py:class:`cryptography.exceptions.InvalidSignature`
686699
@@ -716,6 +729,7 @@ def verify(self, data, require_x509=True, x509_cert=None, cert_subject_name=None
716729
signature_alg = signature_method.get("Algorithm")
717730
raw_signature = b64decode(signature_value.text)
718731
x509_data = signature.find("ds:KeyInfo/ds:X509Data", namespaces=namespaces)
732+
key_value = signature.find("ds:KeyInfo/ds:KeyValue", namespaces=namespaces)
719733
signed_info_c14n = self._c14n(signed_info, algorithm=c14n_algorithm)
720734

721735
# TODO: if both X509Data and KeyValue is present, match one against the other and raise an error on mismatch
@@ -749,6 +763,12 @@ def verify(self, data, require_x509=True, x509_cert=None, cert_subject_name=None
749763
except Exception:
750764
reason = e
751765
raise InvalidSignature("Signature verification failed: {}".format(reason))
766+
767+
if ignore_ambiguous_key_info is False:
768+
if key_value is not None:
769+
raise InvalidInput("Both X509Data and KeyValue found. Use verify(ignore_ambiguous_key_info=True) "
770+
"to ignore KeyValue and validate using X509Data only.")
771+
752772
# TODO: CN verification goes here
753773
# TODO: require one of the following to be set: either x509_cert or (ca_pem_file or ca_path) or common_name
754774
# Use ssl.match_hostname or code from it to perform match
@@ -764,7 +784,6 @@ def verify(self, data, require_x509=True, x509_cert=None, cert_subject_name=None
764784
if raw_signature != signer.finalize():
765785
raise InvalidSignature("Signature mismatch (HMAC)")
766786
else:
767-
key_value = signature.find("ds:KeyInfo/ds:KeyValue", namespaces=namespaces)
768787
if key_value is None:
769788
raise InvalidInput("Expected to find either KeyValue or X509Data XML element in KeyInfo")
770789

test/test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,13 @@ def test_reference_uris_and_custom_key_info(self):
375375
self.assertTrue(s3.xpath("/ds:Signature/ds:KeyInfo/wsse:SecurityTokenReference",
376376
namespaces=dict(namespaces, wsse=wsse_ns)))
377377

378+
# Test setting both X509Data and KeyInfo
379+
s4 = XMLSigner().sign(data, reference_uri=reference_uri, key=key, cert=crt, always_add_key_value=True)
380+
with self.assertRaisesRegexp(InvalidInput, "Both X509Data and KeyValue found"):
381+
XMLVerifier().verify(s4, x509_cert=crt)
382+
expect_refs = etree.tostring(s4).decode().count("<ds:Reference")
383+
XMLVerifier().verify(s4, x509_cert=crt, ignore_ambiguous_key_info=True, expect_references=expect_refs)
384+
378385
def test_excision_of_untrusted_comments(self):
379386
pass # TODO: test comments excision
380387

0 commit comments

Comments
 (0)