diff options
author | Roland Hedberg <roland@catalogix.se> | 2017-04-24 15:46:35 +0200 |
---|---|---|
committer | Roland Hedberg <roland@catalogix.se> | 2017-04-24 15:46:35 +0200 |
commit | 91bce29d7fd66537797cefa554f63edd0e404ce1 (patch) | |
tree | ddbfeeb2718d88fe5ac97b4be65855e402f8a182 | |
parent | e2adbb523c37643453fbe62469ca7fb2bf1b43f0 (diff) | |
parent | 47e1b34f6c6fd9a48405f1560f24619d77526b3a (diff) | |
download | pysaml2-91bce29d7fd66537797cefa554f63edd0e404ce1.tar.gz |
Merge branch 'master' of github.com:rohe/pysaml2
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | README.rst | 14 | ||||
-rwxr-xr-x | setup.py | 3 | ||||
-rw-r--r-- | src/saml2/__init__.py | 7 | ||||
-rw-r--r-- | src/saml2/cert.py | 37 | ||||
-rw-r--r-- | src/saml2/client_base.py | 15 | ||||
-rw-r--r-- | src/saml2/config.py | 15 | ||||
-rw-r--r-- | src/saml2/entity.py | 3 | ||||
-rw-r--r-- | src/saml2/mdstore.py | 12 | ||||
-rw-r--r-- | src/saml2/pack.py | 3 | ||||
-rw-r--r-- | src/saml2/s_utils.py | 85 | ||||
-rw-r--r-- | src/saml2/sigver.py | 86 | ||||
-rw-r--r-- | src/saml2/soap.py | 7 | ||||
-rw-r--r-- | tests/sp_conf_nameidpolicy.py | 64 | ||||
-rw-r--r-- | tests/test_03_saml2.py | 27 | ||||
-rwxr-xr-x | tests/test_43_soap.py | 43 | ||||
-rw-r--r-- | tests/test_51_client.py | 35 | ||||
-rw-r--r-- | tests/test_requirements.txt | 4 | ||||
-rw-r--r-- | tox.ini | 3 |
19 files changed, 273 insertions, 194 deletions
@@ -33,6 +33,8 @@ tmp* _build/ .cache *.swp +.tox +env example/idp3/htdocs/login.mako @@ -192,8 +194,6 @@ example/sp-repoze/old_sp.xml example/sp-repoze/sp_conf_2.Pygmalion -.gitignore.swp - example/sp-repoze/sp_conf_2.py sp.xml @@ -3,7 +3,7 @@ PySAML2 - SAML2 in Python ************************* :Author: Roland Hedberg -:Version: 4.0.4 +:Version: 4.4.0 .. image:: https://api.travis-ci.org/rohe/pysaml2.png?branch=master :target: https://travis-ci.org/rohe/pysaml2 @@ -26,3 +26,15 @@ necessary pieces for building a SAML2 service provider or an identity provider. The distribution contains examples of both. Originally written to work in a WSGI environment there are extensions that allow you to use it with other frameworks. + +Testing +======= +PySAML2 uses the `pytest <http://doc.pytest.org/en/latest/>`_ framework for +testing. To run the tests on your system's version of python + +1. Create and activate a `virtualenv <https://virtualenv.pypa.io/en/stable/>`_. +2. Inside the virtualenv, install the dependencies needed for testing :code:`pip install -r tests/test_requirements.txt` +3. Run the tests :code:`py.test tests` + +To run tests in multiple python environments, you can use +`pyenv <https://github.com/yyuu/pyenv>`_ with `tox <https://tox.readthedocs.io/en/latest/>`_. @@ -14,10 +14,11 @@ install_requires = [ 'paste', 'zope.interface', 'repoze.who', - 'pycryptodomex', + 'cryptography', 'pytz', 'pyOpenSSL', 'python-dateutil', + 'defusedxml', 'six' ] diff --git a/src/saml2/__init__.py b/src/saml2/__init__.py index d2e606d1..b246caa6 100644 --- a/src/saml2/__init__.py +++ b/src/saml2/__init__.py @@ -17,7 +17,7 @@ provides methods and functions to convert SAML classes to and from strings. """ -__version__ = "4.3.0" +__version__ = "4.4.0" import logging import six @@ -36,6 +36,7 @@ except ImportError: import cElementTree as ElementTree except ImportError: from elementtree import ElementTree +import defusedxml.ElementTree root_logger = logging.getLogger(__name__) root_logger.level = logging.NOTSET @@ -87,7 +88,7 @@ def create_class_from_xml_string(target_class, xml_string): """ if not isinstance(xml_string, six.binary_type): xml_string = xml_string.encode('utf-8') - tree = ElementTree.fromstring(xml_string) + tree = defusedxml.ElementTree.fromstring(xml_string) return create_class_from_element_tree(target_class, tree) @@ -269,7 +270,7 @@ class ExtensionElement(object): def extension_element_from_string(xml_string): - element_tree = ElementTree.fromstring(xml_string) + element_tree = defusedxml.ElementTree.fromstring(xml_string) return _extension_element_from_element_tree(element_tree) diff --git a/src/saml2/cert.py b/src/saml2/cert.py index f71fd8ed..f9f97a6e 100644 --- a/src/saml2/cert.py +++ b/src/saml2/cert.py @@ -8,7 +8,11 @@ import six from OpenSSL import crypto from os.path import join from os import remove -from Cryptodome.Util import asn1 + +from cryptography.hazmat.backends import default_backend +from cryptography.x509 import load_pem_x509_certificate + +backend = default_backend() class WrongInput(Exception): pass @@ -194,9 +198,8 @@ class OpenSSLWrapper(object): f.close() def read_str_from_file(self, file, type="pem"): - f = open(file, 'rt') - str_data = f.read() - f.close() + with open(file, 'rb') as f: + str_data = f.read() if type == "pem": return str_data @@ -336,31 +339,13 @@ class OpenSSLWrapper(object): cert_algorithm = cert.get_signature_algorithm() if six.PY3: cert_algorithm = cert_algorithm.decode('ascii') + cert_str = cert_str.encode('ascii') - cert_asn1 = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert) - - der_seq = asn1.DerSequence() - der_seq.decode(cert_asn1) - - cert_certificate = der_seq[0] - #cert_signature_algorithm=der_seq[1] - cert_signature = der_seq[2] - - cert_signature_decoded = asn1.DerObject() - cert_signature_decoded.decode(cert_signature) - - signature_payload = cert_signature_decoded.payload - - sig_pay0 = signature_payload[0] - if ((isinstance(sig_pay0, int) and sig_pay0 != 0) or - (isinstance(sig_pay0, str) and sig_pay0 != '\x00')): - return (False, - "The certificate should not contain any unused bits.") - - signature = signature_payload[1:] + cert_crypto = load_pem_x509_certificate(cert_str, backend) try: - crypto.verify(ca_cert, signature, cert_certificate, + crypto.verify(ca_cert, cert_crypto.signature, + cert_crypto.tbs_certificate_bytes, cert_algorithm) return True, "Signed certificate is valid and correctly signed by CA certificate." except crypto.Error as e: diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 62410430..4b1b350e 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -207,7 +207,7 @@ class Base(Entity): nameid_format=None, service_url_binding=None, message_id=0, consent=None, extensions=None, sign=None, - allow_create=False, sign_prepare=False, sign_alg=None, + allow_create=None, sign_prepare=False, sign_alg=None, digest_alg=None, **kwargs): """ Creates an authentication request. @@ -288,10 +288,15 @@ class Base(Entity): args["name_id_policy"] = kwargs["name_id_policy"] del kwargs["name_id_policy"] except KeyError: - if allow_create: - allow_create = "true" - else: - allow_create = "false" + if allow_create is None: + allow_create = self.config.getattr("name_id_format_allow_create", "sp") + if allow_create is None: + allow_create = "false" + else: + if allow_create is True: + allow_create = "true" + else: + allow_create = "false" if nameid_format == "": name_id_policy = None diff --git a/src/saml2/config.py b/src/saml2/config.py index c5e8f2c7..50d61c57 100644 --- a/src/saml2/config.py +++ b/src/saml2/config.py @@ -1,13 +1,14 @@ #!/usr/bin/env python + import copy -import sys -import os -import re +import importlib import logging import logging.handlers -import six +import os +import re +import sys -from future.backports.test.support import import_module +import six from saml2 import root_logger, BINDING_URI, SAMLError from saml2 import BINDING_SOAP @@ -72,6 +73,7 @@ SP_ARGS = [ "allow_unsolicited", "ecp", "name_id_format", + "name_id_format_allow_create", "logout_requests_signed", "requested_attribute_name_format" ] @@ -186,6 +188,7 @@ class Config(object): self.contact_person = None self.name_form = None self.name_id_format = None + self.name_id_format_allow_create = None self.virtual_organization = None self.logger = None self.only_use_keys_in_metadata = True @@ -359,7 +362,7 @@ class Config(object): else: sys.path.insert(0, head) - return import_module(tail) + return importlib.import_module(tail) def load_file(self, config_file, metadata_construction=False): if config_file.endswith(".py"): diff --git a/src/saml2/entity.py b/src/saml2/entity.py index c6b287f5..b24c6210 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -7,9 +7,6 @@ import six from binascii import hexlify from hashlib import sha1 -# from Crypto.PublicKey import RSA -from Cryptodome.PublicKey import RSA - from saml2.metadata import ENDPOINTS from saml2.profile import paos, ecp from saml2.soap import parse_soap_enveloped_saml_artifact_resolve diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index ce734a5d..eff75c8b 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -1,17 +1,17 @@ from __future__ import print_function import hashlib +import importlib +import json import logging import os import sys -import json -import requests -import six from hashlib import sha1 from os.path import isfile from os.path import join -from future.backports.test.support import import_module +import requests +import six from saml2 import md from saml2 import saml @@ -694,7 +694,7 @@ class MetaDataLoader(MetaDataFile): i = func.rfind('.') module, attr = func[:i], func[i + 1:] try: - mod = import_module(module) + mod = importlib.import_module(module) except Exception as e: raise RuntimeError( 'Cannot find metadata provider function %s: "%s"' % (func, e)) @@ -930,7 +930,7 @@ class MetadataStore(MetaData): raise SAMLError("Misconfiguration in metadata %s" % item) mod, clas = key.rsplit('.', 1) try: - mod = import_module(mod) + mod = importlib.import_module(mod) MDloader = getattr(mod, clas) except (ImportError, AttributeError): raise SAMLError("Unknown metadata loader %s" % key) diff --git a/src/saml2/pack.py b/src/saml2/pack.py index e4c14625..728a516f 100644 --- a/src/saml2/pack.py +++ b/src/saml2/pack.py @@ -37,6 +37,7 @@ except ImportError: import cElementTree as ElementTree except ImportError: from elementtree import ElementTree +import defusedxml.ElementTree NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" FORM_SPEC = """<form method="post" action="%s"> @@ -235,7 +236,7 @@ def parse_soap_enveloped_saml(text, body_class, header_class=None): :param text: The SOAP object as XML :return: header parts and body as saml.samlbase instances """ - envelope = ElementTree.fromstring(text) + envelope = defusedxml.ElementTree.fromstring(text) assert envelope.tag == '{%s}Envelope' % NAMESPACE # print(len(envelope)) diff --git a/src/saml2/s_utils.py b/src/saml2/s_utils.py index c2689338..8f95d016 100644 --- a/src/saml2/s_utils.py +++ b/src/saml2/s_utils.py @@ -1,33 +1,23 @@ #!/usr/bin/env python -import logging -import random -import time import base64 -import six -import sys +import hashlib import hmac +import logging +import random import string - -# from python 2.5 -import imp +import sys +import time import traceback +import zlib -if sys.version_info >= (2, 5): - import hashlib -else: # before python 2.5 - import sha +import six from saml2 import saml from saml2 import samlp from saml2 import VERSION from saml2.time_util import instant -try: - from hashlib import md5 -except ImportError: - from md5 import md5 -import zlib logger = logging.getLogger(__name__) @@ -407,67 +397,6 @@ def verify_signature(secret, parts): return False -FTICKS_FORMAT = "F-TICKS/SWAMID/2.0%s#" - - -def fticks_log(sp, logf, idp_entity_id, user_id, secret, assertion): - """ - 'F-TICKS/' federationIdentifier '/' version *('#' attribute '=' value) '#' - Allowed attributes: - TS the login time stamp - RP the relying party entityID - AP the asserting party entityID (typcially the IdP) - PN a sha256-hash of the local principal name and a unique key - AM the authentication method URN - - :param sp: Client instance - :param logf: The log function to use - :param idp_entity_id: IdP entity ID - :param user_id: The user identifier - :param secret: A salt to make the hash more secure - :param assertion: A SAML Assertion instance gotten from the IdP - """ - csum = hmac.new(secret, digestmod=hashlib.sha1) - csum.update(user_id) - ac = assertion.AuthnStatement[0].AuthnContext[0] - - info = { - "TS": time.time(), - "RP": sp.entity_id, - "AP": idp_entity_id, - "PN": csum.hexdigest(), - "AM": ac.AuthnContextClassRef.text - } - logf.info(FTICKS_FORMAT % "#".join(["%s=%s" % (a, v) for a, v in info])) - - -def dynamic_importer(name, class_name=None): - """ - Dynamically imports modules / classes - """ - try: - fp, pathname, description = imp.find_module(name) - except ImportError: - print("unable to locate module: " + name) - return None, None - - try: - package = imp.load_module(name, fp, pathname, description) - except Exception: - raise - - if class_name: - try: - _class = imp.load_module("%s.%s" % (name, class_name), fp, - pathname, description) - except Exception: - raise - - return package, _class - else: - return package, None - - def exception_trace(exc): message = traceback.format_exception(*sys.exc_info()) diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index 095a79c8..3d80fb74 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -19,25 +19,13 @@ from binascii import hexlify from future.backports.urllib.parse import urlencode -# from Crypto.PublicKey.RSA import importKey -# from Crypto.Signature import PKCS1_v1_5 -# from Crypto.Util.asn1 import DerSequence -# from Crypto.PublicKey import RSA -# from Crypto.Hash import SHA -# from Crypto.Hash import SHA224 -# from Crypto.Hash import SHA256 -# from Crypto.Hash import SHA384 -# from Crypto.Hash import SHA512 - -from Cryptodome.PublicKey.RSA import importKey -from Cryptodome.Signature import PKCS1_v1_5 -from Cryptodome.Util.asn1 import DerSequence -from Cryptodome.PublicKey import RSA -from Cryptodome.Hash import SHA -from Cryptodome.Hash import SHA224 -from Cryptodome.Hash import SHA256 -from Cryptodome.Hash import SHA384 -from Cryptodome.Hash import SHA512 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.x509 import load_pem_x509_certificate from tempfile import NamedTemporaryFile from subprocess import Popen @@ -87,6 +75,8 @@ XMLTAG = "<?xml version='1.0'?>" PREFIX1 = "<?xml version='1.0' encoding='UTF-8'?>" PREFIX2 = '<?xml version="1.0" encoding="UTF-8"?>' +backend = default_backend() + class SigverError(SAMLError): pass @@ -406,18 +396,10 @@ def active_cert(key): """ try: cert_str = pem_format(key) - try: - certificate = importKey(cert_str) - not_before = to_time(str(certificate.get_not_before())) - not_after = to_time(str(certificate.get_not_after())) - assert not_before < utc_now() - assert not_after > utc_now() - return True - except: - cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str) - assert cert.has_expired() == 0 - assert not OpenSSLWrapper().certificate_not_valid_yet(cert) - return True + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str) + assert cert.has_expired() == 0 + assert not OpenSSLWrapper().certificate_not_valid_yet(cert) + return True except AssertionError: return False except AttributeError: @@ -555,19 +537,8 @@ def rsa_eq(key1, key2): def extract_rsa_key_from_x509_cert(pem): - # Convert from PEM to DER - der = ssl.PEM_cert_to_DER_cert(pem.decode('ascii')) - - # Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280) - cert = DerSequence() - cert.decode(der) - tbsCertificate = DerSequence() - tbsCertificate.decode(cert[0]) - subjectPublicKeyInfo = tbsCertificate[6] - - # Initialize RSA key - rsa_key = RSA.importKey(subjectPublicKeyInfo) - return rsa_key + cert = load_pem_x509_certificate(pem, backend) + return cert.public_key() def pem_format(key): @@ -576,7 +547,7 @@ def pem_format(key): def import_rsa_key_from_file(filename): - return RSA.importKey(read_file(filename, 'r')) + return load_pem_private_key(read_file(filename, 'rb'), None, backend) def parse_xmlsec_output(output): @@ -622,25 +593,28 @@ class RSASigner(Signer): if key is None: key = self.key - h = self.digest.new(msg) - signer = PKCS1_v1_5.new(key) - return signer.sign(h) + return key.sign(msg, PKCS1v15(), self.digest) def verify(self, msg, sig, key=None): if key is None: key = self.key - h = self.digest.new(msg) - verifier = PKCS1_v1_5.new(key) - return verifier.verify(h, sig) + try: + if isinstance(key, rsa.RSAPrivateKey): + key = key.public_key() + + key.verify(sig, msg, PKCS1v15(), self.digest) + return True + except InvalidSignature: + return False SIGNER_ALGS = { - SIG_RSA_SHA1: RSASigner(SHA), - SIG_RSA_SHA224: RSASigner(SHA224), - SIG_RSA_SHA256: RSASigner(SHA256), - SIG_RSA_SHA384: RSASigner(SHA384), - SIG_RSA_SHA512: RSASigner(SHA512), + SIG_RSA_SHA1: RSASigner(hashes.SHA1()), + SIG_RSA_SHA224: RSASigner(hashes.SHA224()), + SIG_RSA_SHA256: RSASigner(hashes.SHA256()), + SIG_RSA_SHA384: RSASigner(hashes.SHA384()), + SIG_RSA_SHA512: RSASigner(hashes.SHA512()), } REQ_ORDER = ["SAMLRequest", "RelayState", "SigAlg"] diff --git a/src/saml2/soap.py b/src/saml2/soap.py index c1be544f..055c690a 100644 --- a/src/saml2/soap.py +++ b/src/saml2/soap.py @@ -19,6 +19,7 @@ except ImportError: except ImportError: #noinspection PyUnresolvedReferences from elementtree import ElementTree +import defusedxml.ElementTree logger = logging.getLogger(__name__) @@ -133,7 +134,7 @@ def parse_soap_enveloped_saml_thingy(text, expected_tags): :param expected_tags: What the tag of the SAML thingy is expected to be. :return: SAML thingy as a string """ - envelope = ElementTree.fromstring(text) + envelope = defusedxml.ElementTree.fromstring(text) # Make sure it's a SOAP message assert envelope.tag == '{%s}Envelope' % soapenv.NAMESPACE @@ -183,7 +184,7 @@ def class_instances_from_soap_enveloped_saml_thingies(text, modules): :return: The body and headers as class instances """ try: - envelope = ElementTree.fromstring(text) + envelope = defusedxml.ElementTree.fromstring(text) except Exception as exc: raise XmlParseError("%s" % exc) @@ -209,7 +210,7 @@ def open_soap_envelope(text): :return: dictionary with two keys "body"/"header" """ try: - envelope = ElementTree.fromstring(text) + envelope = defusedxml.ElementTree.fromstring(text) except Exception as exc: raise XmlParseError("%s" % exc) diff --git a/tests/sp_conf_nameidpolicy.py b/tests/sp_conf_nameidpolicy.py new file mode 100644 index 00000000..d15989c2 --- /dev/null +++ b/tests/sp_conf_nameidpolicy.py @@ -0,0 +1,64 @@ +from pathutils import full_path +from pathutils import xmlsec_path + +CONFIG = { + "entityid": "urn:mace:example.com:saml:roland:sp", + "name": "urn:mace:example.com:saml:roland:sp", + "description": "My own SP", + "service": { + "sp": { + "endpoints": { + "assertion_consumer_service": [ + "http://lingon.catalogix.se:8087/"], + }, + "required_attributes": ["surName", "givenName", "mail"], + "optional_attributes": ["title"], + "idp": ["urn:mace:example.com:saml:roland:idp"], + "name_id_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + "name_id_format_allow_create": "true" + } + }, + "debug": 1, + "key_file": full_path("test.key"), + "cert_file": full_path("test.pem"), + "encryption_keypairs": [{"key_file": full_path("test_1.key"), "cert_file": full_path("test_1.crt")}, + {"key_file": full_path("test_2.key"), "cert_file": full_path("test_2.crt")}], + "ca_certs": full_path("cacerts.txt"), + "xmlsec_binary": xmlsec_path, + "metadata": [{ + "class": "saml2.mdstore.MetaDataFile", + "metadata": [(full_path("idp.xml"), ), (full_path("vo_metadata.xml"), )], + }], + "virtual_organization": { + "urn:mace:example.com:it:tek": { + "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID", + "common_identifier": "umuselin", + } + }, + "subject_data": "subject_data.db", + "accepted_time_diff": 60, + "attribute_map_dir": full_path("attributemaps"), + "valid_for": 6, + "organization": { + "name": ("AB Exempel", "se"), + "display_name": ("AB Exempel", "se"), + "url": "http://www.example.org", + }, + "contact_person": [{ + "given_name": "Roland", + "sur_name": "Hedberg", + "telephone_number": "+46 70 100 0000", + "email_address": ["tech@eample.com", + "tech@example.org"], + "contact_type": "technical" + }, + ], + "logger": { + "rotating": { + "filename": full_path("sp.log"), + "maxBytes": 100000, + "backupCount": 5, + }, + "loglevel": "info", + } +} diff --git a/tests/test_03_saml2.py b/tests/test_03_saml2.py index 136161ab..a71eb3cd 100644 --- a/tests/test_03_saml2.py +++ b/tests/test_03_saml2.py @@ -17,6 +17,7 @@ except ImportError: import cElementTree as ElementTree except ImportError: from elementtree import ElementTree +from defusedxml.common import EntitiesForbidden ITEMS = { NameID: ["""<?xml version="1.0" encoding="utf-8"?> @@ -166,6 +167,19 @@ def test_create_class_from_xml_string_wrong_class_spec(): assert kl == None +def test_create_class_from_xml_string_xxe(): + xml = """<?xml version="1.0"?> + <!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + ]> + <lolz>&lol1;</lolz> + """ + with raises(EntitiesForbidden) as err: + create_class_from_xml_string(NameID, xml) + + def test_ee_1(): ee = saml2.extension_element_from_string( """<?xml version='1.0' encoding='UTF-8'?><foo>bar</foo>""") @@ -454,6 +468,19 @@ def test_ee_7(): assert nid.text.strip() == "http://federationX.org" +def test_ee_xxe(): + xml = """<?xml version="1.0"?> + <!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + ]> + <lolz>&lol1;</lolz> + """ + with raises(EntitiesForbidden): + saml2.extension_element_from_string(xml) + + def test_extension_element_loadd(): ava = {'attributes': {}, 'tag': 'ExternalEntityAttributeAuthority', diff --git a/tests/test_43_soap.py b/tests/test_43_soap.py index 4cde3d6a..bf66a1d0 100755 --- a/tests/test_43_soap.py +++ b/tests/test_43_soap.py @@ -12,9 +12,13 @@ except ImportError: import cElementTree as ElementTree except ImportError: from elementtree import ElementTree +from defusedxml.common import EntitiesForbidden + +from pytest import raises import saml2.samlp as samlp from saml2.samlp import NAMESPACE as SAMLP_NAMESPACE +from saml2 import soap NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" @@ -66,3 +70,42 @@ def test_make_soap_envelope(): assert len(body) == 1 saml_part = body[0] assert saml_part.tag == '{%s}AuthnRequest' % SAMLP_NAMESPACE + + +def test_parse_soap_enveloped_saml_thingy_xxe(): + xml = """<?xml version="1.0"?> + <!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + ]> + <lolz>&lol1;</lolz> + """ + with raises(EntitiesForbidden): + soap.parse_soap_enveloped_saml_thingy(xml, None) + + +def test_class_instances_from_soap_enveloped_saml_thingies_xxe(): + xml = """<?xml version="1.0"?> + <!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + ]> + <lolz>&lol1;</lolz> + """ + with raises(soap.XmlParseError): + soap.class_instances_from_soap_enveloped_saml_thingies(xml, None) + + +def test_open_soap_envelope_xxe(): + xml = """<?xml version="1.0"?> + <!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + ]> + <lolz>&lol1;</lolz> + """ + with raises(soap.XmlParseError): + soap.open_soap_envelope(xml) diff --git a/tests/test_51_client.py b/tests/test_51_client.py index f6958162..7e42045b 100644 --- a/tests/test_51_client.py +++ b/tests/test_51_client.py @@ -7,6 +7,7 @@ import six from future.backports.urllib.parse import parse_qs from future.backports.urllib.parse import urlencode from future.backports.urllib.parse import urlparse +from pytest import raises from saml2.argtree import add_path from saml2.cert import OpenSSLWrapper @@ -25,6 +26,7 @@ from saml2.assertion import Assertion from saml2.authn_context import INTERNETPROTOCOLPASSWORD from saml2.client import Saml2Client from saml2.config import SPConfig +from saml2.pack import parse_soap_enveloped_saml from saml2.response import LogoutResponse from saml2.saml import NAMEID_FORMAT_PERSISTENT, EncryptedAssertion, Advice from saml2.saml import NAMEID_FORMAT_TRANSIENT @@ -38,6 +40,8 @@ from saml2.s_utils import do_attribute_statement from saml2.s_utils import factory from saml2.time_util import in_a_while, a_while_ago +from defusedxml.common import EntitiesForbidden + from fakeIDP import FakeIDP from fakeIDP import unpack_form from pathutils import full_path @@ -276,6 +280,26 @@ class TestClient: assert nid_policy.allow_create == "false" assert nid_policy.format == saml.NAMEID_FORMAT_TRANSIENT + def test_create_auth_request_nameid_policy_allow_create(self): + conf = config.SPConfig() + conf.load_file("sp_conf_nameidpolicy") + client = Saml2Client(conf) + ar_str = "%s" % client.create_authn_request( + "http://www.example.com/sso", message_id="id1")[1] + + ar = samlp.authn_request_from_string(ar_str) + print(ar) + assert ar.assertion_consumer_service_url == ("http://lingon.catalogix" + ".se:8087/") + assert ar.destination == "http://www.example.com/sso" + assert ar.protocol_binding == BINDING_HTTP_POST + assert ar.version == "2.0" + assert ar.provider_name == "urn:mace:example.com:saml:roland:sp" + assert ar.issuer.text == "urn:mace:example.com:saml:roland:sp" + nid_policy = ar.name_id_policy + assert nid_policy.allow_create == "true" + assert nid_policy.format == saml.NAMEID_FORMAT_PERSISTENT + def test_create_auth_request_vo(self): assert list(self.client.config.vorg.keys()) == [ "urn:mace:example.com:it:tek"] @@ -1552,6 +1576,17 @@ class TestClientWithDummy(): 'http://www.example.com/login' assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD +def test_parse_soap_enveloped_saml_xxe(): + xml = """<?xml version="1.0"?> + <!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + ]> + <lolz>&lol1;</lolz> + """ + with raises(EntitiesForbidden): + parse_soap_enveloped_saml(xml, None) # if __name__ == "__main__": # tc = TestClient() diff --git a/tests/test_requirements.txt b/tests/test_requirements.txt index 537fccdf..33301079 100644 --- a/tests/test_requirements.txt +++ b/tests/test_requirements.txt @@ -1,3 +1,5 @@ +mock==2.0.0 pymongo==3.0.1 +pytest==3.0.3 responses==0.5.0 -mock
\ No newline at end of file +pyasn1==0.2.3 @@ -2,6 +2,5 @@ envlist = py27,py34 [testenv] -deps = pytest - -rtests/test_requirements.txt +deps = -rtests/test_requirements.txt commands = py.test tests/ |