Update of pysaml2 and fix for very signature assertion.

This commit is contained in:
Hans Hörberg
2014-06-05 08:40:52 +02:00
11 changed files with 108 additions and 83 deletions

View File

@@ -497,20 +497,33 @@ class Policy(object):
:return: A possibly modified AVA :return: A possibly modified AVA
""" """
_rest = self.get_attribute_restrictions(sp_entity_id) _ava = None
if _rest is None:
_rest = self.get_entity_categories(sp_entity_id, mdstore)
logger.debug("filter based on: %s" % _rest)
_ava = filter_attribute_value_assertions(ava.copy(), _rest)
if required or optional: if required or optional:
logger.debug("required: %s, optional: %s" % (required, optional)) logger.debug("required: %s, optional: %s" % (required, optional))
ava1 = filter_on_attributes( _ava = filter_on_attributes(
ava.copy(), required, optional, self.acs, ava.copy(), required, optional, self.acs,
self.get_fail_on_missing_requested(sp_entity_id)) self.get_fail_on_missing_requested(sp_entity_id))
_ava.update(ava1)
return _ava _rest = self.get_entity_categories(sp_entity_id, mdstore)
if _rest:
ava_ec = filter_attribute_value_assertions(ava.copy(), _rest)
if _ava is None:
_ava = ava_ec
else:
_ava.update(ava_ec)
_rest = self.get_attribute_restrictions(sp_entity_id)
if _rest:
if _ava is None:
_ava = ava.copy()
_ava = filter_attribute_value_assertions(_ava, _rest)
elif _ava is None:
_ava = ava.copy()
if _ava is None:
return {}
else:
return _ava
def restrict(self, ava, sp_entity_id, metadata=None): def restrict(self, ava, sp_entity_id, metadata=None):
""" Identity attribute names are expected to be expressed in """ Identity attribute names are expected to be expressed in
@@ -523,8 +536,8 @@ class Policy(object):
if metadata: if metadata:
spec = metadata.attribute_requirement(sp_entity_id) spec = metadata.attribute_requirement(sp_entity_id)
if spec: if spec:
ava = self.filter(ava, sp_entity_id, metadata, return self.filter(ava, sp_entity_id, metadata,
spec["required"], spec["optional"]) spec["required"], spec["optional"])
return self.filter(ava, sp_entity_id, metadata, [], []) return self.filter(ava, sp_entity_id, metadata, [], [])
@@ -757,5 +770,11 @@ class Assertion(dict):
policy.acs = self.acs policy.acs = self.acs
ava = policy.restrict(self, sp_entity_id, metadata) ava = policy.restrict(self, sp_entity_id, metadata)
self.update(ava)
for key, val in self.items():
if key in ava:
self[key] = ava[key]
else:
del self[key]
return ava return ava

View File

@@ -545,8 +545,8 @@ class Base(Entity):
raise raise
except UnravelError: except UnravelError:
return None return None
except Exception: except Exception as err:
logger.error("XML parse error") logger.error("XML parse error: %s" % err)
raise raise
#logger.debug(">> %s", resp) #logger.debug(">> %s", resp)

View File

@@ -127,7 +127,8 @@ class Entity(HTTPBase):
if _val.startswith("http"): if _val.startswith("http"):
r = requests.request("GET", _val) r = requests.request("GET", _val)
if r.status_code == 200: if r.status_code == 200:
setattr(self.config, item, r.text) _, filename = make_temp(r.text, ".pem", False)
setattr(self.config, item, filename)
else: else:
raise Exception( raise Exception(
"Could not fetch certificate from %s" % _val) "Could not fetch certificate from %s" % _val)
@@ -840,7 +841,7 @@ class Entity(HTTPBase):
response = None response = None
if self.config.accepted_time_diff: if self.config.accepted_time_diff:
timeslack = self.config.accepted_time_diff kwargs["timeslack"] = self.config.accepted_time_diff
if "asynchop" not in kwargs: if "asynchop" not in kwargs:
if binding in [BINDING_SOAP, BINDING_PAOS]: if binding in [BINDING_SOAP, BINDING_PAOS]:

