Merge branch 'master' of github.com:rohe/pysaml2
This commit is contained in:
commit
91bce29d7f
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
|
||||
|
14
README.rst
14
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/>`_.
|
||||
|
3
setup.py
3
setup.py
@ -14,10 +14,11 @@ install_requires = [
|
||||
'paste',
|
||||
'zope.interface',
|
||||
'repoze.who',
|
||||
'pycryptodomex',
|
||||
'cryptography',
|
||||
'pytz',
|
||||
'pyOpenSSL',
|
||||
'python-dateutil',
|
||||
'defusedxml',
|
||||
'six'
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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"):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -1,33 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import random
|
||||
|
||||
import time
|
||||
import base64
|
||||
import six
|
||||
import sys
|
||||
import hmac
|
||||
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())
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
||||
|
64
tests/sp_conf_nameidpolicy.py
Normal file
64
tests/sp_conf_nameidpolicy.py
Normal file
@ -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",
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -1,3 +1,5 @@
|
||||
mock==2.0.0
|
||||
pymongo==3.0.1
|
||||
pytest==3.0.3
|
||||
responses==0.5.0
|
||||
mock
|
||||
pyasn1==0.2.3
|
||||
|
Loading…
Reference in New Issue
Block a user