Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to exclude c14n tranformation from Transform XML node #104

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
10 changes: 8 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions signxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,13 @@ class XMLSigner(XMLSignatureProcessor):
listed under the `Algorithm Identifiers and Implementation Requirements
<http://www.w3.org/TR/xmldsig-core1/#sec-AlgID>`_ 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
Expand All @@ -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

Expand Down Expand Up @@ -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"))
Expand Down
171 changes: 90 additions & 81 deletions test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"<ds:DigestValue>", b"<ds:DigestValue>!")
XMLVerifier().verify(mangled_sig, **verify_kwargs)

with self.assertRaises(cryptography.exceptions.InvalidSignature):
mangled_sig = signed_data.replace(b"<ds:DigestValue>", b"<ds:DigestValue>!")
XMLVerifier().verify(mangled_sig, **verify_kwargs)

with self.assertRaises(cryptography.exceptions.InvalidSignature):
sig_value = re.search(b"<ds:SignatureValue>(.+?)</ds:SignatureValue>", signed_data).group(1)
mangled_sig = re.sub(
b"<ds:SignatureValue>(.+?)</ds:SignatureValue>",
b"<ds:SignatureValue>" + b64encode(b64decode(sig_value)[::-1]) + b"</ds:SignatureValue>",
signed_data
)
XMLVerifier().verify(mangled_sig, **verify_kwargs)
with self.assertRaises(cryptography.exceptions.InvalidSignature):
sig_value = re.search(b"<ds:SignatureValue>(.+?)</ds:SignatureValue>", signed_data).group(1)
mangled_sig = re.sub(
b"<ds:SignatureValue>(.+?)</ds:SignatureValue>",
b"<ds:SignatureValue>" + b64encode(b64decode(sig_value)[::-1]) + b"</ds:SignatureValue>",
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
Expand All @@ -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")
Expand Down Expand Up @@ -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('''
Expand Down