View File

@@ -475,7 +475,7 @@ class AuthnResponse(StatusResponse):
else: else:
self.outstanding_queries = {} self.outstanding_queries = {}
self.context = "AuthnReq" self.context = "AuthnReq"
self.came_from = "" self.came_from = None
self.ava = None self.ava = None
self.assertion = None self.assertion = None
self.assertions = [] self.assertions = []
@@ -507,7 +507,7 @@ class AuthnResponse(StatusResponse):
if self.asynchop: if self.asynchop:
if self.in_response_to in self.outstanding_queries: if self.in_response_to in self.outstanding_queries:
self.came_from = self.outstanding_queries[self.in_response_to] self.came_from = self.outstanding_queries[self.in_response_to]
del self.outstanding_queries[self.in_response_to] #del self.outstanding_queries[self.in_response_to]
try: try:
if not self.check_subject_confirmation_in_response_to( if not self.check_subject_confirmation_in_response_to(
self.in_response_to): self.in_response_to):
@@ -529,7 +529,7 @@ class AuthnResponse(StatusResponse):
def clear(self): def clear(self):
self._clear() self._clear()
self.came_from = "" self.came_from = None
self.ava = None self.ava = None
self.assertion = None self.assertion = None
@@ -667,12 +667,12 @@ class AuthnResponse(StatusResponse):
if not later_than(data.not_on_or_after, data.not_before): if not later_than(data.not_on_or_after, data.not_before):
return False return False
if self.asynchop and not self.came_from: if self.asynchop and self.came_from is None:
if data.in_response_to: if data.in_response_to:
if data.in_response_to in self.outstanding_queries: if data.in_response_to in self.outstanding_queries:
self.came_from = self.outstanding_queries[ self.came_from = self.outstanding_queries[
data.in_response_to] data.in_response_to]
del self.outstanding_queries[data.in_response_to] #del self.outstanding_queries[data.in_response_to]
elif self.allow_unsolicited: elif self.allow_unsolicited:
pass pass
else: else:
@@ -744,7 +744,7 @@ class AuthnResponse(StatusResponse):
logger.info("Subject NameID: %s" % self.name_id) logger.info("Subject NameID: %s" % self.name_id)
return self.name_id return self.name_id
def _assertion(self, assertion): def _assertion(self, assertion, verified=False):
""" """
Check the assertion Check the assertion
:param assertion: :param assertion:
@@ -757,13 +757,14 @@ class AuthnResponse(StatusResponse):
raise SignatureError("Signature missing for assertion") raise SignatureError("Signature missing for assertion")
else: else:
logger.debug("signed") logger.debug("signed")
try: if not verified:
if self.require_signature: try:
self.sec.check_signature(assertion, class_name(assertion), if self.require_signature:
self.xmlstr) self.sec.check_signature(assertion, class_name(assertion),
except Exception as exc: self.xmlstr)
logger.error("correctly_signed_response: %s" % exc) except Exception as exc:
raise logger.error("correctly_signed_response: %s" % exc)
raise
self.assertion = assertion self.assertion = assertion
logger.debug("assertion context: %s" % (self.context,)) logger.debug("assertion context: %s" % (self.context,))
@@ -791,14 +792,14 @@ class AuthnResponse(StatusResponse):
if self.asynchop: if self.asynchop:
if self.allow_unsolicited: if self.allow_unsolicited:
pass pass
elif not self.came_from: elif self.came_from is None:
raise VerificationError("Came from") raise VerificationError("Came from")
return True return True
except Exception: except Exception:
logger.exception("get subject") logger.exception("get subject")
raise raise
def decrypt_assertions(self, encrypted_assertions, key_file=""): def decrypt_assertions(self, encrypted_assertions, decr_txt):
res = [] res = []
for encrypted_assertion in encrypted_assertions: for encrypted_assertion in encrypted_assertions:
if encrypted_assertion.extension_elements: if encrypted_assertion.extension_elements:
@@ -806,8 +807,8 @@ class AuthnResponse(StatusResponse):
encrypted_assertion.extension_elements, [saml, samlp]) encrypted_assertion.extension_elements, [saml, samlp])
for assertion in assertions: for assertion in assertions:
if assertion.signature: if assertion.signature:
if not self.sec.verify_signature( if not self.sec.check_signature(
"%s" % assertion, key_file, assertion, origdoc=decr_txt,
node_name=class_name(assertion)): node_name=class_name(assertion)):
logger.error( logger.error(
"Failed to verify signature on '%s'" % assertion) "Failed to verify signature on '%s'" % assertion)
@@ -826,21 +827,23 @@ class AuthnResponse(StatusResponse):
except AssertionError: except AssertionError:
raise Exception("No assertion part") raise Exception("No assertion part")
res = []
if self.response.encrypted_assertion: if self.response.encrypted_assertion:
logger.debug("***Encrypted assertion/-s***") logger.debug("***Encrypted assertion/-s***")
decr_text = self.sec.decrypt(self.xmlstr, key_file) decr_text = self.sec.decrypt(self.xmlstr, key_file)
resp = samlp.response_from_string(decr_text) resp = samlp.response_from_string(decr_text)
res = self.decrypt_assertions(resp.encrypted_assertion, key_file) res = self.decrypt_assertions(resp.encrypted_assertion, decr_text)
if self.response.assertion: if self.response.assertion:
self.response.assertion.extend(res) self.response.assertion.extend(res)
else: else:
self.response.assertion = res self.response.assertion = res
self.response.encrypted_assertion = [] self.response.encrypted_assertion = []
self.xmlstr = decr_text
if self.response.assertion: if self.response.assertion:
logger.debug("***Unencrypted assertion***") logger.debug("***Unencrypted assertion***")
for assertion in self.response.assertion: for assertion in self.response.assertion:
if not self._assertion(assertion): if not self._assertion(assertion, assertion in res):
return False return False
else: else:
self.assertions.append(assertion) self.assertions.append(assertion)

