Skip to content

Commit b6336dc

Browse files
author
Roland Hedberg
committed
Fixed all tests and various bugs that appeared during that process.
1 parent c2701e9 commit b6336dc

27 files changed

+132353
-44133
lines changed

example/idp2/idp.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from urlparse import parse_qs
1111
from Cookie import SimpleCookie
12-
import subprocess
1312
import os
1413

1514
from saml2 import server
@@ -125,7 +124,12 @@ def operation(self, _dict, binding):
125124
resp = BadRequest('Error parsing request or no request')
126125
return resp(self.environ, self.start_response)
127126
else:
128-
return self.do(_dict["SAMLRequest"], binding, _dict["RelayState"])
127+
try:
128+
return self.do(_dict["SAMLRequest"], binding,
129+
_dict["RelayState"])
130+
except KeyError:
131+
# Can live with no relay state
132+
return self.do(_dict["SAMLRequest"], binding)
129133

130134
def artifact_operation(self, _dict):
131135
if not _dict:
@@ -134,7 +138,11 @@ def artifact_operation(self, _dict):
134138
else:
135139
# exchange artifact for request
136140
request = IDP.artifact2message(_dict["SAMLart"], "spsso")
137-
return self.do(request, BINDING_HTTP_ARTIFACT, _dict["RelayState"])
141+
try:
142+
return self.do(request, BINDING_HTTP_ARTIFACT,
143+
_dict["RelayState"])
144+
except KeyError:
145+
return self.do(request, BINDING_HTTP_ARTIFACT)
138146

139147
def response(self, binding, http_args):
140148
if binding == BINDING_HTTP_ARTIFACT:
@@ -814,6 +822,7 @@ def set_cookie(name, _, *args):
814822

815823
# ----------------------------------------------------------------------------
816824

825+
817826
def metadata(environ, start_response):
818827
try:
819828
path = args.path
@@ -830,6 +839,7 @@ def metadata(environ, start_response):
830839
logger.error("An error occured while creating metadata:" + ex.message)
831840
return not_found(environ, start_response)
832841

