Skip to content

Commit 3989b99

Browse files
Merge pull request #485 from skoranda/want_assertions_or_response_signed
Add want_assertions_or_response_signed functionality
2 parents 40a3699 + 8b79846 commit 3989b99

File tree

6 files changed

+260
-5
lines changed

6 files changed

+260
-5
lines changed

docs/howto/config.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,33 @@ Example::
624624
}
625625
}
626626

627+
want_assertions_or_response_signed
628+
""""""""""""""""""""
629+
630+
Indicates that *either* the Authentication Response *or* the assertions
631+
contained within the response to this SP must be signed.
632+
633+
Valid values are True or False. Default value is False.
634+
635+
This configuration directive **does not** override ``want_response_signed``
636+
or ``want_assertions_signed``. For example, if ``want_response_signed`` is True
637+
and the Authentication Response is not signed an exception will be thrown
638+
regardless of the value for this configuration directive.
639+
640+
Thus to configure the SP to accept either a signed response or signed assertions
641+
set ``want_response_signed`` and ``want_assertions_signed`` both to False and
642+
this directive to True.
643+
644+
Example::
645+
646+
"service": {
647+
"sp": {
648+
"want_response_signed": False,
649+
"want_assertions_signed": False,
650+
"want_assertions_or_response_signed": True
651+
}
652+
}
653+
627654

628655
idp/aa/sp
629656
^^^^^^^^^

src/saml2/client_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def __init__(self, config=None, identity_cache=None, state_cache=None,
120120
"authn_requests_signed": False,
121121
"want_assertions_signed": False,
122122
"want_response_signed": True,
123+
"want_assertions_or_response_signed" : False
123124
}
124125

