Skip to content

Commit e70835b

Browse files
author
Hans Hörberg
committed
Partial commit for decrpyting and verifying signatures at the client. All tests works.
1 parent eb2d968 commit e70835b

File tree

5 files changed

+187
-33
lines changed

5 files changed

+187
-33
lines changed

src/saml2/client_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ def create_name_id_mapping_request(self, name_id_policy,
542542
# ======== response handling ===========
543543

544544
def parse_authn_request_response(self, xmlstr, binding, outstanding=None,
545-
outstanding_certs=None, decrypt=True):
545+
outstanding_certs=None, decrypt=True, pefim=False):
546546
""" Deal with an AuthnResponse
547547
548548
:param xmlstr: The reply as a xml string
@@ -578,7 +578,7 @@ def parse_authn_request_response(self, xmlstr, binding, outstanding=None,
578578
try:
579579
resp = self._parse_response(xmlstr, AuthnResponse,
580580
"assertion_consumer_service",
581-
binding, **kwargs)
581+
binding, pefim=pefim, **kwargs)
582582
except StatusError as err:
583583
logger.error("SAML status error: %s" % err)
584584
raise

src/saml2/entity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ def parse_manage_name_id_request_response(self, string,
978978
# ------------------------------------------------------------------------
979979

980980
def _parse_response(self, xmlstr, response_cls, service, binding,
981-
outstanding_certs=None, **kwargs):
981+
outstanding_certs=None, pefim=False, **kwargs):
982982
""" Deal with a Response
983983
984984
:param xmlstr: The response as a xml string
@@ -1056,7 +1056,7 @@ def _parse_response(self, xmlstr, response_cls, service, binding,
10561056
decrypt = True
10571057
if "decrypt" in kwargs:
10581058
decrypt = kwargs["decrypt"]
1059-
response = response.verify(key_file, decrypt=decrypt)
1059+
response = response.verify(key_file, decrypt=decrypt, pefim=pefim)
10601060

10611061
if not response:
10621062
return None

src/saml2/response.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def _verify(self):
395395
def loads(self, xmldata, decode=True, origxml=None):
396396
return self._loads(xmldata, decode, origxml)
397397

398-
def verify(self, key_file="", decrypt=True):
398+
def verify(self, key_file="", decrypt=True, pefim=False):
399399
try:
400400
return self._verify()
401401
except AssertionError:
@@ -780,10 +780,9 @@ def _assertion(self, assertion, verified=False):
780780

781781
logger.debug("--- Getting Identity ---")
782782

783-
if self.context == "AuthnReq" or self.context == "AttrQuery":
784-
self.ava = self.get_identity()
785-
786-
logger.debug("--- AVA: %s" % (self.ava,))
783+
#if self.context == "AuthnReq" or self.context == "AttrQuery":
784+
# self.ava = self.get_identity()
785+
# logger.debug("--- AVA: %s" % (self.ava,))
787786

788787
try:
789788
self.get_subject()
@@ -808,13 +807,12 @@ def decrypt_assertions(self, encrypted_assertions, decr_txt, issuer=None):
808807
if not self.sec.check_signature(
809808
assertion, origdoc=decr_txt,
810809
node_name=class_name(assertion), issuer=issuer):
811-
logger.error(
812-
"Failed to verify signature on '%s'" % assertion)
810+
logger.error("Failed to verify signature on '%s'" % assertion)
813811
raise SignatureError()
814812
res.append(assertion)
815813
return res
816814

817-
def parse_assertion(self, key_file="", decrypt=True):
815+
def parse_assertion(self, key_file="", decrypt=True, pefim=False):
818816
if self.context == "AuthnQuery":
819817
# can contain one or more assertions
820818
pass
@@ -825,20 +823,28 @@ def parse_assertion(self, key_file="", decrypt=True):
825823
except AssertionError:
826824
raise Exception("No assertion part")
827825

828-
res = []
829826
has_encrypted_assertions = self.response.encrypted_assertion
830827
if not has_encrypted_assertions and self.response.assertion:
831828
for tmp_assertion in self.response.assertion:
832829
if tmp_assertion.advice:
833-
if tmp_assertion.advice.encrypted_assertion:
830+
if tmp_assertion.advice.encrypted_assertion:
834831
has_encrypted_assertions = True
835832
break
836833

834+
if self.response.assertion:
835+
logger.debug("***Unencrypted assertion***")
836+
for assertion in self.response.assertion:
837+
if not self._assertion(assertion, False):
838+
return False
839+
837840
if has_encrypted_assertions and decrypt:
841+
_enc_assertions = []
838842
logger.debug("***Encrypted assertion/-s***")
839843
decr_text = self.sec.decrypt(self.xmlstr, key_file)
840844
resp = samlp.response_from_string(decr_text)
841-
res = self.decrypt_assertions(resp.encrypted_assertion, decr_text)
845+
_enc_assertions = self.decrypt_assertions(resp.encrypted_assertion, decr_text)
846+
decr_text = self.sec.decrypt(decr_text, key_file)
847+
resp = samlp.response_from_string(decr_text)
842848
if resp.assertion:
843849
for tmp_ass in resp.assertion:
844850
if tmp_ass.advice and tmp_ass.advice.encrypted_assertion:
@@ -849,26 +855,32 @@ def parse_assertion(self, key_file="", decrypt=True):
849855
tmp_ass.advice.assertion.extend(advice_res)
850856
else:
851857
tmp_ass.advice.assertion = advice_res
858+
if not pefim:
859+
_enc_assertions.extend(advice_res)
852860
tmp_ass.advice.encrypted_assertion = []
853-
self.response.assertion = resp.assertion
854-
if self.response.assertion:
855-
self.response.assertion.extend(res)
856-
else:
857-
self.response.assertion = res
861+
self.response.assertion = resp.assertion
862+
for assertion in _enc_assertions:
863+
if not self._assertion(assertion, True):
864+
return False
858865
self.xmlstr = decr_text
859866
self.response.encrypted_assertion = []
860867

861868
if self.response.assertion:
862-
logger.debug("***Unencrypted assertion***")
863869
for assertion in self.response.assertion:
864-
if not self._assertion(assertion, assertion in res):
865-
return False
866-
else:
867-
self.assertions.append(assertion)
870+
if assertion.advice and assertion.advice.assertion:
871+
for advice_assertion in assertion.advice.assertion:
872+
self.assertions.append(assertion)
873+
874+
if self.assertions and len(self.assertions) > 0:
868875
self.assertion = self.assertions[0]
876+
877+
if self.context == "AuthnReq" or self.context == "AttrQuery":
878+
self.ava = self.get_identity()
879+
logger.debug("--- AVA: %s" % (self.ava,))
880+
869881
return True
870882

871-
def verify(self, key_file="", decrypt=True):
883+
def verify(self, key_file="", decrypt=True, pefim=False):
872884
""" Verify that the assertion is syntactically correct and
873885
the signature is correct if present.
874886
:param key_file: If not the default key file should be used this is it.
@@ -886,7 +898,7 @@ def verify(self, key_file="", decrypt=True):
886898
if not isinstance(self.response, samlp.Response):
887899
return self
888900

889-
if self.parse_assertion(key_file, decrypt=decrypt):
901+
if self.parse_assertion(key_file, decrypt=decrypt, pefim=pefim):
890902
return self
891903
else:
892904
logger.error("Could not parse the assertion")
@@ -1114,7 +1126,7 @@ def loads(self, xmldata, decode=True, origxml=None):
11141126

11151127
return self._postamble()
11161128

1117-
def verify(self, key_file="", decrypt=True):
1129+
def verify(self, key_file="", decrypt=True, pefim=False):
11181130
try:
11191131
valid_instance(self.response)
11201132
except NotValid as exc:

src/saml2/sigver.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,9 +1306,17 @@ def decrypt(self, enctext, key_file=None):
13061306
:param enctext: The encrypted text as a string
13071307
:return: The decrypted text
13081308
"""
1309+
_enctext = self.crypto.decrypt(enctext, self.key_file)
1310+
if _enctext is not None and len(_enctext) > 0:
1311+
return _enctext
13091312
if key_file is not None and len(key_file.strip()) > 0:
1310-
return self.crypto.decrypt(enctext, key_file)
1311-
return self.crypto.decrypt(enctext, self.key_file)
1313+
_enctext = self.crypto.decrypt(enctext, key_file)
1314+
if _enctext is not None and len(_enctext) > 0:
1315+
return _enctext
1316+
_enctext = self.crypto.decrypt(enctext, self.key_file)
1317+
if _enctext is not None and len(_enctext) > 0:
1318+
return _enctext
1319+
return enctext
13121320

13131321
def verify_signature(self, signedtext, cert_file=None, cert_type="pem",
13141322
node_name=NODE_NAME, node_id=None, id_attr=""):

tests/test_51_client.py

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
# -*- coding: utf-8 -*-
33

44
import base64
5+
import uuid
56
import six
67
import urllib
78
import urlparse
9+
from saml2.cert import OpenSSLWrapper
810
from saml2.xmldsig import SIG_RSA_SHA256
911
from saml2 import BINDING_HTTP_POST
1012
from saml2 import BINDING_HTTP_REDIRECT
@@ -25,7 +27,7 @@
2527
from saml2.saml import NAMEID_FORMAT_TRANSIENT
2628
from saml2.saml import NameID
2729
from saml2.server import Server
28-
from saml2.sigver import pre_encryption_part
30+
from saml2.sigver import pre_encryption_part, make_temp
2931
from saml2.sigver import rm_xmltag
3032
from saml2.sigver import verify_redirect_signature
3133
from saml2.s_utils import do_attribute_statement
@@ -42,6 +44,28 @@
4244
}
4345