842+
833843
def application(environ, start_response):
834844
"""
835845
The main WSGI application. Dispatch the current request to
@@ -890,21 +900,15 @@ def application(environ, start_response):
890900

891901
# ----------------------------------------------------------------------------
892902

893-
from mako.lookup import TemplateLookup
894-
895-
ROOT = './'
896-
LOOKUP = TemplateLookup(directories=[ROOT + 'templates', ROOT + 'htdocs'],
897-
module_directory=ROOT + 'modules',
898-
input_encoding='utf-8', output_encoding='utf-8')
899903

900904
# ----------------------------------------------------------------------------
901905

902906
if __name__ == '__main__':
903-
import sys
904907
import socket
905908
from idp_user import USERS
906909
from idp_user import EXTRA
907910
from wsgiref.simple_server import make_server
911+
from mako.lookup import TemplateLookup
908912

909913
parser = argparse.ArgumentParser()
910914
parser.add_argument('-p', dest='path', help='Path to configuration file.')
@@ -918,9 +922,15 @@ def application(environ, start_response):
918922
parser.add_argument('-n', dest='name')
919923
parser.add_argument('-s', dest='sign', action='store_true',
920924
help="sign the metadata")
925+
parser.add_argument('-m', dest='mako_root', default="./")
921926
parser.add_argument(dest="config")
922927
args = parser.parse_args()
923928

929+
_rot = args.mako_root
930+
LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'],
931+
module_directory=_rot + 'modules',
932+
input_encoding='utf-8', output_encoding='utf-8')
933+
924934
PORT = 8088
925935

926936
AUTHN_BROKER = AuthnBroker()

example/idp2/idp_conf.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from saml2.saml import NAME_FORMAT_URI
88
from saml2.saml import NAMEID_FORMAT_TRANSIENT
99
from saml2.saml import NAMEID_FORMAT_PERSISTENT
10+
import os.path
1011

1112
try:
1213
from saml2.sigver import get_xmlsec_binary
@@ -18,13 +19,20 @@
1819
else:
1920
xmlsec_path = '/usr/bin/xmlsec1'
2021

22+
BASEDIR = os.path.abspath(os.path.dirname(__file__))
23+
24+
25+
def full_path(local_file):
26+
return os.path.join(BASEDIR, local_file)
27+
2128
#BASE = "http://lingon.ladok.umu.se:8088"
2229
#BASE = "http://lingon.catalogix.se:8088"
2330
BASE = "http://localhost:8088"
2431

2532
CONFIG = {
2633
"entityid": "%s/idp.xml" % BASE,
2734
"description": "My IDP",
35+
"valid_for": 168,
2836
"service": {
2937
"aa": {
3038
"endpoints": {
@@ -86,10 +94,10 @@
8694
},
8795
},
8896
"debug": 1,
89-
"key_file": "pki/mykey.pem",
90-
"cert_file": "pki/mycert.pem",
97+
"key_file": full_path("pki/mykey.pem"),
98+
"cert_file": full_path("pki/mycert.pem"),
9199
"metadata": {
92-
"local": ["../sp/sp.xml"],
100+
"local": [full_path("../sp/sp.xml")],
93101
},
94102
"organization": {
95103
"display_name": "Rolands Identiteter",
@@ -111,7 +119,7 @@
111119
# This database holds the map between a subjects local identifier and
112120
# the identifier returned to a SP
113121
"xmlsec_binary": xmlsec_path,
114-
"attribute_map_dir": "../attributemaps",
122+
#"attribute_map_dir": "../attributemaps",
115123
"logger": {
116124
"rotating": {
117125
"filename": "idp.log",

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def run_tests(self):
4343
'paste',
4444
'zope.interface',
4545
'repoze.who',
46-
'm2crypto'
46+
'pycrypto', 'Crypto'
4747
]
4848

4949
tests_require = [

src/saml2/aes.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python
2+
import os
3+
from Crypto import Random
4+
from Crypto.Cipher import AES
5+
from base64 import b64encode, b64decode
6+
7+
__author__ = 'rolandh'
8+
9+
POSTFIX_MODE = {
10+
"cbc": AES.MODE_CBC,
11+
"cfb": AES.MODE_CFB,
12+
"ecb": AES.MODE_CFB,
13+
}
14+
15+
BLOCK_SIZE = 16
16+
17+
18+
class AESCipher(object):
19+
def __init__(self, key, iv=""):
20+
"""
21+
22+
:param key: The encryption key
23+
:param iv: Init vector
24+
:return: AESCipher instance
25+
"""
26+
self.key = key
27+
self.iv = iv
28+
29+
def build_cipher(self, iv="", alg="aes_128_cbc"):
30+
"""
31+
:param iv: init vector
32+
:param alg: cipher algorithm
33+
:return: A Cipher instance
34+
"""
35+
typ, bits, cmode = alg.split("_")
36+
37+
if not iv:
38+
if self.iv:
39+
iv = self.iv
40+
else:
41+
iv = Random.new().read(AES.block_size)
42+
else:
43+
assert len(iv) == AES.block_size
44+
45+
if bits not in ["128", "192", "256"]:
46+
raise Exception("Unsupported key length")
47+
try:
48+
assert len(self.key) == int(bits) >> 3
49+
except AssertionError:
50+
raise Exception("Wrong Key length")
51+
52+
try:
53+
return AES.new(self.key, POSTFIX_MODE[cmode], iv), iv
54+
except KeyError:
55+
raise Exception("Unsupported chaining mode")
56+
57+
58+
def encrypt(self, msg, iv=None, alg="aes_128_cbc", padding="PKCS#7",
59+
b64enc=True, block_size=BLOCK_SIZE):
60+
"""
61+
:param key: The encryption key
62+
:param iv: init vector
63+
:param msg: Message to be encrypted
64+
:param padding: Which padding that should be used
65+
:param b64enc: Whether the result should be base64encoded
66+
:param block_size: If PKCS#7 padding which block size to use
67+
:return: The encrypted message
68+
"""
69+
70+
if padding == "PKCS#7":
71+
_block_size = block_size
72+
elif padding == "PKCS#5":
73+
_block_size = 8
74+
else:
75+
_block_size = 0
76+
77+
if _block_size:
78+
plen = _block_size - (len(msg) % _block_size)
79+
c = chr(plen)
80+
msg += c*plen
81+
82+
cipher, iv = self.build_cipher(iv, alg)
83+
cmsg = iv + cipher.encrypt(msg)
84+
if b64enc:
85+
return b64encode(cmsg)
86+
else:
87+
return cmsg
88+
89+
90+
def decrypt(self, msg, iv=None, padding="PKCS#7", b64dec=True):
91+
"""
92+
:param key: The encryption key
93+
:param iv: init vector
94+
:param msg: Base64 encoded message to be decrypted
95+
:return: The decrypted message
96+
"""
97+
if b64dec:
98+
data = b64decode(msg)
99+
else:
100+
data = msg
101+
102+
_iv = data[:AES.block_size]
103+
if iv:
104+
assert iv == _iv
105+
cipher, iv = self.build_cipher(iv)
106+
res = cipher.decrypt(data)[AES.block_size:]
107+
if padding in ["PKCS#5", "PKCS#7"]:
108+
res = res[:-ord(res[-1])]
109+
return res
110+
111+
if __name__ == "__main__":
112+
key_ = "1234523451234545" # 16 byte key
113+
# Iff padded, the message doesn't have to be multiple of 16 in length
114+
msg_ = "ToBeOrNotTobe W.S."
115+
aes = AESCipher(key_)
116+
iv_ = os.urandom(16)
117+
encrypted_msg = aes.encrypt(key_, msg_, iv_)
118+
txt = aes.decrypt(key_, encrypted_msg, iv_)
119+
assert txt == msg_
120+
121+
encrypted_msg = aes.encrypt(key_, msg_, 0)
122+
txt = aes.decrypt(key_, encrypted_msg, 0)
123+
assert txt == msg_

src/saml2/assertion.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,12 @@ def entity_category_attributes(self, ec):
422422
return []
423423

424424
def get_entity_categories_restriction(self, sp_entity_id, mds):
425+
"""
426+
427+
:param sp_entity_id:
428+
:param mds: MetadataStore instance
429+
:return: A dictionary with restrictionsmetat
430+
"""
425431
if not self._restrictions:
426432
return None
427433

@@ -697,7 +703,7 @@ def construct(self, sp_entity_id, in_response_to, consumer_url,
697703
_ass.authn_statement = [_authn_statement]
698704

699705
if not attr_statement.empty():
700-
_ass.attribute_statement=[attr_statement],
706+
_ass.attribute_statement=[attr_statement]
701707

702708
return _ass
703709

src/saml2/attribute_converter.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,53 @@ def to_local(acs, statement, allow_unknown_attributes=False):
169169
return ava
170170

171171

172+
def list_to_local(acs, attrlist, allow_unknown_attributes=False):
173+
""" Replaces the attribute names in a attribute value assertion with the
174+
equivalent name from a local name format.
175+
176+
:param acs: List of Attribute Converters
177+
:param attrlist: List of Attributes
178+
:param allow_unknown_attributes: If unknown attributes are allowed
179+
:return: A key,values dictionary
180+
"""
181+
if not acs:
182+
acs = [AttributeConverter()]
183+
acsd = {"": acs}
184+
else:
185+
acsd = dict([(a.name_format, a) for a in acs])
186+
187+
ava = {}
188+
for attr in attrlist:
189+
try:
190+
_func = acsd[attr.name_format].ava_from
191+
except KeyError:
192+
if attr.name_format == NAME_FORMAT_UNSPECIFIED or \
193+
allow_unknown_attributes:
194+
_func = acs[0].lcd_ava_from
195+
else:
196+
logger.info("Unsupported attribute name format: %s" % (
197+
attr.name_format,))
198+
continue
199+
200+
try:
201+
key, val = _func(attr)
202+
except KeyError:
203+
if allow_unknown_attributes:
204+
key, val = acs[0].lcd_ava_from(attr)
205+
else:
206+
logger.info("Unknown attribute name: %s" % (attr,))
207+
continue
208+
except AttributeError:
209+
continue
210+
211+
try:
212+
ava[key].extend(val)
213+
except KeyError:
214+
ava[key] = val
215+
216+
return ava
217+
218+
172219
def from_local(acs, ava, name_format):
173220
for aconv in acs:
174221
#print ac.format, name_format

src/saml2/authn.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from urlparse import urlsplit
55
import time
66
from saml2 import SAMLError
7-
from saml2.cipher import AES
7+
from saml2.aes import AESCipher
88
from saml2.httputil import Response
99
from saml2.httputil import make_cookie
1010
from saml2.httputil import Redirect
@@ -110,7 +110,7 @@ def __init__(self, srv, mako_template, template_lookup, pwd, return_to):
110110
self.return_to = return_to
111111
self.active = {}
112112
self.query_param = "upm_answer"
113-
self.aes = AES(srv.iv)
113+
self.aes = AESCipher(self.srv.symkey, srv.iv)
114114

115115
def __call__(self, cookie=None, policy_url=None, logo_url=None,
116116
query="", **kwargs):
@@ -159,8 +159,7 @@ def verify(self, request, **kwargs):
159159
try:
160160
assert _dict["password"][0] == self.passwd[_dict["login"][0]]
161161
timestamp = str(int(time.mktime(time.gmtime())))
162-
info = self.aes.encrypt(self.srv.symkey,
163-
"::".join([_dict["login"][0], timestamp]))
162+
info = self.aes.encrypt("::".join([_dict["login"][0], timestamp]))
164163
self.active[info] = timestamp
165164
cookie = make_cookie(self.cookie_name, info, self.srv.seed)
166165
return_to = create_return_url(self.return_to, _dict["query"][0],
@@ -180,8 +179,7 @@ def authenticated_as(self, cookie=None, **kwargs):
180179
info, timestamp = parse_cookie(self.cookie_name,
181180
self.srv.seed, cookie)
182181
if self.active[info] == timestamp:
183-
uid, _ts = self.aes.decrypt(self.srv.symkey,
184-
info).split("::")
182+
uid, _ts = self.aes.decrypt(info).split("::")
185183
if timestamp == _ts:
186184
return {"uid": uid}
187185
except Exception:

0 commit comments

Comments
 (0)