View File

@@ -35,7 +35,6 @@ from Crypto.PublicKey import RSA
from saml2.cert import OpenSSLWrapper from saml2.cert import OpenSSLWrapper
from saml2.extension import pefim from saml2.extension import pefim
from saml2.saml import EncryptedAssertion from saml2.saml import EncryptedAssertion
from saml2.samlp import Response
import xmldsig as ds import xmldsig as ds
@@ -1378,7 +1377,7 @@ class SecurityContext(object):
""" """
:param item: Parsed entity :param item: Parsed entity
:param node_name: :param node_name: The name of the class that is signed
:param origdoc: The original XML string :param origdoc: The original XML string
:param id_attr: :param id_attr:
:param must: :param must:

View File

@@ -1,48 +1,50 @@
from pathutils import full_path from pathutils import full_path
CONFIG = { CONFIG = {
"entityid" : "urn:mace:example.com:saml:roland:sp", "entityid": "urn:mace:example.com:saml:roland:sp",
"name" : "urn:mace:example.com:saml:roland:sp", "name": "urn:mace:example.com:saml:roland:sp",
"description": "My own SP", "description": "My own SP",
"service": { "service": {
"sp": { "sp": {
"endpoints":{ "endpoints": {
"assertion_consumer_service": ["http://lingon.catalogix.se:8087/"], "assertion_consumer_service": [
}, "http://lingon.catalogix.se:8087/"],
},
"required_attributes": ["surName", "givenName", "mail"], "required_attributes": ["surName", "givenName", "mail"],
"optional_attributes": ["title"], "optional_attributes": ["title"],
"idp": ["urn:mace:example.com:saml:roland:idp"], "idp": ["urn:mace:example.com:saml:roland:idp"],
} }
}, },
"debug" : 1, "debug": 1,
"key_file" : full_path("test.key"), "key_file": full_path("test.key"),
"cert_file" : full_path("test.pem"), "cert_file": full_path("test.pem"),
"xmlsec_binary" : None, "xmlsec_binary": None,
"metadata": { "metadata": {
"local": [full_path("idp_2.xml")], "local": [full_path("idp_2.xml")],
}, },
"virtual_organization" : { "virtual_organization": {
"urn:mace:example.com:it:tek":{ "urn:mace:example.com:it:tek": {
"nameid_format" : "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID", "nameid_format": "urn:oid:1.3.6.1.4.1.1466.115.121.1.15-NameID",
"common_identifier": "umuselin", "common_identifier": "umuselin",
} }
}, },
"subject_data": full_path("subject_data.db"), "subject_data": full_path("subject_data.db"),
"accepted_time_diff": 60, "accepted_time_diff": 60,
"attribute_map_dir" : full_path("attributemaps"), "attribute_map_dir": full_path("attributemaps"),
"organization": { "organization": {
"name": ("AB Exempel", "se"), "name": ("AB Exempel", "se"),
"display_name": ("AB Exempel", "se"), "display_name": ("AB Exempel", "se"),
"url": "http://www.example.org", "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"
}, },
"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"
},
], ],
"secret": "0123456789", "secret": "0123456789",
"only_use_keys_in_metadata": True "only_use_keys_in_metadata": True
} }