4446

47+
def generate_cert():
48+
sn = uuid.uuid4().urn
49+
cert_info = {
50+
"cn": "localhost",
51+
"country_code": "se",
52+
"state": "ac",
53+
"city": "Umea",
54+
"organization": "ITS",
55+
"organization_unit": "DIRG"
56+
}
57+
osw = OpenSSLWrapper()
58+
ca_cert_str = osw.read_str_from_file(
59+
full_path("root_cert/localhost.ca.crt"))
60+
ca_key_str = osw.read_str_from_file(
61+
full_path("root_cert/localhost.ca.key"))
62+
req_cert_str, req_key_str = osw.create_certificate(cert_info, request=True,
63+
sn=sn, key_length=2048)
64+
cert_str = osw.create_cert_signed_certificate(ca_cert_str, ca_key_str,
65+
req_cert_str)
66+
return cert_str, req_key_str
67+
68+
4569
def add_subelement(xmldoc, node_name, subelem):
4670
s = xmldoc.find(node_name)
4771
if s > 0:
@@ -292,7 +316,7 @@ def test_sign_auth_request_0(self):
292316
except Exception: # missing certificate
293317
self.client.sec.verify_signature(ar_str, node_name=class_name(ar))
294318

295-
def test_response(self):
319+
def test_response_1(self):
296320
IDP = "urn:mace:example.com:saml:roland:idp"
297321

