diff --git a/.travis.yml b/.travis.yml index a085d5d..803d959 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,17 +15,23 @@ matrix: python: 2.7 env: PYTHON_VERSION=2 os: osx + osx_image: xcode9.4 - language: generic python: 3.6 env: PYTHON_VERSION=3 os: osx + osx_image: xcode9.4 allow_failures: - python: pypy3 install: - - if [[ $TRAVIS_OS_NAME == osx ]] && [[ $PYTHON_VERSION == 3 ]]; then brew upgrade python; pip3 install --ignore-installed virtualenv; fi + - if [[ $TRAVIS_OS_NAME == osx ]]; then pip2 freeze; pip3 freeze; fi + - if [[ $TRAVIS_OS_NAME == osx && $PYTHON_VERSION == 3 ]]; then brew upgrade python; fi + - if [[ $TRAVIS_OS_NAME == osx ]]; then brew list openssl &>/dev/null || brew install openssl; fi - if [[ $TRAVIS_OS_NAME == osx ]]; then export LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include"; fi - - virtualenv --python=python$PYTHON_VERSION env + - if [[ $TRAVIS_OS_NAME == osx && $PYTHON_VERSION == 3 ]]; then python3 -m venv env; fi + - if [[ $TRAVIS_OS_NAME == osx && $PYTHON_VERSION == 2 ]]; then pip2 install virtualenv; virtualenv --python=python env; fi + - if [[ $TRAVIS_OS_NAME != osx ]]; then virtualenv --python=python$PYTHON_VERSION env; fi - source env/bin/activate - make install - if [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then pip install lxml==3.8.0; fi diff --git a/signxml/__init__.py b/signxml/__init__.py index c9e6191..79e9760 100644 --- a/signxml/__init__.py +++ b/signxml/__init__.py @@ -265,9 +265,13 @@ class XMLSigner(XMLSignatureProcessor): listed under the `Algorithm Identifiers and Implementation Requirements `_ section of the XML Signature 1.1 standard are supported. :type digest_algorithm: string + :param include_c14n_transform: If this parameter equals ``True`` c14n transformation will be included + in ``Transform`` XML node. This parameter is needed because some software is able to use hard restrictions + on Transform XML node, that prohibit includes c14n transformation into Transform XML node. + :type include_c14n_transform: bool """ def __init__(self, method=methods.enveloped, signature_algorithm="rsa-sha256", digest_algorithm="sha256", - c14n_algorithm=XMLSignatureProcessor.default_c14n_algorithm): + c14n_algorithm=XMLSignatureProcessor.default_c14n_algorithm, include_c14n_transform=True): if method not in methods: raise InvalidInput("Unknown signature method {}".format(method)) self.method = method @@ -277,6 +281,7 @@ def __init__(self, method=methods.enveloped, signature_algorithm="rsa-sha256", d self.digest_alg = digest_algorithm assert c14n_algorithm in self.known_c14n_algorithms self.c14n_alg = c14n_algorithm + self.include_c14n_transform = include_c14n_transform self.namespaces = dict(ds=namespaces.ds) self._parser = None @@ -479,7 +484,8 @@ def _build_sig(self, sig_root, reference_uris, c14n_inputs): if self.method == methods.enveloped: transforms = SubElement(reference, ds_tag("Transforms")) SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature") - SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg) + if self.include_c14n_transform is True: + SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg) digest_method = SubElement(reference, ds_tag("DigestMethod"), Algorithm=self.known_digest_tags[self.digest_alg]) digest_value = SubElement(reference, ds_tag("DigestValue")) diff --git a/test/test.py b/test/test.py index 96e059a..9a8c5f0 100755 --- a/test/test.py +++ b/test/test.py @@ -34,6 +34,7 @@ def resolve(self, url, id, context): parser.resolvers.add(URIResolver()) interop_dir = os.path.join(os.path.dirname(__file__), "interop") +include_c14_transformation = {True, False} class TestSignXML(unittest.TestCase): def setUp(self): @@ -69,70 +70,73 @@ def test_basic_signxml_statements(self): continue print(digest_alg, sig_alg, hash_alg, c14n_alg, method, type(d)) reset_tree(d, method) - signer = XMLSigner(method=method, - signature_algorithm="-".join([sig_alg, hash_alg]), - digest_algorithm=digest_alg, - c14n_algorithm=c14n_alg) - signed = signer.sign(d, - key=self.keys[sig_alg], - reference_uri="URI" if method == methods.detached else None) - # print(etree.tostring(signed)) - hmac_key = self.keys["hmac"] if sig_alg == "hmac" else None - verify_kwargs = dict(require_x509=False, hmac_key=hmac_key, validate_schema=True) - - if method == methods.detached: - def resolver(uri): - if isinstance(d, stdlibElementTree.Element): - return etree.fromstring(stdlibElementTree.tostring(d)) - else: - return d - verify_kwargs["uri_resolver"] = resolver - - signed_data = etree.tostring(signed) - XMLVerifier().verify(signed_data, **verify_kwargs) - XMLVerifier().verify(signed_data, parser=parser, **verify_kwargs) - (_d, _x, _s) = XMLVerifier().verify(signed_data, id_attribute="Id", **verify_kwargs) - - if _x is not None: - # Ensure the signature is not part of the signed data - self.assertIsNone(_x.find(".//{http://www.w3.org/2000/09/xmldsig#}Signature")) - self.assertNotEqual(_x.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature") - - # Ensure the signature was returned - self.assertEqual(_s.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature") - - if method == methods.enveloping: - with self.assertRaisesRegexp(InvalidInput, "Unable to resolve reference URI"): - XMLVerifier().verify(signed_data, id_attribute="X", **verify_kwargs) - - with self.assertRaisesRegexp(InvalidInput, "Expected a X.509 certificate based signature"): - XMLVerifier().verify(signed_data, hmac_key=hmac_key, uri_resolver=verify_kwargs.get("uri_resolver")) - - if method != methods.detached: - with self.assertRaisesRegexp(InvalidSignature, "Digest mismatch"): - mangled_sig = signed_data.replace(b"Austria", b"Mongolia").replace(b"x y", b"a b") + for c14_transform_option in include_c14_transformation: + signer = XMLSigner(method=method, + signature_algorithm="-".join([sig_alg, hash_alg]), + digest_algorithm=digest_alg, + c14n_algorithm=c14n_alg, + include_c14n_transform=c14_transform_option) + signed = signer.sign(d, + key=self.keys[sig_alg], + reference_uri="URI" if method == methods.detached else None) + # print(etree.tostring(signed)) + hmac_key = self.keys["hmac"] if sig_alg == "hmac" else None + verify_kwargs = dict(require_x509=False, hmac_key=hmac_key, validate_schema=True) + + if method == methods.detached: + def resolver(uri): + if isinstance(d, stdlibElementTree.Element): + return etree.fromstring(stdlibElementTree.tostring(d)) + else: + return d + verify_kwargs["uri_resolver"] = resolver + + signed_data = etree.tostring(signed) + XMLVerifier().verify(signed_data, **verify_kwargs) + XMLVerifier().verify(signed_data, parser=parser, **verify_kwargs) + (_d, _x, _s) = XMLVerifier().verify(signed_data, id_attribute="Id", **verify_kwargs) + + if _x is not None: + # Ensure the signature is not part of the signed data + self.assertIsNone(_x.find(".//{http://www.w3.org/2000/09/xmldsig#}Signature")) + self.assertNotEqual(_x.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature") + + # Ensure the signature was returned + self.assertEqual(_s.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature") + + if method == methods.enveloping: + with self.assertRaisesRegexp(InvalidInput, "Unable to resolve reference URI"): + XMLVerifier().verify(signed_data, id_attribute="X", **verify_kwargs) + + with self.assertRaisesRegexp(InvalidInput, "Expected a X.509 certificate based signature"): + XMLVerifier().verify(signed_data, hmac_key=hmac_key, + uri_resolver=verify_kwargs.get("uri_resolver")) + + if method != methods.detached: + with self.assertRaisesRegexp(InvalidSignature, "Digest mismatch"): + mangled_sig = signed_data.replace(b"Austria", b"Mongolia").replace(b"x y", b"a b") + XMLVerifier().verify(mangled_sig, **verify_kwargs) + + with self.assertRaises(cryptography.exceptions.InvalidSignature): + mangled_sig = signed_data.replace(b"", b"!") XMLVerifier().verify(mangled_sig, **verify_kwargs) - with self.assertRaises(cryptography.exceptions.InvalidSignature): - mangled_sig = signed_data.replace(b"", b"!") - XMLVerifier().verify(mangled_sig, **verify_kwargs) - - with self.assertRaises(cryptography.exceptions.InvalidSignature): - sig_value = re.search(b"(.+?)", signed_data).group(1) - mangled_sig = re.sub( - b"(.+?)", - b"" + b64encode(b64decode(sig_value)[::-1]) + b"", - signed_data - ) - XMLVerifier().verify(mangled_sig, **verify_kwargs) + with self.assertRaises(cryptography.exceptions.InvalidSignature): + sig_value = re.search(b"(.+?)", signed_data).group(1) + mangled_sig = re.sub( + b"(.+?)", + b"" + b64encode(b64decode(sig_value)[::-1]) + b"", + signed_data + ) + XMLVerifier().verify(mangled_sig, **verify_kwargs) - with self.assertRaises(etree.XMLSyntaxError): - XMLVerifier().verify("", hmac_key=hmac_key, require_x509=False) + with self.assertRaises(etree.XMLSyntaxError): + XMLVerifier().verify("", hmac_key=hmac_key, require_x509=False) - if sig_alg == "hmac": - with self.assertRaisesRegexp(InvalidSignature, "Signature mismatch"): - verify_kwargs["hmac_key"] = b"SECRET" - XMLVerifier().verify(signed_data, **verify_kwargs) + if sig_alg == "hmac": + with self.assertRaisesRegexp(InvalidSignature, "Signature mismatch"): + verify_kwargs["hmac_key"] = b"SECRET" + XMLVerifier().verify(signed_data, **verify_kwargs) def test_x509_certs(self): from OpenSSL.crypto import load_certificate, FILETYPE_PEM, Error as OpenSSLCryptoError @@ -145,26 +149,28 @@ def test_x509_certs(self): key = fh.read() for hash_alg in "sha1", "sha256": for method in methods.enveloped, methods.enveloping: - print(hash_alg, method) - data = tree.getroot() - reset_tree(data, method) - signer = XMLSigner(method=method, signature_algorithm="rsa-" + hash_alg) - signed = signer.sign(data, key=key, cert=crt) - signed_data = etree.tostring(signed) - XMLVerifier().verify(signed_data, ca_pem_file=ca_pem_file) - XMLVerifier().verify(signed_data, x509_cert=crt) - XMLVerifier().verify(signed_data, x509_cert=load_certificate(FILETYPE_PEM, crt)) - XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="*.example.com") - - with self.assertRaises(OpenSSLCryptoError): - XMLVerifier().verify(signed_data, x509_cert=crt[::-1]) - - with self.assertRaises(InvalidSignature): - XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="test") - - with self.assertRaisesRegexp(InvalidCertificate, "unable to get local issuer certificate"): - XMLVerifier().verify(signed_data) - # TODO: negative: verify with wrong cert, wrong CA + for c14_transform_option in include_c14_transformation: + print(hash_alg, method) + data = tree.getroot() + reset_tree(data, method) + signer = XMLSigner(method=method, signature_algorithm="rsa-" + hash_alg, + include_c14n_transform=c14_transform_option) + signed = signer.sign(data, key=key, cert=crt) + signed_data = etree.tostring(signed) + XMLVerifier().verify(signed_data, ca_pem_file=ca_pem_file) + XMLVerifier().verify(signed_data, x509_cert=crt) + XMLVerifier().verify(signed_data, x509_cert=load_certificate(FILETYPE_PEM, crt)) + XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="*.example.com") + + with self.assertRaises(OpenSSLCryptoError): + XMLVerifier().verify(signed_data, x509_cert=crt[::-1]) + + with self.assertRaises(InvalidSignature): + XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="test") + + with self.assertRaisesRegexp(InvalidCertificate, "unable to get local issuer certificate"): + XMLVerifier().verify(signed_data) + # TODO: negative: verify with wrong cert, wrong CA def test_xmldsig_interop_examples(self): ca_pem_file = os.path.join(os.path.dirname(__file__), "interop", "cacert.pem").encode("utf-8") @@ -341,7 +347,10 @@ def test_reference_uris_and_custom_key_info(self): self.assertEqual("{urn:oasis:names:tc:SAML:2.0:assertion}Assertion", signed_data_root.tag) # Also test with detached signing - XMLSigner(method=methods.detached).sign(data, reference_uri=reference_uri, key=key, cert=crt) + for c14_transform_option in include_c14_transformation: + XMLSigner(method=methods.detached, + include_c14n_transform=c14_transform_option + ).sign(data, reference_uri=reference_uri, key=key, cert=crt) # Test setting custom key info custom_key_info = etree.fromstring('''