summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoland Hedberg <roland@catalogix.se>2017-04-24 15:46:35 +0200
committerRoland Hedberg <roland@catalogix.se>2017-04-24 15:46:35 +0200
commit91bce29d7fd66537797cefa554f63edd0e404ce1 (patch)
treeddbfeeb2718d88fe5ac97b4be65855e402f8a182
parente2adbb523c37643453fbe62469ca7fb2bf1b43f0 (diff)
parent47e1b34f6c6fd9a48405f1560f24619d77526b3a (diff)
downloadpysaml2-91bce29d7fd66537797cefa554f63edd0e404ce1.tar.gz
Merge branch 'master' of github.com:rohe/pysaml2
-rw-r--r--.gitignore4
-rw-r--r--README.rst14
-rwxr-xr-xsetup.py3
-rw-r--r--src/saml2/__init__.py7
-rw-r--r--src/saml2/cert.py37
-rw-r--r--src/saml2/client_base.py15
-rw-r--r--src/saml2/config.py15
-rw-r--r--src/saml2/entity.py3
-rw-r--r--src/saml2/mdstore.py12
-rw-r--r--src/saml2/pack.py3
-rw-r--r--src/saml2/s_utils.py85
-rw-r--r--src/saml2/sigver.py86
-rw-r--r--src/saml2/soap.py7
-rw-r--r--tests/sp_conf_nameidpolicy.py64
-rw-r--r--tests/test_03_saml2.py27
-rwxr-xr-xtests/test_43_soap.py43
-rw-r--r--tests/test_51_client.py35
-rw-r--r--tests/test_requirements.txt4
-rw-r--r--tox.ini3
19 files changed, 273 insertions, 194 deletions
diff --git a/.gitignore b/.gitignore
index 65bebc86..0cfb7720 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.rst b/README.rst
index f6abb5df..6aa3e23f 100644
--- a/README.rst
+++ b/README.rst
@@ -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/>`_.
diff --git a/setup.py b/setup.py
index 8d392070..b29c31ca 100755
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tox.ini b/tox.ini
index 327f4c15..d834512d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,6 +2,5 @@
envlist = py27,py34
[testenv]
-deps = pytest
- -rtests/test_requirements.txt
+deps = -rtests/test_requirements.txt
commands = py.test tests/