125126
for attr, val_default in attribute_defaults.items():
@@ -135,7 +136,11 @@ def __init__(self, config=None, identity_cache=None, state_cache=None,
135136
setattr(self, attr, val)
136137

137138
if self.entity_type == "sp" and not any(
138-
[self.want_assertions_signed, self.want_response_signed]
139+
[
140+
self.want_assertions_signed,
141+
self.want_response_signed,
142+
self.want_assertions_or_response_signed,
143+
]
139144
):
140145
logger.warning(
141146
"The SAML service provider accepts unsigned SAML Responses "
@@ -691,6 +696,7 @@ def parse_authn_request_response(self, xmlstr, binding, outstanding=None,
691696
"outstanding_certs": outstanding_certs,
692697
"allow_unsolicited": self.allow_unsolicited,
693698
"want_assertions_signed": self.want_assertions_signed,
699+
"want_assertions_or_response_signed": self.want_assertions_or_response_signed,
694700
"want_response_signed": self.want_response_signed,
695701
"return_addrs": self.service_urls(binding=binding),
696702
"entity_id": self.config.entityid,

src/saml2/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"subject_data",
7878
"want_response_signed",
7979
"want_assertions_signed",
80+
"want_assertions_or_response_signed",
8081
"authn_requests_signed",
8182
"name_form",
8283
"endpoints",

src/saml2/entity.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from saml2.saml import EncryptedAssertion
3131
from saml2.saml import Issuer
3232
from saml2.saml import NAMEID_FORMAT_ENTITY
33+
from saml2.response import AuthnResponse
3334
from saml2.response import LogoutResponse
3435
from saml2.response import UnsolicitedResponse
3536
from saml2.time_util import instant
@@ -63,6 +64,7 @@
6364
from saml2.sigver import security_context
6465
from saml2.sigver import response_factory
6566
from saml2.sigver import SigverError
67+
from saml2.sigver import SignatureError
6668
from saml2.sigver import make_temp
6769
from saml2.sigver import pre_encryption_part
6870
from saml2.sigver import pre_signature_part
@@ -1136,17 +1138,36 @@ def _parse_response(self, xmlstr, response_cls, service, binding,
11361138
return None
11371139

11381140
try:
1141+
response_is_signed = False
1142+
# Record the response signature requirement.
1143+
require_response_signature = response.require_response_signature
1144+
# Force the requirement that the response be signed in order to
1145+
# force signature checking to happen so that we can know whether
1146+
# or not the response is signed. The attribute on the response class
1147+
# is reset to the recorded value in the finally clause below.
1148+
response.require_response_signature = True
11391149
response = response.loads(xmlstr, False, origxml=xmlstr)
11401150
except SigverError as err:
1141-
logger.error("Signature Error: %s", err)
1142-
raise
1151+
if require_response_signature:
1152+
logger.error("Signature Error: %s", err)
1153+
raise
1154+
else:
1155+
# The response is not signed but a signature is not required
1156+
# so reset the attribute on the response class to the recorded
1157+
# value and attempt to consume the unpacked XML again.
1158+
response.require_response_signature = require_response_signature
1159+
response = response.loads(xmlstr, False, origxml=xmlstr)
11431160
except UnsolicitedResponse:
11441161
logger.error("Unsolicited response")
11451162
raise
11461163
except Exception as err:
11471164
if "not well-formed" in "%s" % err:
11481165
logger.error("Not well-formed XML")
11491166
raise
1167+
else:
1168+
response_is_signed = True
1169+
finally:
1170+
response.require_response_signature = require_response_signature
11501171

11511172
logger.debug("XMLSTR: %s", xmlstr)
11521173

@@ -1166,7 +1187,40 @@ def _parse_response(self, xmlstr, response_cls, service, binding,
11661187
for _cert in cert:
11671188
keys.append(_cert["key"])
11681189

1169-
response = response.verify(keys)
1190+
try:
1191+
assertions_are_signed = False
1192+
# Record the assertions signature requirement.
1193+
require_signature = response.require_signature
1194+
# Force the requirement that the assertions be signed in order to
1195+
# force signature checking to happen so that we can know whether
1196+
# or not the assertions are signed. The attribute on the response class
1197+
# is reset to the recorded value in the finally clause below.
1198+
response.require_signature = True
1199+
# Verify that the assertion is syntactically correct and the
1200+
# signature on the assertion is correct if present.
1201+
response = response.verify(keys)
1202+
except SignatureError as err:
1203+
if require_signature:
1204+
logger.error("Signature Error: %s", err)
1205+
raise
1206+
else:
1207+
response.require_signature = require_signature
1208+
response = response.verify(keys)
1209+
except Exception as err:
1210+
logger.error("Exception verifying assertion: %s" % err)
1211+
else:
1212+
assertions_are_signed = True
1213+
finally:
1214+
response.require_signature = require_signature
1215+
1216+
# If so configured enforce that either the response is signed
1217+
# or the assertions within it are signed.
1218+
if response.require_signature_or_response_signature:
1219+
if not response_is_signed and not assertions_are_signed:
1220+
msg = "Neither the response nor the assertions are signed"
1221+
logger.error(msg)
1222+
raise SigverError(msg)
1223+
11701224
return response
11711225

11721226
# ------------------------------------------------------------------------

src/saml2/response.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ def __init__(self, sec_context, return_addrs=None, timeslack=0,
271271
self.signature_check = self.sec.correctly_signed_response
272272
self.require_signature = False
273273
self.require_response_signature = False
274+
self.require_signature_or_response_signature = False
274275
self.not_signed = False
275276
self.asynchop = asynchop
276277
self.do_not_verify = False
@@ -474,7 +475,9 @@ def __init__(self, sec_context, attribute_converters, entity_id,
474475
return_addrs=None, outstanding_queries=None,
475476
timeslack=0, asynchop=True, allow_unsolicited=False,
476477
test=False, allow_unknown_attributes=False,
477-
want_assertions_signed=False, want_response_signed=False,
478+
want_assertions_signed=False,
479+
want_assertions_or_response_signed=False,
480+
want_response_signed=False,
478481
conv_info=None, **kwargs):
479482

480483
StatusResponse.__init__(self, sec_context, return_addrs, timeslack,
@@ -493,6 +496,7 @@ def __init__(self, sec_context, attribute_converters, entity_id,
493496
self.session_not_on_or_after = 0
494497
self.allow_unsolicited = allow_unsolicited
495498
self.require_signature = want_assertions_signed
499+
self.require_signature_or_response_signature = want_assertions_or_response_signed
496500
self.require_response_signature = want_response_signed
497501
self.test = test
498502
self.allow_unknown_attributes = allow_unknown_attributes
@@ -1277,6 +1281,14 @@ def __init__(self, sec_context, attribute_converters, timeslack=0,
12771281
self.context = "AssertionIdResponse"
12781282
self.signature_check = self.sec.correctly_signed_assertion_id_response
12791283

1284+
# Because this class is not a subclass of StatusResponse we need
1285+
# to add these attributes directly so that the _parse_response()
1286+
# method of the Entity class can treat instances of this class
1287+
# like all other responses.
1288+
self.require_signature = False
1289+
self.require_response_signature = False
1290+
self.require_signature_or_response_signature = False
1291+
12801292
def loads(self, xmldata, decode=True, origxml=None):
12811293
# own copy
12821294
self.xmlstr = xmldata[:]

tests/test_51_client.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from saml2.sigver import pre_encryption_part, pre_encrypt_assertion
3838
from saml2.sigver import rm_xmltag
3939
from saml2.sigver import verify_redirect_signature
40+
from saml2.sigver import SignatureError, SigverError
4041
from saml2.s_utils import do_attribute_statement
4142
from saml2.s_utils import factory
4243
from saml2.time_util import in_a_while, a_while_ago
@@ -1487,6 +1488,160 @@ def test_do_logout_session_expired(self):
14871488
BINDING_HTTP_POST)
14881489
assert b'<ns0:SessionIndex>_foo</ns0:SessionIndex>' in res.xmlstr
14891490

1491+
def test_signature_wants(self):
1492+
1493+
ava = {
1494+
"givenName": ["Derek"],
1495+
"sn": ["Jeter"],
1496+
"mail": ["[email protected]"],
1497+
"title": ["The man"]
1498+
}
1499+
1500+
nameid_policy = samlp.NameIDPolicy(
1501+
allow_create="false",
1502+
format=saml.NAMEID_FORMAT_PERSISTENT)
1503+
1504+
kwargs = {
1505+
"identity": ava,
1506+
"in_response_to": "id1",
1507+
"destination": "http://lingon.catalogix.se:8087/",
1508+
"sp_entity_id": "urn:mace:example.com:saml:roland:sp",
1509+
"name_id_policy": nameid_policy,
1510+
"userid": "[email protected]",
1511+
"authn": AUTHN
1512+
}
1513+
1514+
outstanding = {"id1": "http://foo.example.com/service"}
1515+
1516+
def create_authn_response(**kwargs):
1517+
return encode_fn(
1518+
str(self.server.create_authn_response(**kwargs)).encode())
1519+
1520+
def parse_authn_response(response):
1521+
self.client.parse_authn_request_response(response,
1522+
BINDING_HTTP_POST, outstanding)
1523+
1524+
def set_client_want(response, assertion, either):
1525+
self.client.want_response_signed = response
1526+
self.client.want_assertions_signed = assertion
1527+
self.client.want_assertions_or_response_signed = either
1528+
1529+
# Response is signed but assertion is not.
1530+
kwargs["sign_response"] = True
1531+
kwargs["sign_assertion"] = False
1532+
response = create_authn_response(**kwargs)
1533+
1534+
set_client_want(True, True, True)
1535+
raises(SignatureError, parse_authn_response, response)
1536+
1537+
set_client_want(True, True, False)
1538+
raises(SignatureError, parse_authn_response, response)
1539+
1540+
set_client_want(True, False, True)
1541+
parse_authn_response(response)
1542+
1543+
set_client_want(True, False, False)
1544+
parse_authn_response(response)
1545+
1546+
set_client_want(False, True, True)
1547+
raises(SignatureError, parse_authn_response, response)
1548+
1549+
set_client_want(False, True, False)
1550+
raises(SignatureError, parse_authn_response, response)
1551+
1552+
set_client_want(False, False, True)
1553+
parse_authn_response(response)
1554+
1555+
set_client_want(False, False, False)
1556+
parse_authn_response(response)
1557+
1558+
# Response is not signed but assertion is signed.
1559+
kwargs["sign_response"] = False
1560+
kwargs["sign_assertion"] = True
1561+
response = create_authn_response(**kwargs)
1562+
1563+
set_client_want(True, True, True)
1564+
raises(SignatureError, parse_authn_response, response)
1565+
1566+
set_client_want(True, True, False)
1567+
raises(SignatureError, parse_authn_response, response)
1568+
1569+
set_client_want(True, False, True)
1570+
raises(SignatureError, parse_authn_response, response)
1571+
1572+
set_client_want(True, False, False)
1573+
raises(SignatureError, parse_authn_response, response)
1574+
1575+
set_client_want(False, True, True)
1576+
parse_authn_response(response)
1577+
1578+
set_client_want(False, True, False)
1579+
parse_authn_response(response)
1580+
1581+
set_client_want(False, False, True)
1582+
parse_authn_response(response)
1583+
1584+
set_client_want(False, False, False)
1585+
parse_authn_response(response)
1586+
1587+
# Both response and assertion are signed.
1588+
kwargs["sign_response"] = True
1589+
kwargs["sign_assertion"] = True
1590+
response = create_authn_response(**kwargs)
1591+
1592+
set_client_want(True, True, True)
1593+
parse_authn_response(response)
1594+
1595+
set_client_want(True, True, False)
1596+
parse_authn_response(response)
1597+
1598+
set_client_want(True, False, True)
1599+
parse_authn_response(response)
1600+
1601+
set_client_want(True, False, False)
1602+
parse_authn_response(response)
1603+
1604+
set_client_want(False, True, True)
1605+
parse_authn_response(response)
1606+
1607+
set_client_want(False, True, False)
1608+
parse_authn_response(response)
1609+
1610+
set_client_want(False, False, True)
1611+
parse_authn_response(response)
1612+
1613+
set_client_want(False, False, False)
1614+
parse_authn_response(response)
1615+
1616+
# Neither response nor assertion is signed.
1617+
kwargs["sign_response"] = False
1618+
kwargs["sign_assertion"] = False
1619+
response = create_authn_response(**kwargs)
1620+
1621+
set_client_want(True, True, True)
1622+
raises(SignatureError, parse_authn_response, response)
1623+
1624+
set_client_want(True, True, False)
1625+
raises(SignatureError, parse_authn_response, response)
1626+
1627+
set_client_want(True, False, True)
1628+
raises(SignatureError, parse_authn_response, response)
1629+
1630+
set_client_want(True, False, False)
1631+
raises(SignatureError, parse_authn_response, response)
1632+
1633+
set_client_want(False, True, True)
1634+
raises(SignatureError, parse_authn_response, response)
1635+
1636+
set_client_want(False, True, False)
1637+
raises(SignatureError, parse_authn_response, response)
1638+
1639+
set_client_want(False, False, True)
1640+
raises(SigverError, parse_authn_response, response)
1641+
1642+
set_client_want(False, False, False)
1643+
parse_authn_response(response)
1644+
14901645

14911646
class TestClientNonAsciiAva:
14921647
def setup_class(self):

0 commit comments

Comments
 (0)