View File

@@ -173,16 +173,20 @@ def test_ava_filter_2():
"mail": "derek@example.com"} "mail": "derek@example.com"}
# mail removed because it doesn't match the regular expression # mail removed because it doesn't match the regular expression
# So this should fail. _ava =policy.filter(ava, 'urn:mace:umu.se:saml:roland:sp', None, [mail],
raises(MissingValue, policy.filter, ava, 'urn:mace:umu.se:saml:roland:sp', [gn, sn])
None, [mail], [gn, sn])
assert _eq(_ava.keys(), ["givenName", "surName"])
ava = {"givenName": "Derek", ava = {"givenName": "Derek",
"surName": "Jeter"} "surName": "Jeter"}
# it wasn't there to begin with # it wasn't there to begin with
raises(Exception, policy.filter, ava, 'urn:mace:umu.se:saml:roland:sp', try:
None, [gn, sn, mail]) policy.filter(ava, 'urn:mace:umu.se:saml:roland:sp', None,
[gn, sn, mail])
except MissingValue:
pass
def test_ava_filter_dont_fail(): def test_ava_filter_dont_fail():
@@ -843,4 +847,4 @@ def test_assertion_with_authn_instant():
if __name__ == "__main__": if __name__ == "__main__":
test_ava_filter_dont_fail() test_assertion_2()

View File

@@ -64,7 +64,7 @@ def test_filter_ava2():
"default": { "default": {
"lifetime": {"minutes": 15}, "lifetime": {"minutes": 15},
#"attribute_restrictions": None # means all I have #"attribute_restrictions": None # means all I have
"entity_categories": ["edugain"] "entity_categories": ["refeds", "edugain"]
} }
}) })
@@ -158,7 +158,7 @@ def test_idp_policy_filter():
"norEduPersonNIN": "19800101134"} "norEduPersonNIN": "19800101134"}
policy = idp.config.getattr("policy", "idp") policy = idp.config.getattr("policy", "idp")
policy.filter(ava, "urn:mace:example.com:saml:roland:sp", idp.metadata) ava = policy.filter(ava, "urn:mace:example.com:saml:roland:sp", idp.metadata)
print ava print ava
assert ava.keys() == ["eduPersonTargetedID"] # because no entity category assert ava.keys() == ["eduPersonTargetedID"] # because no entity category

View File

