Skip to content

Commit 058cc80

Browse files
vkadamc00kiemon5ter
authored andcommitted
633: Support for redirect binding signature check using query param values
1 parent 5caf6da commit 058cc80

File tree

5 files changed

+150
-13
lines changed

5 files changed

+150
-13
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ venv.bak/
123123

124124
# PyCharm project files
125125
.idea/
126+
*.iml
126127

127128
# mkdocs documentation
128129
/site

src/saml2/entity.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,8 @@ def srv2typ(service):
10241024
else:
10251025
return typ
10261026

1027-
def _parse_request(self, enc_request, request_cls, service, binding):
1027+
def _parse_request(self, enc_request, request_cls, service, binding,
1028+
relay_state=None, sigalg=None, signature=None):
10281029
"""Parse a Request
10291030
10301031
:param enc_request: The request in its transport format
@@ -1070,7 +1071,9 @@ def _parse_request(self, enc_request, request_cls, service, binding):
10701071
if only_valid_cert:
10711072
must = True
10721073
_request = _request.loads(xmlstr, binding, origdoc=enc_request,
1073-
must=must, only_valid_cert=only_valid_cert)
1074+
must=must, only_valid_cert=only_valid_cert,
1075+
relay_state=relay_state, sigalg=sigalg,
1076+
signature=signature)
10741077

10751078
_log_debug("Loaded request")
10761079

src/saml2/request.py

+46-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from saml2.validate import valid_instance
88
from saml2.validate import NotValid
99
from saml2.response import IncorrectlySigned
10+
from saml2.sigver import verify_redirect_signature
11+
from saml2.sigver import SignatureError
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -37,20 +39,56 @@ def _clear(self):
3739
self.not_on_or_after = 0
3840

3941
def _loads(self, xmldata, binding=None, origdoc=None, must=None,
40-
only_valid_cert=False):
41-
if binding == BINDING_HTTP_REDIRECT:
42-
pass
43-
42+
only_valid_cert=False, relayState=None, sigalg=None, signature=None):
4443
# own copy
4544
self.xmlstr = xmldata[:]
46-
logger.debug("xmlstr: %s", self.xmlstr)
45+
logger.debug("xmlstr: %s, relayState: %s, sigalg: %s, signature: %s",
46+
self.xmlstr, relayState, sigalg, signature)
4747
try:
48+
# If redirect binding, and provided SigAlg, Signature use that to verify
49+
# and skip signatureCheck withing SAMLRequest/xmldata
50+
_do_redirect_sig_check = False
51+
_saml_msg = {}
52+
if binding == BINDING_HTTP_REDIRECT and must \
53+
and sigalg is not None and signature is not None:
54+
logger.debug("Request signature check will be done using query param,"
55+
" instead of SAMLRequest content")
56+
_do_redirect_sig_check = True
57+
must = False
58+
_saml_msg = {
59+
"SAMLRequest": origdoc,
60+
"SigAlg": sigalg,
61+
"Signature": signature
62+
}
63+
# RelayState is optional so only add when available,
64+
# signature validate fails if passed as None
65+
if relayState is not None:
66+
_saml_msg["RelayState"] = relayState
67+
4868
self.message = self.signature_check(xmldata, origdoc=origdoc,
4969
must=must,
5070
only_valid_cert=only_valid_cert)
71+
72+
if _do_redirect_sig_check:
73+
_issuer = self.message.issuer.text.strip()
74+
_certs = self.sec.metadata.certs(_issuer, "any", "signing")
75+
logger.debug("Certs: %s, _saml_msg: %s", _certs, _saml_msg)
76+
_verified_ok = False
77+
for cert in _certs:
78+
if verify_redirect_signature(_saml_msg, self.sec.sec_backend, cert):
79+
_verified_ok = True
80+
break
81+
82+
logger.info("Redirect request signature check: %s", _verified_ok)
83+
# Set self.message to None, it shall raise error further down.
84+
if not _verified_ok:
85+
self.message = None
86+
raise SignatureError('Failed to verify signature')
87+
5188
except TypeError:
5289
raise
5390
except Exception as excp:
91+
self.message = None
5492
logger.info("EXCEPTION: %s", excp)
5593

5694
if not self.message:
@@ -97,9 +135,10 @@ def _verify(self):
97135
return valid
98136

99137
def loads(self, xmldata, binding, origdoc=None, must=None,
100-
only_valid_cert=False):
138+
only_valid_cert=False, relay_state=None, sigalg=None, signature=None):
101139
return self._loads(xmldata, binding, origdoc, must,
102-
only_valid_cert=only_valid_cert)
140+
only_valid_cert=only_valid_cert, relayState=relay_state,
141+
sigalg=sigalg, signature=signature)
103142

104143
def verify(self):
105144
try:

src/saml2/server.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -226,17 +226,23 @@ def verify_assertion_consumer_service(self, request):
226226

227227
# -------------------------------------------------------------------------
228228

229-
def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT):
229+
def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT,
230+
relay_state=None, sigalg=None, signature=None):
230231
"""Parse a Authentication Request
231232
232233
:param enc_request: The request in its transport format
233234
:param binding: Which binding that was used to transport the message
235+
:param relay_state: RelayState, when binding=redirect
236+
:param sigalg: Signature Algorithm, when binding=redirect
237+
:param signature: Signature, when binding=redirect
234238
to this entity.
235239
:return: A request instance
236240
"""
237241

238242
return self._parse_request(enc_request, AuthnRequest,
239-
"single_sign_on_service", binding)
243+
"single_sign_on_service", binding,
244+
relay_state=relay_state, sigalg=sigalg,
245+
signature=signature)
240246

241247
def parse_attribute_query(self, xml_string, binding):
242248
""" Parse an attribute query

