Error reporting clarified: CRITICAL level for unexpected conditions in saml2test; ERROR and lower for conditions in the test target; various documentation improvements
This commit is contained in:
parent
781fbe4393
commit
a2a4698936
@ -24,7 +24,7 @@ These files should be stored outside the saml2test package to have a clean separ
|
|||||||
Client (sp_test/__init__.py)
|
Client (sp_test/__init__.py)
|
||||||
.........................
|
.........................
|
||||||
Its life cycle is responsible for following activites:
|
Its life cycle is responsible for following activites:
|
||||||
- read config files and command line argumants
|
- read config files and command line argumants (the test driver's config is "json_config")
|
||||||
- initialize the test driver IDP
|
- initialize the test driver IDP
|
||||||
- initialize a Conversation
|
- initialize a Conversation
|
||||||
- start the Conversion with .do_sequence_and_tests()
|
- start the Conversion with .do_sequence_and_tests()
|
||||||
@ -33,35 +33,37 @@ Its life cycle is responsible for following activites:
|
|||||||
Conversation (sp_test/base.py)
|
Conversation (sp_test/base.py)
|
||||||
..............................
|
..............................
|
||||||
|
|
||||||
operation (oper)
|
Operation (oper)
|
||||||
................
|
................
|
||||||
- comprises an id, name, sequence and tests
|
- Comprises an id, name, sequence and tests
|
||||||
- names oper in
|
|
||||||
- Example: 'sp-00': {"name": 'Basic Login test', "sequence": [(Login, AuthnRequest, AuthnResponse, None)], "tests": {"pre": [], "post": []}
|
- Example: 'sp-00': {"name": 'Basic Login test', "sequence": [(Login, AuthnRequest, AuthnResponse, None)], "tests": {"pre": [], "post": []}
|
||||||
- Names a use case in STHREP
|
|
||||||
|
|
||||||
OPERATIONS
|
OPERATIONS
|
||||||
..........
|
..........
|
||||||
- set of operations provided in sp_test
|
- set of operations provided in sp_test
|
||||||
- can be listed with the -l command line option
|
- can be listed with the -l command line option
|
||||||
|
|
||||||
sequence
|
Sequence
|
||||||
........
|
........
|
||||||
- A list of flows
|
- A list of flows
|
||||||
- Example: see "sequence" item in operation dictionary
|
- Example: see "sequence" item in operation dict
|
||||||
|
|
||||||
test (in the context of an operation)
|
Test (in the context of an operation)
|
||||||
....
|
....
|
||||||
- class to be executed as part of an operation, either before ("pre") or after ("post") the sequence
|
- class to be executed as part of an operation, either before ("pre") or after ("post") the sequence or inbetween a SAML request and response ("mid").
|
||||||
|
There are standard tests with the Request class (VerifyAuthnRequest) and operation-specific tests.
|
||||||
|
- Example for an operation-specific "mid" test: VerifyIfRequestIsSigned
|
||||||
|
- A test may be specified together with an argument as a tupel
|
||||||
|
|
||||||
flow
|
Flow
|
||||||
....
|
....
|
||||||
- A tupel of classes that together implement an SAML request-response pair between IDP and SP (and possible a discovery service). A class can be derived from Request, Response or (other), Check or Operation.
|
* A tupel of classes that together implement an SAML request-response pair between IDP and SP (and possible other actors, such as a discovery service or IDP-proxy). A class can be derived from Request, Response (or other), Check or Operation.
|
||||||
- A flow for a solicited authentication consists of 4 classes:
|
* A flow for a solicited authentication consists of 4 classes:
|
||||||
flow[0]: Operation (Handling a login flow such as discovery or WAYF - not implemented yet)
|
|
||||||
flow[1]: Request (process the authentication request)
|
* flow[0]: Operation (Handling a login flow such as discovery or WAYF - not implemented yet)
|
||||||
flow[2]: Response (send the authentication response)
|
* flow[1]: Request (process the authentication request)
|
||||||
flow[3]: Check (optional - can be None. E.g. check the response if a correct error status was raised when sending a broken response)
|
* flow[2]: Response (send the authentication response)
|
||||||
|
* flow[3]: Check (optional - can be None. E.g. check the response if a correct error status was raised when sending a broken response)
|
||||||
|
|
||||||
Check (and subclasses)
|
Check (and subclasses)
|
||||||
.....
|
.....
|
||||||
@ -69,18 +71,17 @@ Check (and subclasses)
|
|||||||
- writes a structured test report to conv.test_output
|
- writes a structured test report to conv.test_output
|
||||||
- It can check for expected errors, which do not cause an exception but in contrary are reported as success
|
- It can check for expected errors, which do not cause an exception but in contrary are reported as success
|
||||||
|
|
||||||
interaction
|
Interaction
|
||||||
...........
|
...........
|
||||||
- An interaction automates a human interaction. It searches a response from a test target for some constants, and if
|
- An interaction automates a human interaction. It searches a response from a test target for some constants, and if
|
||||||
there is a match, it will create a response suitable response.
|
there is a match, it will create a response suitable response.
|
||||||
|
|
||||||
(2) Simplefied Flow
|
(2) Simplyfied Flow
|
||||||
:::::::::::::::::::
|
:::::::::::::::::::
|
||||||
|
|
||||||
The following pseudocdoe is an extract showing an overview of what is executed
|
The following pseudocode is an extract showing an overview of what is executed
|
||||||
for test sp-00:
|
for test sp-00::
|
||||||
|
|
||||||
::
|
|
||||||
do_sequence_and_test(self, oper, test):
|
do_sequence_and_test(self, oper, test):
|
||||||
self.test_sequence(tests["pre"]) # currently no tests defined for sp_test
|
self.test_sequence(tests["pre"]) # currently no tests defined for sp_test
|
||||||
for flow in oper:
|
for flow in oper:
|
||||||
@ -97,8 +98,8 @@ for test sp-00:
|
|||||||
else:
|
else:
|
||||||
self.handle_result()
|
self.handle_result()
|
||||||
|
|
||||||
send_idp_response(req, resp):
|
send_idp_response(req_flow, resp_flow):
|
||||||
self.test_sequence(req.tests["post"]) # execute "post"-tests (request has "VerifyContent"-test built in; others from config)
|
self.test_sequence(req_flow.tests["mid"]) # execute "mid"-tests (request has "VerifyContent"-test built in; others from config)
|
||||||
# this line stands for a part that is a bit more involved .. see source
|
# this line stands for a part that is a bit more involved .. see source
|
||||||
|
|
||||||
args.update(resp._response_args) # set userid, identity
|
args.update(resp._response_args) # set userid, identity
|
||||||
@ -129,3 +130,20 @@ for test sp-00:
|
|||||||
_txt = self.last_response.content
|
_txt = self.last_response.content
|
||||||
if self.last_response.status_code >= 400:
|
if self.last_response.status_code >= 400:
|
||||||
raise FatalError("Did not expected error")
|
raise FatalError("Did not expected error")
|
||||||
|
|
||||||
|
(3) Status Reporting
|
||||||
|
::::::::::::::::::::
|
||||||
|
The proper reporting of results is at the core of saml2test. Several conditions
|
||||||
|
must be considered:
|
||||||
|
|
||||||
|
#. An operation that was successful because the test target reports OK (e.g. HTTP 200)
|
||||||
|
#. An operation that was successful because the test target reports NOK as expected, e.g. because of an invalid signature - HTTP 500 could be the correct response
|
||||||
|
#. An errror in SAML2Test
|
||||||
|
#. An errror in configuration of SAML2Test
|
||||||
|
|
||||||
|
Status values are defined in saml2test.check like this:
|
||||||
|
INFORMATION = 0, OK = 1, WARNING = 2, ERROR = 3, CRITICAL = 4, INTERACTION = 5
|
||||||
|
|
||||||
|
|
||||||
|
There are 2 targets to write output to:
|
||||||
|
* Test_ouput is written to conv.test_ouput during the execution of the flows.
|
2
setup.py
2
setup.py
@ -26,7 +26,7 @@ setup(
|
|||||||
author = "Roland Hedberg",
|
author = "Roland Hedberg",
|
||||||
author_email = "roland.hedberg@adm.umu.se",
|
author_email = "roland.hedberg@adm.umu.se",
|
||||||
license="Apache 2.0",
|
license="Apache 2.0",
|
||||||
packages=["idp_test", "idp_test/package", "saml2test", "sp_test"],
|
packages=["idp_test", "idp_test/package", "saml2test", "sp_test", "utility"],
|
||||||
package_dir = {"": "src"},
|
package_dir = {"": "src"},
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
|
@ -9,8 +9,8 @@ import sys
|
|||||||
INFORMATION = 0
|
INFORMATION = 0
|
||||||
OK = 1
|
OK = 1
|
||||||
WARNING = 2
|
WARNING = 2
|
||||||
ERROR = 3
|
ERROR = 3 # an error condition in the test target
|
||||||
CRITICAL = 4
|
CRITICAL = 4 # an error condition in the test driver
|
||||||
INTERACTION = 5
|
INTERACTION = 5
|
||||||
|
|
||||||
STATUSCODE = ["INFORMATION", "OK", "WARNING", "ERROR", "CRITICAL",
|
STATUSCODE = ["INFORMATION", "OK", "WARNING", "ERROR", "CRITICAL",
|
||||||
@ -124,8 +124,8 @@ class CheckErrorResponse(ExpectedError):
|
|||||||
|
|
||||||
class VerifyBadRequestResponse(ExpectedError):
|
class VerifyBadRequestResponse(ExpectedError):
|
||||||
"""
|
"""
|
||||||
Verifies that the OP returned a 400 Bad Request response containing a
|
Verifies that the test target returned a 400 Bad Request response
|
||||||
Error message.
|
containing a an error message.
|
||||||
"""
|
"""
|
||||||
cid = "verify-bad-request-response"
|
cid = "verify-bad-request-response"
|
||||||
msg = "OP error"
|
msg = "OP error"
|
||||||
@ -138,7 +138,7 @@ class VerifyBadRequestResponse(ExpectedError):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self._message = "Expected a 400 error message"
|
self._message = "Expected a 400 error message"
|
||||||
self._status = CRITICAL
|
self._status = ERROR
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -191,22 +191,22 @@ class Other(CriticalError):
|
|||||||
msg = "Other error"
|
msg = "Other error"
|
||||||
|
|
||||||
|
|
||||||
class CheckHTTPResponse(CriticalError):
|
class CheckSpHttpResponseOK(Error):
|
||||||
"""
|
"""
|
||||||
Checks that the HTTP response status is within the 200 or 300 range
|
Checks that the SP's HTTP response status is within the 200 or 300 range
|
||||||
"""
|
"""
|
||||||
cid = "check-http-response"
|
cid = "check-sp-http-response-ok"
|
||||||
msg = "OP error"
|
msg = "SP error OK"
|
||||||
|
|
||||||
def _func(self, conv):
|
def _func(self, conv):
|
||||||
_response = conv.last_response
|
_response = conv.last_response
|
||||||
_content = conv.last_content
|
_content = conv.last_response.content
|
||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
if _response.status_code >= 400:
|
if _response.status_code >= 400:
|
||||||
self._status = self.status
|
self._status = self.status
|
||||||
self._message = self.msg
|
self._message = self.msg
|
||||||
res["content"] = _content
|
#res["content"] = _content #too big + charset converstion needed
|
||||||
res["url"] = conv.position
|
res["url"] = conv.position
|
||||||
res["http_status"] = _response.status_code
|
res["http_status"] = _response.status_code
|
||||||
|
|
||||||
|
@ -74,8 +74,8 @@ class Client(object):
|
|||||||
help="Path to the configuration file for the IdP")
|
help="Path to the configuration file for the IdP")
|
||||||
self._parser.add_argument("-t", dest="testpackage",
|
self._parser.add_argument("-t", dest="testpackage",
|
||||||
help="Module describing tests")
|
help="Module describing tests")
|
||||||
self._parser.add_argument('-v', dest='verbose', action='store_true',
|
#self._parser.add_argument('-v', dest='verbose', action='store_true',
|
||||||
help="Print runtime information")
|
# help="Print runtime information") # unsused
|
||||||
self._parser.add_argument("-Y", dest="pysamllog", action='store_true',
|
self._parser.add_argument("-Y", dest="pysamllog", action='store_true',
|
||||||
help="Print PySAML2 logs")
|
help="Print PySAML2 logs")
|
||||||
self._parser.add_argument("oper", nargs="?", help="Which test to run")
|
self._parser.add_argument("oper", nargs="?", help="Which test to run")
|
||||||
|
@ -37,13 +37,13 @@ camel2underscore = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
|
|||||||
class Conversation():
|
class Conversation():
|
||||||
def __init__(self, instance, config, interaction, json_config,
|
def __init__(self, instance, config, interaction, json_config,
|
||||||
check_factory, entity_id, msg_factory=None,
|
check_factory, entity_id, msg_factory=None,
|
||||||
features=None, verbose=False, constraints=None,
|
features=None, constraints=None, # verbose=False,
|
||||||
expect_exception=None):
|
expect_exception=None, commandlineargs=None):
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self._config = config
|
self._config = config
|
||||||
self.test_output = []
|
self.test_output = []
|
||||||
self.features = features
|
self.features = features
|
||||||
self.verbose = verbose
|
#self.verbose = verbose # removed (not used)
|
||||||
self.check_factory = check_factory
|
self.check_factory = check_factory
|
||||||
self.msg_factory = msg_factory
|
self.msg_factory = msg_factory
|
||||||
self.expect_exception = expect_exception
|
self.expect_exception = expect_exception
|
||||||
@ -70,7 +70,7 @@ class Conversation():
|
|||||||
self.position = ""
|
self.position = ""
|
||||||
self.response = None
|
self.response = None
|
||||||
self.oper = None
|
self.oper = None
|
||||||
self.idp_constraints = constraints
|
self.msg_constraints = constraints
|
||||||
self.json_config = json_config
|
self.json_config = json_config
|
||||||
self.start_page = json_config["start_page"]
|
self.start_page = json_config["start_page"]
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ class Conversation():
|
|||||||
kwargs = {}
|
kwargs = {}
|
||||||
self.do_check(test, **kwargs)
|
self.do_check(test, **kwargs)
|
||||||
if test == ExpectedError:
|
if test == ExpectedError:
|
||||||
return False
|
return False # TODO: return value is unused
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def my_endpoints(self):
|
def my_endpoints(self):
|
||||||
@ -167,11 +167,6 @@ class Conversation():
|
|||||||
elif isinstance(response(), Check):
|
elif isinstance(response(), Check):
|
||||||
self.do_check(response)
|
self.do_check(response)
|
||||||
else:
|
else:
|
||||||
# rhoerbe: I guess that this branch is never used, therefore
|
|
||||||
# I am proposing this exception:
|
|
||||||
#raise FatalError("can't use " + response.__class__.__name__ +
|
|
||||||
# ", because it is not a subclass of 'Check'")
|
|
||||||
#
|
|
||||||
# A HTTP redirect or HTTP Post
|
# A HTTP redirect or HTTP Post
|
||||||
if 300 < self.last_response.status_code <= 303:
|
if 300 < self.last_response.status_code <= 303:
|
||||||
self._redirect(self.last_response)
|
self._redirect(self.last_response)
|
||||||
@ -241,11 +236,11 @@ class Conversation():
|
|||||||
break
|
break
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def send_idp_response(self, req, resp):
|
def send_idp_response(self, req_flow, resp_flow):
|
||||||
"""
|
"""
|
||||||
:param req: The expected request
|
:param req_flow: The flow to check the request
|
||||||
:param resp: The response type to be used
|
:param resp_flow: The flow to prepare the response
|
||||||
:return: A response
|
:return: The SP's HTTP response on receiving the SAML response
|
||||||
"""
|
"""
|
||||||
# make sure I got the request I expected
|
# make sure I got the request I expected
|
||||||
assert isinstance(self.saml_request.message, req._class)
|
assert isinstance(self.saml_request.message, req._class)
|
||||||
@ -257,8 +252,8 @@ class Conversation():
|
|||||||
|
|
||||||
# Pick information from the request that should be in the response
|
# Pick information from the request that should be in the response
|
||||||
args = self.instance.response_args(self.saml_request.message,
|
args = self.instance.response_args(self.saml_request.message,
|
||||||
[resp._binding])
|
[resp_flow._binding])
|
||||||
_mods = list(resp.__mro__[:])
|
_mods = list(resp_flow.__mro__[:])
|
||||||
_mods.reverse()
|
_mods.reverse()
|
||||||
for m in _mods:
|
for m in _mods:
|
||||||
try:
|
try:
|
||||||
@ -266,28 +261,32 @@ class Conversation():
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
args.update(resp._response_args)
|
args.update(resp_flow._response_args)
|
||||||
|
|
||||||
for param in ["identity", "userid"]:
|
for param in ["identity", "userid"]:
|
||||||
if param in self.json_config:
|
if param in self.json_config:
|
||||||
args[param] = self.json_config[param]
|
args[param] = self.json_config[param]
|
||||||
|
|
||||||
if resp == ErrorResponse:
|
if resp_flow == ErrorResponse:
|
||||||
func = getattr(self.instance, "create_error_response")
|
func = getattr(self.instance, "create_error_response")
|
||||||
else:
|
else:
|
||||||
_op = camel2underscore.sub(r'_\1', req._class.c_tag).lower()
|
_op = camel2underscore.sub(r'_\1', req_flow._class.c_tag).lower()
|
||||||
func = getattr(self.instance, "create_%s_response" % _op)
|
func = getattr(self.instance, "create_%s_response" % _op)
|
||||||
|
|
||||||
# get from config which parts shall be signed
|
# get from config which parts shall be signed
|
||||||
sign = []
|
sign = []
|
||||||
for styp in ["sign_assertion", "sign_response"]:
|
for styp in ["sign_assertion", "sign_response"]:
|
||||||
if styp in args:
|
if styp in args:
|
||||||
|
try:
|
||||||
if args[styp].lower() == "always":
|
if args[styp].lower() == "always":
|
||||||
sign.append(styp)
|
sign.append(styp)
|
||||||
del args[styp]
|
del args[styp]
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
raise AssertionError('config parameters "sign_assertion", '
|
||||||
|
'"sign_response" must be of type string')
|
||||||
|
|
||||||
response = func(**args)
|
response = func(**args)
|
||||||
response = resp(self).pre_processing(response)
|
response = resp_flow(self).pre_processing(response)
|
||||||
# and now for signing
|
# and now for signing
|
||||||
if sign:
|
if sign:
|
||||||
to_sign = []
|
to_sign = []
|
||||||
@ -315,21 +314,21 @@ class Conversation():
|
|||||||
response = signed_instance_factory(response, self.instance.sec,
|
response = signed_instance_factory(response, self.instance.sec,
|
||||||
to_sign)
|
to_sign)
|
||||||
|
|
||||||
info = self.instance.apply_binding(resp._binding, response,
|
info = self.instance.apply_binding(resp_flow._binding, response,
|
||||||
args["destination"],
|
args["destination"],
|
||||||
self.relay_state,
|
self.relay_state,
|
||||||
"SAMLResponse", resp._sign)
|
"SAMLResponse", resp_flow._sign)
|
||||||
|
|
||||||
if resp._binding == BINDING_HTTP_REDIRECT:
|
if resp_flow._binding == BINDING_HTTP_REDIRECT:
|
||||||
url = None
|
url = None
|
||||||
for param, value in info["headers"]:
|
for param, value in info["headers"]:
|
||||||
if param == "Location":
|
if param == "Location":
|
||||||
url = value
|
url = value
|
||||||
break
|
break
|
||||||
self.last_response = self.instance.send(url)
|
self.last_response = self.instance.send(url)
|
||||||
elif resp._binding == BINDING_HTTP_POST:
|
elif resp_flow._binding == BINDING_HTTP_POST:
|
||||||
resp = base64.b64encode("%s" % response)
|
resp_flow = base64.b64encode("%s" % response)
|
||||||
info["data"] = urllib.urlencode({"SAMLResponse": resp,
|
info["data"] = urllib.urlencode({"SAMLResponse": resp_flow,
|
||||||
"RelayState": self.relay_state})
|
"RelayState": self.relay_state})
|
||||||
info["method"] = "POST"
|
info["method"] = "POST"
|
||||||
info["headers"] = {
|
info["headers"] = {
|
||||||
@ -343,7 +342,7 @@ class Conversation():
|
|||||||
Solicited or 'un-solicited' flows.
|
Solicited or 'un-solicited' flows.
|
||||||
|
|
||||||
Solicited always starts with the Web client accessing a page.
|
Solicited always starts with the Web client accessing a page.
|
||||||
Un-solicited starts with the IDP sending something.
|
Un-solicited starts with the IDP sending a SAMl Response.
|
||||||
"""
|
"""
|
||||||
if len(flow) >= 3:
|
if len(flow) >= 3:
|
||||||
self.wb_send_GET_startpage()
|
self.wb_send_GET_startpage()
|
||||||
@ -373,7 +372,7 @@ class Conversation():
|
|||||||
break
|
break
|
||||||
except FatalError:
|
except FatalError:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception as err:
|
||||||
#self.err_check("exception", err)
|
#self.err_check("exception", err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from saml2 import BINDING_HTTP_REDIRECT
|
||||||
from saml2test.check import Check
|
from saml2test.check import Check
|
||||||
from saml2test.check import CRITICAL
|
from saml2test.check import CRITICAL
|
||||||
from saml2test import check
|
from saml2test import check
|
||||||
@ -9,27 +11,23 @@ from saml2test.interaction import Interaction
|
|||||||
|
|
||||||
__author__ = 'rolandh'
|
__author__ = 'rolandh'
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class VerifyContent(Check):
|
|
||||||
""" Basic content verification class, does required and max/min checks
|
class VerifyAuthnRequest(Check):
|
||||||
|
""" Basic AuthnRequest verification as provided by pysaml2
|
||||||
"""
|
"""
|
||||||
cid = "verify-content"
|
cid = "verify-authnrequest"
|
||||||
|
|
||||||
def _func(self, conv):
|
def _func(self, conv):
|
||||||
try:
|
try:
|
||||||
conv.saml_request.message.verify()
|
conv.saml_request.message.verify()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._status = CRITICAL
|
self._status = ERROR
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class VerifyAuthnRequest(VerifyContent):
|
|
||||||
""" Basic AuthnRequest verification as provided by pysaml2
|
|
||||||
"""
|
|
||||||
cid = "verify-authnrequest"
|
|
||||||
|
|
||||||
|
|
||||||
class MatchResult(Check):
|
class MatchResult(Check):
|
||||||
cid = "match-result"
|
cid = "match-result"
|
||||||
|
|
||||||
@ -42,14 +40,131 @@ class MatchResult(Check):
|
|||||||
|
|
||||||
class ErrorResponse(Check):
|
class ErrorResponse(Check):
|
||||||
cid = "saml-error"
|
cid = "saml-error"
|
||||||
msg = "Expected error message"
|
msg = "Expected error message, but test target returned OK"
|
||||||
|
|
||||||
def _func(self, conv):
|
def _func(self, conv):
|
||||||
try:
|
try:
|
||||||
assert conv.last_response.status_code >= 400
|
assert conv.last_response.status_code >= 400
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
self._message = self.msg
|
self._message = self.msg
|
||||||
self._status = CRITICAL
|
self._status = ERROR
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyDigestAlgorithm(Check):
|
||||||
|
"""
|
||||||
|
verify that the used digest algorithm was one from the approved set.
|
||||||
|
"""
|
||||||
|
cid = "verify-digest-algorithm"
|
||||||
|
|
||||||
|
|
||||||
|
def _digest_algo(self, signature, allowed):
|
||||||
|
_alg = signature.signed_info.reference[0].digest_method.algorithm
|
||||||
|
try:
|
||||||
|
assert alg in allowed
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "signature digest algorithm not allowed: " + alg
|
||||||
|
self._status = ERROR
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _func(self, conv):
|
||||||
|
if "digest_algorithm" not in conv.msg_constraints:
|
||||||
|
logger.info("Not verifying digest_algorithm (not configured)")
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
assert len(conv.msg_constraints["digest_algorithm"]) > 0
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "List of allowed digest algorithm must not be empty"
|
||||||
|
self._status = ERROR
|
||||||
|
return {}
|
||||||
|
_algs = conv.msg_constraints["digest_algorithm"]
|
||||||
|
|
||||||
|
request = conv.saml_request.message
|
||||||
|
|
||||||
|
if request.signature:
|
||||||
|
if not self._digest_algo(request.signature, _algs):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyIfRequestIsSigned(Check):
|
||||||
|
"""
|
||||||
|
verify that the request has been signed
|
||||||
|
"""
|
||||||
|
cid = "verify-if-request-is-signed"
|
||||||
|
|
||||||
|
def _func(self, conv):
|
||||||
|
try:
|
||||||
|
check_sig = conv.msg_constraints["authnRequest_signature_required"]
|
||||||
|
except KeyError:
|
||||||
|
check_sig = False
|
||||||
|
if check_sig:
|
||||||
|
if conv._binding == BINDING_HTTP_REDIRECT:
|
||||||
|
try:
|
||||||
|
assert conv.http_parameters.signature is not None
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "No AuthnRequest simple signature found"
|
||||||
|
self._status = ERROR
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
assert conv.saml_request.message.signature is not None
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "No AuthnRequest XML signature found"
|
||||||
|
self._status = ERROR
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
logger.debug("AuthnRequest signature is optional")
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class VerifySignatureAlgorithm(Check):
|
||||||
|
"""
|
||||||
|
verify that the used signature algorithm was one from an approved set.
|
||||||
|
"""
|
||||||
|
cid = "verify-signature-algorithm"
|
||||||
|
|
||||||
|
def _func(self, conv):
|
||||||
|
if "signature_algorithm" not in conv.msg_constraints:
|
||||||
|
logger.info("Not verifying signature_algorithm (not configured)")
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
assert len(conv.msg_constraints["signature_algorithm"]) > 0
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "List of allowed signature algorithm must " \
|
||||||
|
"not be empty"
|
||||||
|
self._status = ERROR
|
||||||
|
return {}
|
||||||
|
|
||||||
|
allowed_algs = [a[1] for a in conv.msg_constraints["signature_algorithm"]]
|
||||||
|
if conv._binding == BINDING_HTTP_REDIRECT:
|
||||||
|
if getattr(conv.http_parameters, "signature", None):
|
||||||
|
_alg = conv.http_parameters.sigalg
|
||||||
|
try:
|
||||||
|
assert _alg in allowed_algs
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "Algorithm not in white list for " \
|
||||||
|
"redirect signing: " + _alg
|
||||||
|
self._status = ERROR
|
||||||
|
else:
|
||||||
|
signature = getattr(conv.saml_request.message, "signature", None)
|
||||||
|
if signature:
|
||||||
|
try:
|
||||||
|
assert signature.signed_info.signature_method.algorithm in \
|
||||||
|
allowed_algs
|
||||||
|
except AssertionError:
|
||||||
|
self._message = "Wrong algorithm used for signing: '%s'" % \
|
||||||
|
signature.signed_info.signature_method.algorithm
|
||||||
|
self._status = ERROR
|
||||||
|
else:
|
||||||
|
self._message = "cannot verify signature algorithm: request not signed"
|
||||||
|
self._status = WARNING
|
||||||
|
return {}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@ -70,7 +185,7 @@ class VerifyEchopageContents(Check):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
self._message = "Cannot match expected static contents " \
|
self._message = "Cannot match expected static contents " \
|
||||||
"in SP echo page"
|
"in SP echo page"
|
||||||
self._status = CRITICAL
|
self._status = ERROR
|
||||||
for pattern in conv.json_config["echopageContentPattern"]:
|
for pattern in conv.json_config["echopageContentPattern"]:
|
||||||
m = re.search(pattern, conv.last_response.content)
|
m = re.search(pattern, conv.last_response.content)
|
||||||
try:
|
try:
|
||||||
@ -78,11 +193,11 @@ class VerifyEchopageContents(Check):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
self._message = 'Cannot match expected response value' \
|
self._message = 'Cannot match expected response value' \
|
||||||
', pattern="' + pattern + '"'
|
', pattern="' + pattern + '"'
|
||||||
self._status = CRITICAL
|
self._status = ERROR
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._message = 'Configuration error: missing key ' \
|
self._message = 'Configuration error: missing key ' \
|
||||||
'"echopageIdString" in test target config'
|
'"echopageIdString" in test target config'
|
||||||
self._status = CRITICAL
|
self._status = ERROR
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def call_on_redirect(self):
|
def call_on_redirect(self):
|
||||||
|
@ -4,6 +4,8 @@ from saml2.saml import NAME_FORMAT_URI
|
|||||||
__author__ = 'rolandh'
|
__author__ = 'rolandh'
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import xmldsig as ds
|
||||||
|
from saml2.saml import NAME_FORMAT_UNSPECIFIED, NAME_FORMAT_URI, NAME_FORMAT_BASIC
|
||||||
|
|
||||||
BASE = "http://localhost:8088"
|
BASE = "http://localhost:8088"
|
||||||
|
|
||||||
@ -70,18 +72,24 @@ info = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
# metadata source for the test target's EntityDescriptor:
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
"name_format": NAME_FORMAT_URI
|
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"signature_algorithm": [ # allowed for assertion & response signature
|
# test if attribute name format matches the given value. Absence of this
|
||||||
ds.SIG_RSA_SHA1,
|
# option or the value NAME_FORMAT_UNSPECIFIED will match any format
|
||||||
|
#"name_format": NAME_FORMAT_BASIC,
|
||||||
|
#"name_format": NAME_FORMAT_UNSPECIFIED,
|
||||||
|
"name_format": NAME_FORMAT_URI,
|
||||||
|
# allowed for assertion & response:
|
||||||
|
"signature_algorithm": [
|
||||||
|
#ds.SIG_RSA_SHA1, # you may need this for legacy deployments
|
||||||
ds.SIG_RSA_SHA224,
|
ds.SIG_RSA_SHA224,
|
||||||
ds.SIG_RSA_SHA256,
|
ds.SIG_RSA_SHA256,
|
||||||
ds.SIG_RSA_SHA384,
|
ds.SIG_RSA_SHA384,
|
||||||
ds.SIG_RSA_SHA512,
|
ds.SIG_RSA_SHA512,
|
||||||
],
|
],
|
||||||
"digest_algorithm": [
|
"digest_algorithm": [
|
||||||
ds.DIGEST_SHA1,
|
#ds.DIGEST_SHA1, # you may need this for legacy deployments
|
||||||
ds.DIGEST_SHA224,
|
ds.DIGEST_SHA224,
|
||||||
ds.DIGEST_SHA256,
|
ds.DIGEST_SHA256,
|
||||||
ds.DIGEST_SHA384,
|
ds.DIGEST_SHA384,
|
||||||
|
@ -49,7 +49,27 @@ info = {
|
|||||||
"echopageContentPattern": [r"Given Name\s*</td>\s*<td>Roland</td>",
|
"echopageContentPattern": [r"Given Name\s*</td>\s*<td>Roland</td>",
|
||||||
r"Userid\s*</td>\s*<td>roalnd</td>",
|
r"Userid\s*</td>\s*<td>roalnd</td>",
|
||||||
r"Surname\s*</td>\s*<td>Hedberg</td>",
|
r"Surname\s*</td>\s*<td>Hedberg</td>",
|
||||||
]
|
],
|
||||||
|
"constraints": {
|
||||||
|
"authnRequest_signature_required": True,
|
||||||
|
# allowed for assertion & response signature:
|
||||||
|
"signature_algorithm": [
|
||||||
|
#ds.SIG_RSA_SHA1, # you may need this for legacy deployments
|
||||||
|
ds.SIG_RSA_SHA224,
|
||||||
|
ds.SIG_RSA_SHA256,
|
||||||
|
ds.SIG_RSA_SHA384,
|
||||||
|
ds.SIG_RSA_SHA512,
|
||||||
|
],
|
||||||
|
"digest_algorithm": [
|
||||||
|
#ds.DIGEST_SHA1, # you may need this for legacy deployments
|
||||||
|
ds.DIGEST_SHA1,
|
||||||
|
ds.DIGEST_SHA224,
|
||||||
|
ds.DIGEST_SHA256,
|
||||||
|
ds.DIGEST_SHA384,
|
||||||
|
ds.DIGEST_SHA512,
|
||||||
|
ds.DIGEST_RIPEMD160,
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
print json.dumps(info)
|
print json.dumps(info)
|
Loading…
Reference in New Issue
Block a user