Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -21,6 +21,10 @@ To make it easy, for me :-), both the IdP and the SP uses the same keys.
|
||||
|
||||
To run the setup do
|
||||
|
||||
./run.sh
|
||||
./all.sh start
|
||||
|
||||
and then use your favourit webbrowser to look at "http://localhost:8087/whoami"
|
||||
|
||||
./all stop
|
||||
|
||||
will of course stop your IdP and SP.
|
37
example/all.sh
Executable file
37
example/all.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
|
||||
startme() {
|
||||
cd sp-wsgi
|
||||
if [ ! -f conf.py ] ; then
|
||||
cp conf.py.example conf.py
|
||||
fi
|
||||
../../tools/make_metadata.py conf > sp.xml
|
||||
|
||||
cd ../idp2
|
||||
if [ ! -f idp_conf.py ] ; then
|
||||
cp idp_conf.py.example conf.py
|
||||
fi
|
||||
../../tools/make_metadata.py idp_conf > idp.xml
|
||||
|
||||
cd ../sp-wsgi
|
||||
./sp.py conf &
|
||||
|
||||
cd ../idp2
|
||||
./idp.py idp_conf &
|
||||
|
||||
cd ..
|
||||
}
|
||||
|
||||
stopme() {
|
||||
pkill -f "sp.py"
|
||||
pkill -f "idp.py"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start) startme ;;
|
||||
stop) stopme ;;
|
||||
restart) stopme; startme ;;
|
||||
*) echo "usage: $0 start|stop|restart" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd sp
|
||||
../../tools/make_metadata.py sp_conf > sp.xml
|
||||
|
||||
cd ../idp2
|
||||
../../tools/make_metadata.py idp_conf > idp.xml
|
||||
|
||||
cd ../sp
|
||||
./sp.py sp_conf &
|
||||
|
||||
cd ../idp2
|
||||
./idp.py idp_conf &
|
||||
|
||||
cd ..
|
||||
|
@@ -35,7 +35,5 @@ CONFIG = {
|
||||
"cert_file": "pki/mycert.pem",
|
||||
"xmlsec_binary": xmlsec_path,
|
||||
"metadata": {"local": ["../idp2/idp.xml"]},
|
||||
#"metadata": {"mdfile": ["./swamid2.md"]},
|
||||
#"metadata": {"local": ["./swamid-2.0.xml"]},
|
||||
"name_form": NAME_FORMAT_URI,
|
||||
}
|
2
setup.py
2
setup.py
@@ -66,7 +66,7 @@ if sys.version_info < (2, 7):
|
||||
|
||||
setup(
|
||||
name='pysaml2',
|
||||
version='2.0.0beta',
|
||||
version='2.0.1beta',
|
||||
description='Python implementation of SAML Version 2 to be used in a WSGI environment',
|
||||
# long_description = read("README"),
|
||||
author='Roland Hedberg',
|
||||
|
@@ -24,7 +24,7 @@ import xmlenc
|
||||
from saml2 import saml
|
||||
|
||||
from saml2.time_util import instant, in_a_while
|
||||
from saml2.attribute_converter import from_local
|
||||
from saml2.attribute_converter import from_local, get_local_name
|
||||
from saml2.s_utils import sid, MissingValue
|
||||
from saml2.s_utils import factory
|
||||
from saml2.s_utils import assertion_factory
|
||||
@@ -78,7 +78,7 @@ def _match(attr, ava):
|
||||
return None
|
||||
|
||||
|
||||
def filter_on_attributes(ava, required=None, optional=None):
|
||||
def filter_on_attributes(ava, required=None, optional=None, acs=None):
|
||||
""" Filter
|
||||
|
||||
:param ava: An attribute value assertion as a dictionary
|
||||
@@ -93,27 +93,33 @@ def filter_on_attributes(ava, required=None, optional=None):
|
||||
if required is None:
|
||||
required = []
|
||||
|
||||
nform = "friendly_name"
|
||||
for attr in required:
|
||||
found = False
|
||||
nform = ""
|
||||
for nform in ["friendly_name", "name"]:
|
||||
try:
|
||||
_fn = _match(attr[nform], ava)
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
_name = attr[nform]
|
||||
except KeyError:
|
||||
if nform == "friendly_name":
|
||||
_name = get_local_name(acs, attr["name"],
|
||||
attr["name_format"])
|
||||
else:
|
||||
if _fn:
|
||||
try:
|
||||
values = [av["text"] for av in attr["attribute_value"]]
|
||||
except KeyError:
|
||||
values = []
|
||||
res[_fn] = _filter_values(ava[_fn], values, True)
|
||||
found = True
|
||||
break
|
||||
continue
|
||||
|
||||
if not found:
|
||||
raise MissingValue("Required attribute missing: '%s'" % (
|
||||
attr[nform],))
|
||||
_fn = _match(_name, ava)
|
||||
if not _fn: # In the unlikely case that someone has provided us
|
||||
# with URIs as attribute names
|
||||
_fn = _match(attr["name"], ava)
|
||||
|
||||
if _fn:
|
||||
try:
|
||||
values = [av["text"] for av in attr["attribute_value"]]
|
||||
except KeyError:
|
||||
values = []
|
||||
res[_fn] = _filter_values(ava[_fn], values, True)
|
||||
continue
|
||||
else:
|
||||
desc = "Required attribute missing: '%s' (%s)" % (attr["name"],
|
||||
_name)
|
||||
raise MissingValue(desc)
|
||||
|
||||
if optional is None:
|
||||
optional = []
|
||||
@@ -311,6 +317,7 @@ class Policy(object):
|
||||
self.compile(restrictions)
|
||||
else:
|
||||
self._restrictions = None
|
||||
self.acs = []
|
||||
|
||||
def compile(self, restrictions):
|
||||
""" This is only for IdPs or AAs, and it's about limiting what
|
||||
@@ -484,7 +491,8 @@ class Policy(object):
|
||||
ava = filter_attribute_value_assertions(ava, _rest)
|
||||
|
||||
if required or optional:
|
||||
ava = filter_on_attributes(ava, required, optional)
|
||||
logger.debug("required: %s, optional: %s" % (required, optional))
|
||||
ava = filter_on_attributes(ava, required, optional, self.acs)
|
||||
|
||||
return ava
|
||||
|
||||
@@ -540,8 +548,10 @@ class Assertion(dict):
|
||||
|
||||
def __init__(self, dic=None):
|
||||
dict.__init__(self, dic)
|
||||
self.acs = []
|
||||
|
||||
def _authn_context_decl(self, decl, authn_auth=None):
|
||||
@staticmethod
|
||||
def _authn_context_decl(decl, authn_auth=None):
|
||||
"""
|
||||
Construct the authn context with a authn context declaration
|
||||
:param decl: The authn context declaration
|
||||
@@ -726,6 +736,8 @@ class Assertion(dict):
|
||||
:param metadata: Metadata to use
|
||||
:return: The resulting AVA after the policy is applied
|
||||
"""
|
||||
|
||||
policy.acs = self.acs
|
||||
ava = policy.restrict(self, sp_entity_id, metadata)
|
||||
self.update(ava)
|
||||
return ava
|
@@ -255,6 +255,13 @@ def to_local_name(acs, attr):
|
||||
return attr.friendly_name
|
||||
|
||||
|
||||
def get_local_name(acs, attr, name_format):
|
||||
for aconv in acs:
|
||||
#print ac.format, name_format
|
||||
if aconv.name_format == name_format:
|
||||
return aconv._fro[attr]
|
||||
|
||||
|
||||
def d_to_local_name(acs, attr):
|
||||
"""
|
||||
:param acs: List of AttributeConverter instances
|
||||
|
@@ -177,6 +177,7 @@ MAP = {
|
||||
'edupersonaffiliation': EDUPERSON_OID+'1',
|
||||
'eduPersonPrincipalName': EDUPERSON_OID+'6',
|
||||
'edupersonprincipalname': EDUPERSON_OID+'6',
|
||||
'eppn': EDUPERSON_OID+'6',
|
||||
'localityName': X500ATTR_OID+'7',
|
||||
'owner': X500ATTR_OID+'32',
|
||||
'norEduOrgUnitUniqueNumber': NOREDUPERSON_OID+'2',
|
||||
|
@@ -9,7 +9,6 @@ from os.path import join
|
||||
from os import remove
|
||||
from Crypto.Util import asn1
|
||||
|
||||
|
||||
class WrongInput(Exception):
|
||||
pass
|
||||
|
||||
@@ -23,55 +22,82 @@ class PayloadError(Exception):
|
||||
|
||||
|
||||
class OpenSSLWrapper(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def create_certificate(self, cert_info, request=False, valid_from=0, valid_to=315360000, sn=1, key_length=1024,
|
||||
hash_alg="sha256", write_to_file=False, cert_dir="", cipher_passphrase = None):
|
||||
def create_certificate(self, cert_info, request=False, valid_from=0,
|
||||
valid_to=315360000, sn=1, key_length=1024,
|
||||
hash_alg="sha256", write_to_file=False, cert_dir="",
|
||||
cipher_passphrase=None):
|
||||
"""
|
||||
Can create certificate requests, to be signed later by another certificate with the method
|
||||
Can create certificate requests, to be signed later by another
|
||||
certificate with the method
|
||||
create_cert_signed_certificate. If request is True.
|
||||
|
||||
Can also create self signed root certificates if request is False. This is default behaviour.
|
||||
Can also create self signed root certificates if request is False.
|
||||
This is default behaviour.
|
||||
|
||||
:param cert_info: Contains information about the certificate.
|
||||
Is a dictionary that must contain the keys:
|
||||
cn = Common name. This part must match the host being authenticated
|
||||
country_code = Two letter description of the country.
|
||||
cn = Common name. This part
|
||||
must match the host being authenticated
|
||||
country_code = Two letter description
|
||||
of the country.
|
||||
state = State
|
||||
city = City
|
||||
organization = Organization, can be a company name.
|
||||
organization_unit = A unit at the organization, can be a department.
|
||||
organization = Organization, can be a
|
||||
company name.
|
||||
organization_unit = A unit at the
|
||||
organization, can be a department.
|
||||
Example:
|
||||
cert_info_ca = {
|
||||
"cn": "company.com",
|
||||
"country_code": "se",
|
||||
"state": "AC",
|
||||
"city": "Dorotea",
|
||||
"organization": "Company",
|
||||
"organization_unit": "Sales"
|
||||
"organization":
|
||||
"Company",
|
||||
"organization_unit":
|
||||
"Sales"
|
||||
}
|
||||
:param request: True if this is a request for certificate, that should be signed.
|
||||
False if this is a self signed certificate, root certificate.
|
||||
:param valid_from: When the certificate starts to be valid. Amount of seconds from when the
|
||||
:param request: True if this is a request for certificate,
|
||||
that should be signed.
|
||||
False if this is a self signed certificate,
|
||||
root certificate.
|
||||
:param valid_from: When the certificate starts to be valid.
|
||||
Amount of seconds from when the
|
||||
certificate is generated.
|
||||
:param valid_to: How long the certificate will be valid from when it is generated.
|
||||
The value is in seconds. Default is 315360000 seconds, a.k.a 10 years.
|
||||
:param sn: Serial number for the certificate. Default is 1.
|
||||
:param key_length: Length of the key to be generated. Defaults to 1024.
|
||||
:param hash_alg: Hash algorithm to use for the key. Default is sha256.
|
||||
:param write_to_file: True if you want to write the certificate to a file. The method will then return
|
||||
a tuple with path to certificate file and path to key file.
|
||||
False if you want to get the result as strings. The method will then return a tuple
|
||||
with the certificate string and the key as string.
|
||||
WILL OVERWRITE ALL EXISTING FILES WITHOUT ASKING!
|
||||
:param cert_dir: Where to save the files if write_to_file is true.
|
||||
:param cipher_passphrase A dictionary with cipher and passphrase. Example:
|
||||
{"cipher": "blowfish", "passphrase": "qwerty"}
|
||||
:return: string representation of certificate, string representation of private key
|
||||
:param valid_to: How long the certificate will be valid from
|
||||
when it is generated.
|
||||
The value is in seconds. Default is
|
||||
315360000 seconds, a.k.a 10 years.
|
||||
:param sn: Serial number for the certificate. Default
|
||||
is 1.
|
||||
:param key_length: Length of the key to be generated. Defaults
|
||||
to 1024.
|
||||
:param hash_alg: Hash algorithm to use for the key. Default
|
||||
is sha256.
|
||||
:param write_to_file: True if you want to write the certificate
|
||||
to a file. The method will then return
|
||||
a tuple with path to certificate file and
|
||||
path to key file.
|
||||
False if you want to get the result as
|
||||
strings. The method will then return a tuple
|
||||
with the certificate string and the key as
|
||||
string.
|
||||
WILL OVERWRITE ALL EXISTING FILES WITHOUT
|
||||
ASKING!
|
||||
:param cert_dir: Where to save the files if write_to_file is
|
||||
true.
|
||||
:param cipher_passphrase A dictionary with cipher and passphrase.
|
||||
Example::
|
||||
{"cipher": "blowfish", "passphrase": "qwerty"}
|
||||
|
||||
:return: string representation of certificate,
|
||||
string representation of private key
|
||||
if write_to_file parameter is False otherwise
|
||||
path to certificate file, path to private key file
|
||||
path to certificate file, path to private
|
||||
key file
|
||||
"""
|
||||
cn = cert_info["cn"]
|
||||
|
||||
@@ -97,7 +123,7 @@ class OpenSSLWrapper(object):
|
||||
k = crypto.PKey()
|
||||
k.generate_key(crypto.TYPE_RSA, key_length)
|
||||
|
||||
# create a self-signed cert
|
||||
# create a self-signed cert
|
||||
cert = crypto.X509()
|
||||
|
||||
if request:
|
||||
@@ -113,8 +139,8 @@ class OpenSSLWrapper(object):
|
||||
cert.get_subject().CN = cn
|
||||
if not request:
|
||||
cert.set_serial_number(sn)
|
||||
cert.gmtime_adj_notBefore(valid_from) #Valid before present time
|
||||
cert.gmtime_adj_notAfter(valid_to) #3 650 days
|
||||
cert.gmtime_adj_notBefore(valid_from) #Valid before present time
|
||||
cert.gmtime_adj_notAfter(valid_to) #3 650 days
|
||||
cert.set_issuer(cert.get_subject())
|
||||
cert.set_pubkey(k)
|
||||
cert.sign(k, hash_alg)
|
||||
@@ -122,13 +148,16 @@ class OpenSSLWrapper(object):
|
||||
filesCreated = False
|
||||
try:
|
||||
if request:
|
||||
tmp_cert = crypto.dump_certificate_request(crypto.FILETYPE_PEM, cert)
|
||||
tmp_cert = crypto.dump_certificate_request(crypto.FILETYPE_PEM,
|
||||
cert)
|
||||
else:
|
||||
tmp_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
tmp_key = None
|
||||
if cipher_passphrase is not None:
|
||||
tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k, cipher_passphrase["cipher"],
|
||||
cipher_passphrase["passphrase"])
|
||||
tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k,
|
||||
cipher_passphrase["cipher"],
|
||||
cipher_passphrase[
|
||||
"passphrase"])
|
||||
else:
|
||||
tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
|
||||
if write_to_file:
|
||||
@@ -172,36 +201,52 @@ class OpenSSLWrapper(object):
|
||||
return base64.b64encode(str(str_data))
|
||||
|
||||
|
||||
def create_cert_signed_certificate(self, sign_cert_str, sign_key_str, request_cert_str, hash_alg="sha256",
|
||||
valid_from=0, valid_to=315360000, sn=1, passphrase=None):
|
||||
def create_cert_signed_certificate(self, sign_cert_str, sign_key_str,
|
||||
request_cert_str, hash_alg="sha256",
|
||||
valid_from=0, valid_to=315360000, sn=1,
|
||||
passphrase=None):
|
||||
|
||||
"""
|
||||
Will sign a certificate request with a give certificate.
|
||||
:param sign_cert_str: This certificate will be used to sign with. Must be a string representation of
|
||||
the certificate. If you only have a file use the method read_str_from_file to
|
||||
:param sign_cert_str: This certificate will be used to sign with.
|
||||
Must be a string representation of
|
||||
the certificate. If you only have a file
|
||||
use the method read_str_from_file to
|
||||
get a string representation.
|
||||
:param sign_key_str: This is the key for the ca_cert_str represented as a string.
|
||||
If you only have a file use the method read_str_from_file to get a string
|
||||
:param sign_key_str: This is the key for the ca_cert_str
|
||||
represented as a string.
|
||||
If you only have a file use the method
|
||||
read_str_from_file to get a string
|
||||
representation.
|
||||
:param request_cert_str: This is the prepared certificate to be signed. Must be a string representation of
|
||||
the requested certificate. If you only have a file use the method read_str_from_file
|
||||
:param request_cert_str: This is the prepared certificate to be
|
||||
signed. Must be a string representation of
|
||||
the requested certificate. If you only have
|
||||
a file use the method read_str_from_file
|
||||
to get a string representation.
|
||||
:param hash_alg: Hash algorithm to use for the key. Default is sha256.
|
||||
:param valid_from: When the certificate starts to be valid. Amount of seconds from when the
|
||||
:param hash_alg: Hash algorithm to use for the key. Default
|
||||
is sha256.
|
||||
:param valid_from: When the certificate starts to be valid.
|
||||
Amount of seconds from when the
|
||||
certificate is generated.
|
||||
:param valid_to: How long the certificate will be valid from when it is generated.
|
||||
The value is in seconds. Default is 315360000 seconds, a.k.a 10 years.
|
||||
:param sn: Serial number for the certificate. Default is 1.
|
||||
:param valid_to: How long the certificate will be valid from
|
||||
when it is generated.
|
||||
The value is in seconds. Default is
|
||||
315360000 seconds, a.k.a 10 years.
|
||||
:param sn: Serial number for the certificate. Default
|
||||
is 1.
|
||||
:param passphrase: Password for the private key in sign_key_str.
|
||||
:return: String representation of the signed certificate.
|
||||
:return: String representation of the signed
|
||||
certificate.
|
||||
"""
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, sign_cert_str)
|
||||
ca_key = None
|
||||
if passphrase is not None:
|
||||
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str, passphrase)
|
||||
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str,
|
||||
passphrase)
|
||||
else:
|
||||
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str)
|
||||
req_cert = crypto.load_certificate_request(crypto.FILETYPE_PEM, request_cert_str)
|
||||
req_cert = crypto.load_certificate_request(crypto.FILETYPE_PEM,
|
||||
request_cert_str)
|
||||
|
||||
cert = crypto.X509()
|
||||
cert.set_subject(req_cert.get_subject())
|
||||
@@ -217,7 +262,8 @@ class OpenSSLWrapper(object):
|
||||
def verify_chain(self, cert_chain_str_list, cert_str):
|
||||
"""
|
||||
|
||||
:param cert_chain_str_list: Must be a list of certificate strings, where the first certificate to be validate
|
||||
:param cert_chain_str_list: Must be a list of certificate strings,
|
||||
where the first certificate to be validate
|
||||
is in the beginning and the root certificate is last.
|
||||
:param cert_str: The certificate to be validated.
|
||||
:return:
|
||||
@@ -229,7 +275,8 @@ class OpenSSLWrapper(object):
|
||||
else:
|
||||
cert_str = tmp_cert_str
|
||||
return (True,
|
||||
"Signed certificate is valid and correctly signed by CA certificate.")
|
||||
"Signed certificate is valid and correctly signed by CA "
|
||||
"certificate.")
|
||||
|
||||
def certificate_not_valid_yet(self, cert):
|
||||
starts_to_be_valid = dateutil.parser.parse(cert.get_notBefore())
|
||||
@@ -243,18 +290,24 @@ class OpenSSLWrapper(object):
|
||||
"""
|
||||
Verifies if a certificate is valid and signed by a given certificate.
|
||||
|
||||
:param signing_cert_str: This certificate will be used to verify the signature. Must be a string representation
|
||||
of the certificate. If you only have a file use the method read_str_from_file to
|
||||
:param signing_cert_str: This certificate will be used to verify the
|
||||
signature. Must be a string representation
|
||||
of the certificate. If you only have a file
|
||||
use the method read_str_from_file to
|
||||
get a string representation.
|
||||
:param cert_str: This certificate will be verified if it is correct. Must be a string representation
|
||||
of the certificate. If you only have a file use the method read_str_from_file to
|
||||
:param cert_str: This certificate will be verified if it is
|
||||
correct. Must be a string representation
|
||||
of the certificate. If you only have a file
|
||||
use the method read_str_from_file to
|
||||
get a string representation.
|
||||
:return: Valid, Message
|
||||
Valid = True if the certificate is valid, otherwise false.
|
||||
Valid = True if the certificate is valid,
|
||||
otherwise false.
|
||||
Message = Why the validation failed.
|
||||
"""
|
||||
try:
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, signing_cert_str)
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM,
|
||||
signing_cert_str)
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str)
|
||||
|
||||
if self.certificate_not_valid_yet(ca_cert):
|
||||
@@ -270,7 +323,8 @@ class OpenSSLWrapper(object):
|
||||
return False, "The signed certificate is not valid yet."
|
||||
|
||||
if ca_cert.get_subject().CN == cert.get_subject().CN:
|
||||
return False, "CN may not be equal for CA certificate and the signed certificate."
|
||||
return False, ("CN may not be equal for CA certificate and the "
|
||||
"signed certificate.")
|
||||
|
||||
cert_algorithm = cert.get_signature_algorithm()
|
||||
|
||||
@@ -279,9 +333,9 @@ class OpenSSLWrapper(object):
|
||||
der_seq = asn1.DerSequence()
|
||||
der_seq.decode(cert_asn1)
|
||||
|
||||
cert_certificate=der_seq[0]
|
||||
cert_certificate = der_seq[0]
|
||||
#cert_signature_algorithm=der_seq[1]
|
||||
cert_signature=der_seq[2]
|
||||
cert_signature = der_seq[2]
|
||||
|
||||
cert_signature_decoded = asn1.DerObject()
|
||||
cert_signature_decoded.decode(cert_signature)
|
||||
@@ -289,12 +343,14 @@ class OpenSSLWrapper(object):
|
||||
signature_payload = cert_signature_decoded.payload
|
||||
|
||||
if signature_payload[0] != '\x00':
|
||||
return False, "The certificate should not contain any unused bits."
|
||||
return (False,
|
||||
"The certificate should not contain any unused bits.")
|
||||
|
||||
signature = signature_payload[1:]
|
||||
|
||||
try:
|
||||
crypto.verify(ca_cert, signature, cert_certificate, cert_algorithm)
|
||||
crypto.verify(ca_cert, signature, cert_certificate,
|
||||
cert_algorithm)
|
||||
return True, "Signed certificate is valid and correctly signed by CA certificate."
|
||||
except crypto.Error, e:
|
||||
return False, "Certificate is incorrectly signed."
|
||||
|
@@ -243,7 +243,7 @@ class Base(Entity):
|
||||
try:
|
||||
args["assertion_consumer_service_url"] = kwargs[
|
||||
"assertion_consumer_service_url"]
|
||||
del kwargs["assertion_consumer_service_urls"]
|
||||
del kwargs["assertion_consumer_service_url"]
|
||||
except KeyError:
|
||||
try:
|
||||
args["attribute_consuming_service_index"] = str(kwargs[
|
||||
@@ -301,6 +301,9 @@ class Base(Entity):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if sign is None:
|
||||
sign = self.authn_requests_signed
|
||||
|
||||
if (sign and self.sec.cert_handler.generate_cert()) or \
|
||||
client_crt is not None:
|
||||
with self.lock:
|
||||
|
@@ -62,7 +62,8 @@ class DiscoveryServer(Entity):
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def create_discovery_service_response(self, return_url=None,
|
||||
@staticmethod
|
||||
def create_discovery_service_response(return_url=None,
|
||||
returnIDParam="entityID",
|
||||
entity_id=None, **kwargs):
|
||||
if return_url is None:
|
||||
@@ -87,3 +88,13 @@ class DiscoveryServer(Entity):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def verify_return(self, entity_id, return_url):
|
||||
for endp in self.metadata.discovery_response(entity_id):
|
||||
try:
|
||||
assert return_url.startswith(endp["location"])
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
@@ -23,6 +23,7 @@ from saml2.saml import NameID
|
||||
from saml2.saml import Issuer
|
||||
from saml2.saml import NAMEID_FORMAT_ENTITY
|
||||
from saml2.response import LogoutResponse
|
||||
from saml2.response import UnsolicitedResponse
|
||||
from saml2.time_util import instant
|
||||
from saml2.s_utils import sid
|
||||
from saml2.s_utils import UnravelError
|
||||
@@ -839,11 +840,14 @@ class Entity(HTTPBase):
|
||||
response = response.loads(xmlstr, False, origxml=origxml)
|
||||
except SigverError, err:
|
||||
logger.error("Signature Error: %s" % err)
|
||||
return None
|
||||
raise
|
||||
except UnsolicitedResponse:
|
||||
logger.error("Unsolicited response")
|
||||
raise
|
||||
except Exception, err:
|
||||
if "not well-formed" in "%s" % err:
|
||||
logger.error("Not well-formed XML")
|
||||
return None
|
||||
raise
|
||||
|
||||
logger.debug("XMLSTR: %s" % xmlstr)
|
||||
|
||||
|
@@ -67,11 +67,20 @@ def _since_epoch(cdate):
|
||||
if len(cdate) < 5:
|
||||
return utc_now()
|
||||
|
||||
cdate = cdate[5:]
|
||||
cdate = cdate[5:] # assume short weekday, i.e. do not support obsolete RFC 1036 date format
|
||||
try:
|
||||
t = time.strptime(cdate, "%d-%b-%Y %H:%M:%S %Z")
|
||||
t = time.strptime(cdate, "%d-%b-%Y %H:%M:%S %Z") # e.g. 18-Apr-2014 12:30:51 GMT
|
||||
except ValueError:
|
||||
t = time.strptime(cdate, "%d-%b-%y %H:%M:%S %Z")
|
||||
try:
|
||||
t = time.strptime(cdate, "%d-%b-%y %H:%M:%S %Z") # e.g. 18-Apr-14 12:30:51 GMT
|
||||
except ValueError:
|
||||
try:
|
||||
t = time.strptime(cdate, "%d %b %Y %H:%M:%S %Z") # e.g. 18 Apr 2014 12:30:51 GMT
|
||||
except ValueError:
|
||||
raise Exception, 'ValueError: Date "{0}" does not match any of '.format(cdate) + \
|
||||
'"%d-%b-%Y %H:%M:%S %Z", ' + \
|
||||
'"%d-%b-%y %H:%M:%S %Z", ' + \
|
||||
'"%d %b %Y %H:%M:%S %Z".'
|
||||
#return int(time.mktime(t))
|
||||
return calendar.timegm(t)
|
||||
|
||||
|
@@ -456,13 +456,13 @@ def do_spsso_descriptor(conf, cert=None):
|
||||
ENDPOINTS["sp"]).items():
|
||||
setattr(spsso, endpoint, instlist)
|
||||
|
||||
# ext = do_endpoints(endps, ENDPOINT_EXT["sp"])
|
||||
# if ext:
|
||||
# if spsso.extensions is None:
|
||||
# spsso.extensions = md.Extensions()
|
||||
# for vals in ext.values():
|
||||
# for val in vals:
|
||||
# spsso.extensions.add_extension_element(val)
|
||||
ext = do_endpoints(endps, ENDPOINT_EXT["sp"])
|
||||
if ext:
|
||||
if spsso.extensions is None:
|
||||
spsso.extensions = md.Extensions()
|
||||
for vals in ext.values():
|
||||
for val in vals:
|
||||
spsso.extensions.add_extension_element(val)
|
||||
|
||||
if cert:
|
||||
encryption_type = conf.encryption_type
|
||||
|
@@ -204,7 +204,7 @@ def _dummy(_):
|
||||
def for_me(conditions, myself):
|
||||
""" Am I among the intended audiences """
|
||||
|
||||
if not conditions.audience_restriction: # No audience restriction
|
||||
if not conditions.audience_restriction: # No audience restriction
|
||||
return True
|
||||
|
||||
for restriction in conditions.audience_restriction:
|
||||
@@ -221,7 +221,8 @@ def for_me(conditions, myself):
|
||||
|
||||
|
||||
def authn_response(conf, return_addrs, outstanding_queries=None, timeslack=0,
|
||||
asynchop=True, allow_unsolicited=False, want_assertions_signed=False):
|
||||
asynchop=True, allow_unsolicited=False,
|
||||
want_assertions_signed=False):
|
||||
sec = security_context(conf)
|
||||
if not timeslack:
|
||||
try:
|
||||
@@ -319,7 +320,8 @@ class StatusResponse(object):
|
||||
logger.debug("xmlstr: %s" % (self.xmlstr,))
|
||||
|
||||
try:
|
||||
self.response = self.signature_check(xmldata, origdoc=origxml, must=self.require_signature,
|
||||
self.response = self.signature_check(xmldata, origdoc=origxml,
|
||||
must=self.require_signature,
|
||||
require_response_signature=self.require_response_signature)
|
||||
|
||||
except TypeError:
|
||||
@@ -369,7 +371,7 @@ class StatusResponse(object):
|
||||
|
||||
def _verify(self):
|
||||
if self.request_id and self.in_response_to and \
|
||||
self.in_response_to != self.request_id:
|
||||
self.in_response_to != self.request_id:
|
||||
logger.error("Not the id I expected: %s != %s" % (
|
||||
self.in_response_to, self.request_id))
|
||||
return None
|
||||
@@ -385,9 +387,9 @@ class StatusResponse(object):
|
||||
|
||||
if self.asynchop:
|
||||
if self.response.destination and \
|
||||
self.response.destination not in self.return_addrs:
|
||||
self.response.destination not in self.return_addrs:
|
||||
logger.error("%s not in %s" % (self.response.destination,
|
||||
self.return_addrs))
|
||||
self.return_addrs))
|
||||
return None
|
||||
|
||||
assert self.issue_instant_ok()
|
||||
@@ -430,7 +432,8 @@ class NameIDMappingResponse(StatusResponse):
|
||||
request_id=0, asynchop=True):
|
||||
StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
|
||||
request_id, asynchop)
|
||||
self.signature_check = self.sec.correctly_signed_name_id_mapping_response
|
||||
self.signature_check = self.sec\
|
||||
.correctly_signed_name_id_mapping_response
|
||||
|
||||
|
||||
class ManageNameIDResponse(StatusResponse):
|
||||
@@ -455,7 +458,8 @@ class AuthnResponse(StatusResponse):
|
||||
return_addrs=None, outstanding_queries=None,
|
||||
timeslack=0, asynchop=True, allow_unsolicited=False,
|
||||
test=False, allow_unknown_attributes=False,
|
||||
want_assertions_signed=False, want_response_signed=False, **kwargs):
|
||||
want_assertions_signed=False, want_response_signed=False,
|
||||
**kwargs):
|
||||
|
||||
StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
|
||||
asynchop=asynchop)
|
||||
@@ -481,6 +485,16 @@ class AuthnResponse(StatusResponse):
|
||||
except KeyError:
|
||||
self.extension_schema = {}
|
||||
|
||||
def check_subject_confirmation_in_response_to(self, irp):
|
||||
for assertion in self.response.assertion:
|
||||
for _sc in assertion.subject.subject_confirmation:
|
||||
try:
|
||||
assert _sc.subject_confirmation_data.in_response_to == irp
|
||||
except AssertionError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def loads(self, xmldata, decode=True, origxml=None):
|
||||
self._loads(xmldata, decode, origxml)
|
||||
|
||||
@@ -488,11 +502,22 @@ class AuthnResponse(StatusResponse):
|
||||
if self.in_response_to in self.outstanding_queries:
|
||||
self.came_from = self.outstanding_queries[self.in_response_to]
|
||||
del self.outstanding_queries[self.in_response_to]
|
||||
try:
|
||||
if not self.check_subject_confirmation_in_response_to(
|
||||
self.in_response_to):
|
||||
logger.exception(
|
||||
"Unsolicited response %s" % self.in_response_to)
|
||||
raise UnsolicitedResponse(
|
||||
"Unsolicited response: %s" % self.in_response_to)
|
||||
except AttributeError:
|
||||
pass
|
||||
elif self.allow_unsolicited:
|
||||
pass
|
||||
else:
|
||||
logger.exception("Unsolicited response %s" % self.in_response_to)
|
||||
raise UnsolicitedResponse("Unsolicited response: %s" % self.in_response_to)
|
||||
logger.exception(
|
||||
"Unsolicited response %s" % self.in_response_to)
|
||||
raise UnsolicitedResponse(
|
||||
"Unsolicited response: %s" % self.in_response_to)
|
||||
|
||||
return self
|
||||
|
||||
@@ -541,7 +566,8 @@ class AuthnResponse(StatusResponse):
|
||||
|
||||
# if both are present NotBefore must be earlier than NotOnOrAfter
|
||||
if conditions.not_before and conditions.not_on_or_after:
|
||||
if not later_than(conditions.not_on_or_after, conditions.not_before):
|
||||
if not later_than(conditions.not_on_or_after,
|
||||
conditions.not_before):
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -562,10 +588,11 @@ class AuthnResponse(StatusResponse):
|
||||
if not lax:
|
||||
raise Exception("Not for me!!!")
|
||||
|
||||
if conditions.condition: # extra conditions
|
||||
if conditions.condition: # extra conditions
|
||||
for cond in conditions.condition:
|
||||
try:
|
||||
if cond.extension_attributes[XSI_TYPE] in self.extension_schema:
|
||||
if cond.extension_attributes[
|
||||
XSI_TYPE] in self.extension_schema:
|
||||
pass
|
||||
else:
|
||||
raise Exception("Unknown condition")
|
||||
@@ -582,9 +609,9 @@ class AuthnResponse(StatusResponse):
|
||||
:param attribute_statement: A SAML.AttributeStatement which might
|
||||
contain both encrypted attributes and attributes.
|
||||
"""
|
||||
# _node_name = [
|
||||
# "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedData",
|
||||
# "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedAttribute"]
|
||||
# _node_name = [
|
||||
# "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedData",
|
||||
# "urn:oasis:names:tc:SAML:2.0:assertion:EncryptedAttribute"]
|
||||
|
||||
for encattr in attribute_statement.encrypted_attribute:
|
||||
if not encattr.encrypted_key:
|
||||
@@ -624,7 +651,7 @@ class AuthnResponse(StatusResponse):
|
||||
if data.address:
|
||||
if not valid_address(data.address):
|
||||
return False
|
||||
# verify that I got it from the correct sender
|
||||
# verify that I got it from the correct sender
|
||||
|
||||
# These two will raise exception if untrue
|
||||
validate_on_or_after(data.not_on_or_after, self.timeslack)
|
||||
@@ -650,7 +677,8 @@ class AuthnResponse(StatusResponse):
|
||||
logger.info("outstanding queries: %s" % (
|
||||
self.outstanding_queries.keys(),))
|
||||
raise Exception(
|
||||
"Combination of session id and requestURI I don't recall")
|
||||
"Combination of session id and requestURI I don't "
|
||||
"recall")
|
||||
return True
|
||||
|
||||
def _holder_of_key_confirmed(self, data):
|
||||
@@ -720,8 +748,8 @@ class AuthnResponse(StatusResponse):
|
||||
#if self.context == "AuthnReq" or self.context == "AttrQuery":
|
||||
if self.context == "AuthnReq":
|
||||
self.authn_statement_ok()
|
||||
# elif self.context == "AttrQuery":
|
||||
# self.authn_statement_ok(True)
|
||||
# elif self.context == "AttrQuery":
|
||||
# self.authn_statement_ok(True)
|
||||
|
||||
if not self.condition_ok():
|
||||
raise VerificationError("Condition not OK")
|
||||
@@ -773,7 +801,7 @@ class AuthnResponse(StatusResponse):
|
||||
else: # This is a saml2int limitation
|
||||
try:
|
||||
assert len(self.response.assertion) == 1 or \
|
||||
len(self.response.encrypted_assertion) == 1
|
||||
len(self.response.encrypted_assertion) == 1
|
||||
except AssertionError:
|
||||
raise Exception("No assertion part")
|
||||
|
||||
@@ -873,7 +901,9 @@ class AuthnResponse(StatusResponse):
|
||||
|
||||
correct = 0
|
||||
for subject_conf in self.assertion.subject.subject_confirmation:
|
||||
if subject_conf.subject_confirmation_data.address:
|
||||
if subject_conf.subject_confirmation_data is None:
|
||||
correct += 1 # In reality undefined
|
||||
elif subject_conf.subject_confirmation_data.address:
|
||||
if subject_conf.subject_confirmation_data.address == address:
|
||||
correct += 1
|
||||
else:
|
||||
@@ -890,7 +920,6 @@ class AuthnQueryResponse(AuthnResponse):
|
||||
|
||||
def __init__(self, sec_context, attribute_converters, entity_id,
|
||||
return_addrs=None, timeslack=0, asynchop=False, test=False):
|
||||
|
||||
AuthnResponse.__init__(self, sec_context, attribute_converters,
|
||||
entity_id, return_addrs, timeslack=timeslack,
|
||||
asynchop=asynchop, test=test)
|
||||
@@ -908,7 +937,6 @@ class AttributeResponse(AuthnResponse):
|
||||
|
||||
def __init__(self, sec_context, attribute_converters, entity_id,
|
||||
return_addrs=None, timeslack=0, asynchop=False, test=False):
|
||||
|
||||
AuthnResponse.__init__(self, sec_context, attribute_converters,
|
||||
entity_id, return_addrs, timeslack=timeslack,
|
||||
asynchop=asynchop, test=test)
|
||||
@@ -939,7 +967,6 @@ class ArtifactResponse(AuthnResponse):
|
||||
|
||||
def __init__(self, sec_context, attribute_converters, entity_id,
|
||||
return_addrs=None, timeslack=0, asynchop=False, test=False):
|
||||
|
||||
AuthnResponse.__init__(self, sec_context, attribute_converters,
|
||||
entity_id, return_addrs, timeslack=timeslack,
|
||||
asynchop=asynchop, test=test)
|
||||
@@ -951,7 +978,8 @@ class ArtifactResponse(AuthnResponse):
|
||||
|
||||
def response_factory(xmlstr, conf, return_addrs=None, outstanding_queries=None,
|
||||
timeslack=0, decode=True, request_id=0, origxml=None,
|
||||
asynchop=True, allow_unsolicited=False, want_assertions_signed=False):
|
||||
asynchop=True, allow_unsolicited=False,
|
||||
want_assertions_signed=False):
|
||||
sec_context = security_context(conf)
|
||||
if not timeslack:
|
||||
try:
|
||||
@@ -986,6 +1014,7 @@ def response_factory(xmlstr, conf, return_addrs=None, outstanding_queries=None,
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# A class of it's own
|
||||
|
||||
|
@@ -308,6 +308,7 @@ class Server(Entity):
|
||||
#if identity:
|
||||
_issuer = self._issuer(issuer)
|
||||
ast = Assertion(identity)
|
||||
ast.acs = self.config.getattr("attribute_converters", "idp")
|
||||
if policy is None:
|
||||
policy = Policy()
|
||||
try:
|
||||
@@ -521,7 +522,6 @@ class Server(Entity):
|
||||
|
||||
try:
|
||||
_authn = authn
|
||||
response = None
|
||||
if (sign_assertion or sign_response) and self.sec.cert_handler.generate_cert():
|
||||
with self.lock:
|
||||
self.sec.cert_handler.update_cert(True)
|
||||
@@ -536,7 +536,8 @@ class Server(Entity):
|
||||
sign_assertion=sign_assertion,
|
||||
sign_response=sign_response,
|
||||
best_effort=best_effort,
|
||||
encrypt_assertion=encrypt_assertion, encrypt_cert=encrypt_cert)
|
||||
encrypt_assertion=encrypt_assertion,
|
||||
encrypt_cert=encrypt_cert)
|
||||
return self._authn_response(in_response_to, # in_response_to
|
||||
destination, # consumer_url
|
||||
sp_entity_id, # sp_entity_id
|
||||
@@ -548,7 +549,8 @@ class Server(Entity):
|
||||
sign_assertion=sign_assertion,
|
||||
sign_response=sign_response,
|
||||
best_effort=best_effort,
|
||||
encrypt_assertion=encrypt_assertion, encrypt_cert=encrypt_cert)
|
||||
encrypt_assertion=encrypt_assertion,
|
||||
encrypt_cert=encrypt_cert)
|
||||
|
||||
except MissingValue, exc:
|
||||
return self.create_error_response(in_response_to, destination,
|
||||
|
@@ -1011,6 +1011,7 @@ def security_context(conf, debug=None):
|
||||
tmp_key_file=conf.tmp_key_file,
|
||||
validate_certificate=conf.validate_certificate)
|
||||
|
||||
|
||||
def encrypt_cert_from_item(item):
|
||||
_encrypt_cert = None
|
||||
try:
|
||||
@@ -1031,6 +1032,7 @@ def encrypt_cert_from_item(item):
|
||||
return None
|
||||
return _encrypt_cert
|
||||
|
||||
|
||||
class CertHandlerExtra(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -1488,7 +1490,8 @@ class SecurityContext(object):
|
||||
return self.correctly_signed_message(decoded_xml, "assertion", must,
|
||||
origdoc, only_valid_cert)
|
||||
|
||||
def correctly_signed_response(self, decoded_xml, must=False, origdoc=None,only_valid_cert=False,
|
||||
def correctly_signed_response(self, decoded_xml, must=False, origdoc=None,
|
||||
only_valid_cert=False,
|
||||
require_response_signature=False, **kwargs):
|
||||
""" Check if a instance is correctly signed, if we have metadata for
|
||||
the IdP that sent the info use that, if not use the key that are in
|
||||
|
@@ -1,82 +1,32 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
xmlns:ns1="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
|
||||
xmlns:ns2="http://www.w3.org/2000/09/xmldsig#"
|
||||
entityID="urn:mace:example.com:saml:roland:sp">
|
||||
<ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true"
|
||||
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<ns0:Extensions>
|
||||
<ns1:DiscoveryResponse
|
||||
Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol"
|
||||
Location="http://lingon.catalogix.se:8087/disco" index="1"/>
|
||||
</ns0:Extensions>
|
||||
<ns0:KeyDescriptor use="signing">
|
||||
<ns2:KeyInfo>
|
||||
<ns2:X509Data>
|
||||
<ns2:X509Certificate>
|
||||
MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy
|
||||
3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN
|
||||
efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G
|
||||
A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs
|
||||
iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
|
||||
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw
|
||||
mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6
|
||||
h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5
|
||||
U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6
|
||||
mrPzGzk3ECbupFnqyREH3+ZPSdk=
|
||||
</ns2:X509Certificate>
|
||||
</ns2:X509Data>
|
||||
</ns2:KeyInfo>
|
||||
</ns0:KeyDescriptor>
|
||||
<ns0:ArtifactResolutionService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
Location="http://lingon.catalogix.se:8087/ars" index="1"/>
|
||||
<ns0:SingleLogoutService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
Location="http://lingon.catalogix.se:8087/sls"/>
|
||||
<ns0:ManageNameIDService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
|
||||
Location="http://lingon.catalogix.se:8087/mni/soap"/>
|
||||
<ns0:ManageNameIDService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="http://lingon.catalogix.se:8087/mni/post"/>
|
||||
<ns0:ManageNameIDService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="http://lingon.catalogix.se:8087/mni/redirect"/>
|
||||
<ns0:ManageNameIDService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
|
||||
Location="http://lingon.catalogix.se:8087/mni/art"/>
|
||||
<ns0:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
</ns0:NameIDFormat>
|
||||
<ns0:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||
</ns0:NameIDFormat>
|
||||
<ns0:AssertionConsumerService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="http://lingon.catalogix.se:8087/" index="1"/>
|
||||
<ns0:AssertionConsumerService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
|
||||
Location="http://lingon.catalogix.se:8087/paos" index="2"/>
|
||||
<ns0:AssertionConsumerService
|
||||
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
Location="http://lingon.catalogix.se:8087/redirect" index="3"/>
|
||||
</ns0:SPSSODescriptor>
|
||||
<ns0:Organization>
|
||||
<ns0:OrganizationName xml:lang="se">AB Exempel</ns0:OrganizationName>
|
||||
<ns0:OrganizationDisplayName xml:lang="se">AB Exempel
|
||||
</ns0:OrganizationDisplayName>
|
||||
<ns0:OrganizationURL xml:lang="en">http://www.example.org
|
||||
</ns0:OrganizationURL>
|
||||
</ns0:Organization>
|
||||
<ns0:ContactPerson contactType="technical">
|
||||
<ns0:GivenName>Roland</ns0:GivenName>
|
||||
<ns0:SurName>Hedberg</ns0:SurName>
|
||||
<ns0:EmailAddress>tech@eample.com</ns0:EmailAddress>
|
||||
<ns0:EmailAddress>tech@example.org</ns0:EmailAddress>
|
||||
<ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber>
|
||||
</ns0:ContactPerson>
|
||||
</ns0:EntityDescriptor>
|
||||
<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns1="urn:oasis:names:tc:SAML:metadata:attribute" xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ns4="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" xmlns:ns5="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" entityID="urn:mace:example.com:saml:roland:sp"><ns0:Extensions><ns1:EntityAttributes><ns2:Attribute Name="http://macedir.org/entity-category"><ns2:AttributeValue xsi:type="xs:string">http://www.swamid.se/category/sfs-1993-1153</ns2:AttributeValue><ns2:AttributeValue xsi:type="xs:string">http://www.swamid.se/category/hei-service</ns2:AttributeValue></ns2:Attribute></ns1:EntityAttributes></ns0:Extensions><ns0:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><ns0:Extensions><ns4:DiscoveryResponse Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="http://lingon.catalogix.se:8087/disco" index="1" /></ns0:Extensions><ns0:KeyDescriptor use="encryption"><ns5:KeyInfo><ns5:X509Data><ns5:X509Certificate>MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy
|
||||
3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN
|
||||
efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G
|
||||
A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs
|
||||
iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
|
||||
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw
|
||||
mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6
|
||||
h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5
|
||||
U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6
|
||||
mrPzGzk3ECbupFnqyREH3+ZPSdk=
|
||||
</ns5:X509Certificate></ns5:X509Data></ns5:KeyInfo></ns0:KeyDescriptor><ns0:KeyDescriptor use="signing"><ns5:KeyInfo><ns5:X509Data><ns5:X509Certificate>MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
|
||||
gQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy
|
||||
3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaN
|
||||
efiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0G
|
||||
A1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJs
|
||||
iojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
|
||||
U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSw
|
||||
mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6
|
||||
h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5
|
||||
U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6
|
||||
mrPzGzk3ECbupFnqyREH3+ZPSdk=
|
||||
</ns5:X509Certificate></ns5:X509Data></ns5:KeyInfo></ns0:KeyDescriptor><ns0:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://lingon.catalogix.se:8087/ars" index="1" /><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://lingon.catalogix.se:8087/sls" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://lingon.catalogix.se:8087/mni/soap" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://lingon.catalogix.se:8087/mni/post" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://lingon.catalogix.se:8087/mni/redirect" /><ns0:ManageNameIDService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="http://lingon.catalogix.se:8087/mni/art" /><ns0:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</ns0:NameIDFormat><ns0:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</ns0:NameIDFormat><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://lingon.catalogix.se:8087/" index="1" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="http://lingon.catalogix.se:8087/paos" index="2" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://lingon.catalogix.se:8087/redirect" index="3" /></ns0:SPSSODescriptor><ns0:Organization><ns0:OrganizationName xml:lang="se">AB Exempel</ns0:OrganizationName><ns0:OrganizationDisplayName xml:lang="se">AB Exempel</ns0:OrganizationDisplayName><ns0:OrganizationURL xml:lang="en">http://www.example.org</ns0:OrganizationURL></ns0:Organization><ns0:ContactPerson contactType="technical"><ns0:GivenName>Roland</ns0:GivenName><ns0:SurName>Hedberg</ns0:SurName><ns0:EmailAddress>tech@eample.com</ns0:EmailAddress><ns0:EmailAddress>tech@example.org</ns0:EmailAddress><ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber></ns0:ContactPerson></ns0:EntityDescriptor>
|
||||
|
@@ -1,2 +1,3 @@
|
||||
#!/bin/sh
|
||||
curl -O -G http://md.swamid.se/md/swamid-2.0.xml
|
||||
mdexport.py -t local -o swamid2.md swamid-2.0.xml
|
||||
|
Reference in New Issue
Block a user