tests/test_51_client.py

+90-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from saml2.argtree import add_path
1212
from saml2.cert import OpenSSLWrapper
1313
from saml2.xmldsig import sig_default
14-
from saml2.xmldsig import SIG_RSA_SHA256
14+
from saml2.xmldsig import SIG_RSA_SHA256, SIG_RSA_SHA1
1515
from saml2 import BINDING_HTTP_POST
1616
from saml2 import BINDING_HTTP_REDIRECT
1717
from saml2 import config
@@ -29,7 +29,8 @@
2929
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
3030
from saml2.client import Saml2Client
3131
from saml2.pack import parse_soap_enveloped_saml
32-
from saml2.response import LogoutResponse, StatusInvalidNameidPolicy, StatusError
32+
from saml2.response import LogoutResponse, StatusInvalidNameidPolicy, StatusError, \
33+
IncorrectlySigned
3334
from saml2.saml import NAMEID_FORMAT_PERSISTENT, EncryptedAssertion, Advice
3435
from saml2.saml import NAMEID_FORMAT_TRANSIENT
3536
from saml2.saml import NameID
@@ -172,6 +173,9 @@ def setup_class(self):
172173
conf.load_file("server_conf")
173174
self.client = Saml2Client(conf)
174175

176+
def setup_method(self):
177+
self.server.config.setattr("idp", "want_authn_requests_signed", None)
178+
175179
def teardown_class(self):
176180
self.server.close()
177181

@@ -1524,6 +1528,90 @@ def test_signed_redirect(self):
15241528
qs["SAMLRequest"][0], BINDING_HTTP_REDIRECT
15251529
)
15261530

1531+
def test_signed_redirect_passes_if_needs_signed_requests(self):
1532+
# Revert configuration change to disallow unsinged responses
1533+
self.client.want_response_signed = True
1534+
self.server.config.setattr("idp", "want_authn_requests_signed", True)
1535+
1536+
reqid, req = self.client.create_authn_request(
1537+
"http://localhost:8088/sso", message_id="id1"
1538+
)
1539+
1540+
info = self.client.apply_binding(
1541+
BINDING_HTTP_REDIRECT,
1542+
str(req),
1543+
destination="",
1544+
relay_state="relay2",
1545+
sign=True,
1546+
sigalg=SIG_RSA_SHA256,
1547+
)
1548+
loc = info["headers"][0][1]
1549+
qs = list_values2simpletons(parse.parse_qs(loc[1:]))
1550+
1551+
res = self.server.parse_authn_request(
1552+
qs["SAMLRequest"],
1553+
BINDING_HTTP_REDIRECT,
1554+
relay_state=qs["RelayState"],
1555+
sigalg=qs["SigAlg"],
1556+
signature=qs["Signature"]
1557+
)
1558+
assert res.message.destination == "http://localhost:8088/sso"
1559+
assert res.message.id == "id1"
1560+
1561+
def test_signed_redirect_fail_if_needs_signed_request_but_received_unsigned(self):
1562+
# Revert configuration change to disallow unsinged responses
1563+
self.client.want_response_signed = True
1564+
self.server.config.setattr("idp", "want_authn_requests_signed", True)
1565+
1566+
reqid, req = self.client.create_authn_request(
1567+
"http://localhost:8088/sso", message_id="id1"
1568+
)
1569+
1570+
info = self.client.apply_binding(
1571+
BINDING_HTTP_REDIRECT,
1572+
str(req),
1573+
destination="",
1574+
relay_state="relay2",
1575+
sign=True,
1576+
sigalg=SIG_RSA_SHA256,
1577+
)
1578+
loc = info["headers"][0][1]
1579+
qs = list_values2simpletons(parse.parse_qs(loc[1:]))
1580+
1581+
with raises(IncorrectlySigned):
1582+
self.server.parse_authn_request(
1583+
qs["SAMLRequest"], BINDING_HTTP_REDIRECT
1584+
)
1585+
1586+
def test_signed_redirect_fail_if_needs_signed_request_but_sigalg_not_matches(self):
1587+
# Revert configuration change to disallow unsinged responses
1588+
self.client.want_response_signed = True
1589+
self.server.config.setattr("idp", "want_authn_requests_signed", True)
1590+
1591+
reqid, req = self.client.create_authn_request(
1592+
"http://localhost:8088/sso", message_id="id1"
1593+
)
1594+
1595+
info = self.client.apply_binding(
1596+
BINDING_HTTP_REDIRECT,
1597+
str(req),
1598+
destination="",
1599+
relay_state="relay2",
1600+
sign=True,
1601+
sigalg=SIG_RSA_SHA256,
1602+
)
1603+
loc = info["headers"][0][1]
1604+
qs = list_values2simpletons(parse.parse_qs(loc[1:]))
1605+
1606+
with raises(IncorrectlySigned):
1607+
self.server.parse_authn_request(
1608+
qs["SAMLRequest"],
1609+
BINDING_HTTP_REDIRECT,
1610+
relay_state=qs["RelayState"],
1611+
sigalg=SIG_RSA_SHA1,
1612+
signature=qs["Signature"]
1613+
)
1614+
15271615
def test_do_logout_signed_redirect(self):
15281616
conf = config.SPConfig()
15291617
conf.load_file("sp_slo_redirect_conf")

0 commit comments

Comments
 (0)