298322
ava = {"givenName": ["Derek"], "surName": ["Jeter"],
@@ -368,6 +392,116 @@ def test_response(self):
368392
print(issuers)
369393
assert issuers == [[IDP], [IDP]]
370394

395+
def test_response_2(self):
396+
conf = config.SPConfig()
397+
conf.load_file("server_conf")
398+
_client = Saml2Client(conf)
399+
400+
idp, ava, ava_verify, nameid_policy = self.setup_verify_authn_response()
401+
402+
cert_str, cert_key_str = generate_cert()
403+
404+
cert =\
405+
{
406+
"cert": cert_str,
407+
"key": cert_key_str
408+
}
409+
410+
self.name_id = self.server.ident.transient_nameid(
411+
"urn:mace:example.com:saml:roland:sp", "id1")
412+
413+
resp = self.server.create_authn_response(
414+
identity=ava,
415+
in_response_to="id1",
416+
destination="http://lingon.catalogix.se:8087/",
417+
sp_entity_id="urn:mace:example.com:saml:roland:sp",
418+
#name_id_policy=nameid_policy,
419+
name_id=self.name_id,
420+
421+
authn=AUTHN,
422+
sign_response=True,
423+
sign_assertion=True,
424+
encrypt_assertion=False,
425+
encrypt_assertion_self_contained=True,
426+
#encrypted_advice_attributes=True,
427+
pefim=True,
428+
encrypt_cert_advice=cert_str
429+
)
430+
431+
resp_str = "%s" % resp
432+
433+
resp_str = base64.encodestring(resp_str)
434+
435+
authn_response = _client.parse_authn_request_response(
436+
resp_str, BINDING_HTTP_POST,
437+
{"id1": "http://foo.example.com/service"}, {"id1": cert}, pefim=True)
438+
439+
self.verify_authn_response(idp, authn_response, _client, ava_verify)
440+
441+
def test_response_3(self):
442+
conf = config.SPConfig()
443+
conf.load_file("server_conf")
444+
_client = Saml2Client(conf)
445+
446+
idp, ava, ava_verify, nameid_policy = self.setup_verify_authn_response()
447+
448+
self.name_id = self.server.ident.transient_nameid(
449+
"urn:mace:example.com:saml:roland:sp", "id1")
450+
451+
resp = self.server.create_authn_response(
452+
identity=ava,
453+
in_response_to="id1",
454+
destination="http://lingon.catalogix.se:8087/",
455+
sp_entity_id="urn:mace:example.com:saml:roland:sp",
456+
#name_id_policy=nameid_policy,
457+
name_id=self.name_id,
458+
459+
authn=AUTHN,
460+
sign_response=True,
461+
sign_assertion=True,
462+
encrypt_assertion=False,
463+
encrypt_assertion_self_contained=True,
464+
#encrypted_advice_attributes=True,
465+
pefim=True,
466+
)
467+
468+
resp_str = "%s" % resp
469+
470+
resp_str = base64.encodestring(resp_str)
471+
472+
authn_response = _client.parse_authn_request_response(
473+
resp_str, BINDING_HTTP_POST,
474+
{"id1": "http://foo.example.com/service"}, pefim=True)
475+
476+
self.verify_authn_response(idp, authn_response, _client, ava_verify)
477+
478+
def setup_verify_authn_response(self):
479+
idp = "urn:mace:example.com:saml:roland:idp"
480+
ava = {"givenName": ["Derek"], "surName": ["Jeter"], "mail": ["[email protected]"], "title": ["The man"]}
481+
ava_verify = {'mail': ['[email protected]'], 'givenName': ['Derek'], 'sn': ['Jeter'], 'title': ["The man"]}
482+
nameid_policy = samlp.NameIDPolicy(allow_create="false", format=saml.NAMEID_FORMAT_PERSISTENT)
483+
return idp, ava, ava_verify, nameid_policy
484+
485+
486+
def verify_authn_response(self, idp, authn_response, _client, ava_verify):
487+
assert authn_response is not None
488+
assert authn_response.issuer() == idp
489+
assert authn_response.response.assertion[0].issuer.text == idp
490+
session_info = authn_response.session_info()
491+
492+
assert session_info["ava"] == ava_verify
493+
assert session_info["issuer"] == idp
494+
assert session_info["came_from"] == "http://foo.example.com/service"
495+
response = samlp.response_from_string(authn_response.xmlstr)
496+
assert response.destination == "http://lingon.catalogix.se:8087/"
497+
498+
# One person in the cache
499+
assert len(_client.users.subjects()) == 1
500+
subject_id = _client.users.subjects()[0]
501+
# The information I have about the subject comes from one source
502+
assert _client.users.issuers_of_info(subject_id) == [idp]
503+
504+
371505
def test_init_values(self):
372506
entityid = self.client.config.entityid
373507
print(entityid)
@@ -764,4 +898,4 @@ def test_negotiated_post_sso(self):
764898
if __name__ == "__main__":
765899
tc = TestClient()
766900
tc.setup_class()
767-
tc.test_sign_then_encrypt_assertion_advice()
901+
tc.test_response_3()

0 commit comments

Comments
 (0)