@@ -8,13 +8,11 @@ from saml2.server import Server
from saml2.response import response_factory from saml2.response import response_factory
from saml2.response import StatusResponse from saml2.response import StatusResponse
from saml2.response import AuthnResponse from saml2.response import AuthnResponse
from saml2.sigver import security_context, SignatureError from saml2.sigver import SignatureError
from saml2.sigver import MissingKey
from pytest import raises
FALSE_ASSERT_SIGNED = "saml_false_signed.xml" FALSE_ASSERT_SIGNED = "saml_false_signed.xml"
TIMESLACK = 2592000 # Roughly 3 month
def _eq(l1, l2): def _eq(l1, l2):
return set(l1) == set(l2) return set(l1) == set(l2)
@@ -73,7 +71,7 @@ class TestResponse:
"http://lingon.catalogix.se:8087/"], "http://lingon.catalogix.se:8087/"],
outstanding_queries={ outstanding_queries={
"id12": "http://localhost:8088/sso"}, "id12": "http://localhost:8088/sso"},
timeslack=10000, decode=False) timeslack=TIMESLACK, decode=False)
assert isinstance(resp, StatusResponse) assert isinstance(resp, StatusResponse)
assert isinstance(resp, AuthnResponse) assert isinstance(resp, AuthnResponse)
@@ -85,7 +83,7 @@ class TestResponse:
"http://lingon.catalogix.se:8087/"], "http://lingon.catalogix.se:8087/"],
outstanding_queries={ outstanding_queries={
"id12": "http://localhost:8088/sso"}, "id12": "http://localhost:8088/sso"},
timeslack=10000, decode=False) timeslack=TIMESLACK, decode=False)
assert isinstance(resp, StatusResponse) assert isinstance(resp, StatusResponse)
assert isinstance(resp, AuthnResponse) assert isinstance(resp, AuthnResponse)
@@ -98,7 +96,7 @@ class TestResponse:
outstanding_queries={ outstanding_queries={
"bahigehogffohiphlfmplepdpcohkhhmheppcdie": "bahigehogffohiphlfmplepdpcohkhhmheppcdie":
"http://localhost:8088/sso"}, "http://localhost:8088/sso"},
timeslack=10000, decode=False) timeslack=TIMESLACK, decode=False)
assert isinstance(resp, StatusResponse) assert isinstance(resp, StatusResponse)
assert isinstance(resp, AuthnResponse) assert isinstance(resp, AuthnResponse)

View File

@@ -71,7 +71,7 @@ class TestAuthnResponse:
print self.ar.__dict__ print self.ar.__dict__
assert self.ar.came_from == 'http://localhost:8088/sso' assert self.ar.came_from == 'http://localhost:8088/sso'
assert self.ar.session_id() == "id12" assert self.ar.session_id() == "id12"
assert self.ar.ava["eduPersonAffiliation"] == IDENTITY["eduPersonAffiliation"] assert self.ar.ava["givenName"] == IDENTITY["givenName"]
assert self.ar.name_id assert self.ar.name_id
assert self.ar.issuer() == 'urn:mace:example.com:saml:roland:idp' assert self.ar.issuer() == 'urn:mace:example.com:saml:roland:idp'

View File

@@ -227,18 +227,17 @@ class TestServer1():
assert assertion.attribute_statement assert assertion.attribute_statement
attribute_statement = assertion.attribute_statement attribute_statement = assertion.attribute_statement
print attribute_statement print attribute_statement
assert len(attribute_statement[0].attribute) == 5 assert len(attribute_statement[0].attribute) == 4
# Pick out one attribute # Pick out one attribute
attr = None attr = None
for attr in attribute_statement[0].attribute: for attr in attribute_statement[0].attribute:
if attr.friendly_name == "edupersonentitlement": if attr.friendly_name == "givenname":
break break
assert len(attr.attribute_value) == 1 assert len(attr.attribute_value) == 1
assert attr.name == "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" assert attr.name == "urn:oid:2.5.4.42"
assert attr.name_format == "urn:oasis:names:tc:SAML:2" \ assert attr.name_format == "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
".0:attrname-format:uri"
value = attr.attribute_value[0] value = attr.attribute_value[0]
assert value.text.strip() == "Short stop" assert value.text.strip() == "Derek"
assert value.get_type() == "xs:string" assert value.get_type() == "xs:string"
assert assertion.subject assert assertion.subject
assert assertion.subject.name_id assert assertion.subject.name_id