Skip to content

Commit 1e59eaa

Browse files
committed
Improve signature checks
- Enforce allowed canonicalization methods - Enforce allowed transform aglorithms - Ensure the Object element is absent Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent 4d2dcce commit 1e59eaa

File tree

3 files changed

+85
-34
lines changed

3 files changed

+85
-34
lines changed

Diff for: src/saml2/sigver.py

+50-5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
from saml2.s_utils import Unsupported
4343
from saml2.time_util import instant
4444
from saml2.time_util import str_to_time
45+
from saml2.xmldsig import ALLOWED_CANONICALIZATIONS
46+
from saml2.xmldsig import ALLOWED_TRANSFORMS
47+
from saml2.xmldsig import TRANSFORM_C14N
48+
from saml2.xmldsig import TRANSFORM_ENVELOPED
4549
from saml2.xmldsig import SIG_RSA_SHA1
4650
from saml2.xmldsig import SIG_RSA_SHA224
4751
from saml2.xmldsig import SIG_RSA_SHA256
@@ -1503,7 +1507,8 @@ def _check_signature(self, decoded_xml, item, node_name=NODE_NAME, origdoc=None,
15031507
# * the Reference element must have a URI attribute
15041508
# * the URI attribute contains an anchor
15051509
# * the anchor points to the enclosing element's ID attribute
1506-
references = item.signature.signed_info.reference
1510+
signed_info = item.signature.signed_info
1511+
references = signed_info.reference
15071512
signatures_must_have_a_single_reference_element = len(references) == 1
15081513
the_Reference_element_must_have_a_URI_attribute = (
15091514
signatures_must_have_a_single_reference_element
@@ -1518,6 +1523,41 @@ def _check_signature(self, decoded_xml, item, node_name=NODE_NAME, origdoc=None,
15181523
the_URI_attribute_contains_an_anchor
15191524
and references[0].uri == "#{id}".format(id=item.id)
15201525
)
1526+
1527+
# SAML implementations SHOULD use Exclusive Canonicalization,
1528+
# with or without comments
1529+
canonicalization_method_is_c14n = (
1530+
signed_info.canonicalization_method.algorithm in ALLOWED_CANONICALIZATIONS
1531+
)
1532+
1533+
# Signatures in SAML messages SHOULD NOT contain transforms other than the
1534+
# - enveloped signature transform
1535+
# (with the identifier http://www.w3.org/2000/09/xmldsig#enveloped-signature)
1536+
# - or the exclusive canonicalization transforms
1537+
# (with the identifier http://www.w3.org/2001/10/xml-exc-c14n#
1538+
# or http://www.w3.org/2001/10/xml-exc-c14n#WithComments).
1539+
transform_alogs = [
1540+
transform.algorithm
1541+
for transform in references[0].transforms.transform
1542+
]
1543+
transform_alogs_n = len(transform_alogs)
1544+
only_up_to_two_transforms_are_defined = (
1545+
signatures_must_have_a_single_reference_element
1546+
and 1 <= transform_alogs_n <= 2
1547+
)
1548+
all_transform_algs_are_allowed = (
1549+
only_up_to_two_transforms_are_defined
1550+
and transform_alogs_n == len(
1551+
ALLOWED_TRANSFORMS.intersection(transform_alogs)
1552+
)
1553+
)
1554+
1555+
# The <ds:Object> element is not defined for use with SAML signatures,
1556+
# and SHOULD NOT be present.
1557+
# Since it can be used in service of an attacker by carrying unsigned data,
1558+
# verifiers SHOULD reject signatures that contain a <ds:Object> element.
1559+
object_element_is_not_present = not item.signature.object
1560+
15211561
validators = {
15221562
"signatures must have a single reference element": (
15231563
signatures_must_have_a_single_reference_element
@@ -1531,6 +1571,12 @@ def _check_signature(self, decoded_xml, item, node_name=NODE_NAME, origdoc=None,
15311571
"the anchor points to the enclosing element ID attribute": (
15321572
the_anchor_points_to_the_enclosing_element_ID_attribute
15331573
),
1574+
"canonicalization method is c14n": canonicalization_method_is_c14n,
1575+
"only up to two transforms are defined": (
1576+
only_up_to_two_transforms_are_defined
1577+
),
1578+
"all transform algs are allowed": all_transform_algs_are_allowed,
1579+
"object element is not present": object_element_is_not_present,
15341580
}
15351581
if not all(validators.values()):
15361582
error_context = {
@@ -1818,10 +1864,9 @@ def pre_signature_part(
18181864
sign_alg = ds.DefaultSignature().get_sign_alg()
18191865

18201866
signature_method = ds.SignatureMethod(algorithm=sign_alg)
1821-
canonicalization_method = ds.CanonicalizationMethod(
1822-
algorithm=ds.ALG_EXC_C14N)
1823-
trans0 = ds.Transform(algorithm=ds.TRANSFORM_ENVELOPED)
1824-
trans1 = ds.Transform(algorithm=ds.ALG_EXC_C14N)
1867+
canonicalization_method = ds.CanonicalizationMethod(algorithm=TRANSFORM_C14N)
1868+
trans0 = ds.Transform(algorithm=TRANSFORM_ENVELOPED)
1869+
trans1 = ds.Transform(algorithm=TRANSFORM_C14N)
18251870
transforms = ds.Transforms(transform=[trans0, trans1])
18261871
digest_method = ds.DigestMethod(algorithm=digest_alg)
18271872

Diff for: src/saml2/xmldsig/__init__.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,21 @@
5353

5454
MAC_SHA1 = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1'
5555

56-
C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
57-
C14N_WITH_C = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'
58-
ALG_EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'
59-
6056
TRANSFORM_XSLT = 'http://www.w3.org/TR/1999/REC-xslt-19991116'
6157
TRANSFORM_XPATH = 'http://www.w3.org/TR/1999/REC-xpath-19991116'
6258
TRANSFORM_ENVELOPED = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'
59+
TRANSFORM_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'
60+
TRANSFORM_C14N_WITH_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments'
6361

62+
ALLOWED_CANONICALIZATIONS = {
63+
TRANSFORM_C14N,
64+
TRANSFORM_C14N_WITH_COMMENTS,
65+
}
66+
ALLOWED_TRANSFORMS = {
67+
TRANSFORM_ENVELOPED,
68+
TRANSFORM_C14N,
69+
TRANSFORM_C14N_WITH_COMMENTS,
70+
}
6471

6572
class DefaultSignature(object):
6673
class _DefaultSignature(object):

Diff for: tests/test_00_xmldsig.py

+24-25
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,10 @@ def testUsingTestData(self):
3333
new_object = ds.object_from_string(ds_data.TEST_OBJECT)
3434
assert new_object.id == "object_id"
3535
assert new_object.encoding == ds.ENCODING_BASE64
36-
assert new_object.text.strip() == \
37-
"V2VkIEp1biAgNCAxMjoxMTowMyBFRFQgMjAwMwo"
38-
36+
assert new_object.text.strip() == "V2VkIEp1biAgNCAxMjoxMTowMyBFRFQgMjAwMwo"
3937

40-
class TestMgmtData:
4138

39+
class TestMgmtData:
4240
def setup_class(self):
4341
self.mgmt_data = ds.MgmtData()
4442

@@ -156,7 +154,7 @@ def testAccessors(self):
156154
self.x509_data.x509_certificate = ds.X509Certificate(
157155
text="x509 certificate")
158156
self.x509_data.x509_crl = ds.X509CRL(text="x509 crl")
159-
157+
160158
new_x509_data = ds.x509_data_from_string(self.x509_data.to_string())
161159
print(new_x509_data.keyswv())
162160
print(new_x509_data.__dict__.keys())
@@ -231,7 +229,7 @@ def testAccessors(self):
231229
ds.TRANSFORM_ENVELOPED
232230
assert new_transforms.transform[0].x_path[0].text.strip() == "xpath"
233231
assert new_transforms.transform[1].x_path[0].text.strip() == "xpath"
234-
232+
235233
def testUsingTestData(self):
236234
"""Test for transform_from_string() using test data"""
237235
new_transforms = ds.transforms_from_string(ds_data.TEST_TRANSFORMS)
@@ -261,7 +259,7 @@ def testAccessors(self):
261259
assert new_retrieval_method.uri == "http://www.example.com/URI"
262260
assert new_retrieval_method.type == "http://www.example.com/Type"
263261
assert isinstance(new_retrieval_method.transforms, ds.Transforms)
264-
262+
265263
def testUsingTestData(self):
266264
"""Test for retrieval_method_from_string() using test data"""
267265
new_retrieval_method = ds.retrieval_method_from_string(
@@ -285,7 +283,7 @@ def testAccessors(self):
285283
assert isinstance(new_rsa_key_value.exponent, ds.Exponent)
286284
assert new_rsa_key_value.modulus.text.strip() == "modulus"
287285
assert new_rsa_key_value.exponent.text.strip() == "exponent"
288-
286+
289287
def testUsingTestData(self):
290288
"""Test for rsa_key_value_from_string() using test data"""
291289
new_rsa_key_value = ds.rsa_key_value_from_string(
@@ -325,7 +323,7 @@ def testAccessors(self):
325323
assert new_dsa_key_value.j.text.strip() == "j"
326324
assert new_dsa_key_value.seed.text.strip() == "seed"
327325
assert new_dsa_key_value.pgen_counter.text.strip() == "pgen counter"
328-
326+
329327
def testUsingTestData(self):
330328
"""Test for dsa_key_value_from_string() using test data"""
331329
new_dsa_key_value = ds.dsa_key_value_from_string(
@@ -362,7 +360,7 @@ def testAccessors(self):
362360
ds_data.TEST_RSA_KEY_VALUE)
363361
new_key_value = ds.key_value_from_string(self.key_value.to_string())
364362
assert isinstance(new_key_value.rsa_key_value, ds.RSAKeyValue)
365-
363+
366364
def testUsingTestData(self):
367365
"""Test for key_value_from_string() using test data"""
368366
new_key_value = ds.key_value_from_string(ds_data.TEST_KEY_VALUE1)
@@ -384,7 +382,7 @@ def testAccessors(self):
384382
self.key_name.text = "key name"
385383
new_key_name = ds.key_name_from_string(self.key_name.to_string())
386384
assert new_key_name.text.strip() == "key name"
387-
385+
388386
def testUsingTestData(self):
389387
"""Test for key_name_from_string() using test data"""
390388
new_key_name = ds.key_name_from_string(ds_data.TEST_KEY_NAME)
@@ -423,7 +421,7 @@ def testAccessors(self):
423421
assert isinstance(new_key_info.spki_data[0], ds.SPKIData)
424422
assert isinstance(new_key_info.mgmt_data[0], ds.MgmtData)
425423
assert new_key_info.id == "id"
426-
424+
427425
def testUsingTestData(self):
428426
"""Test for key_info_from_string() using test data"""
429427
new_key_info = ds.key_info_from_string(ds_data.TEST_KEY_INFO)
@@ -436,7 +434,7 @@ def testUsingTestData(self):
436434
assert isinstance(new_key_info.spki_data[0], ds.SPKIData)
437435
assert isinstance(new_key_info.mgmt_data[0], ds.MgmtData)
438436
assert new_key_info.id == "id"
439-
437+
440438

441439
class TestDigestValue:
442440

@@ -448,7 +446,7 @@ def testAccessors(self):
448446
self.digest_value.text = "digest value"
449447
new_digest_value = ds.digest_value_from_string(self.digest_value.to_string())
450448
assert new_digest_value.text.strip() == "digest value"
451-
449+
452450
def testUsingTestData(self):
453451
"""Test for digest_value_from_string() using test data"""
454452
new_digest_value = ds.digest_value_from_string(ds_data.TEST_DIGEST_VALUE)
@@ -466,7 +464,7 @@ def testAccessors(self):
466464
new_digest_method = ds.digest_method_from_string(
467465
self.digest_method.to_string())
468466
assert new_digest_method.algorithm == ds.DIGEST_SHA1
469-
467+
470468
def testUsingTestData(self):
471469
"""Test for digest_method_from_string() using test data"""
472470
new_digest_method = ds.digest_method_from_string(
@@ -497,7 +495,7 @@ def testAccessors(self):
497495
assert new_reference.id == "id"
498496
assert new_reference.uri == "http://www.example.com/URI"
499497
assert new_reference.type == "http://www.example.com/Type"
500-
498+
501499
def testUsingTestData(self):
502500
"""Test for reference_from_string() using test data"""
503501
new_reference = ds.reference_from_string(ds_data.TEST_REFERENCE)
@@ -524,7 +522,7 @@ def testAccessors(self):
524522
ds.HMACOutputLength)
525523
assert new_signature_method.hmac_output_length.text.strip() == "8"
526524
assert new_signature_method.algorithm == ds.SIG_RSA_SHA1
527-
525+
528526
def testUsingTestData(self):
529527
"""Test for signature_method_from_string() using test data"""
530528
new_signature_method = ds.signature_method_from_string(
@@ -542,16 +540,17 @@ def setup_class(self):
542540

543541
def testAccessors(self):
544542
"""Test for CanonicalizationMethod accessors"""
545-
self.canonicalization_method.algorithm = ds.C14N_WITH_C
543+
self.canonicalization_method.algorithm = ds.TRANSFORM_C14N_WITH_COMMENTS
546544
new_canonicalization_method = ds.canonicalization_method_from_string(
547545
self.canonicalization_method.to_string())
548-
assert new_canonicalization_method.algorithm == ds.C14N_WITH_C
549-
546+
assert new_canonicalization_method.algorithm == ds.TRANSFORM_C14N_WITH_COMMENTS
547+
550548
def testUsingTestData(self):
551549
"""Test for canonicalization_method_from_string() using test data"""
552550
new_canonicalization_method = ds.canonicalization_method_from_string(
553-
ds_data.TEST_CANONICALIZATION_METHOD)
554-
assert new_canonicalization_method.algorithm == ds.C14N_WITH_C
551+
ds_data.TEST_CANONICALIZATION_METHOD
552+
)
553+
assert new_canonicalization_method.algorithm == "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
555554

556555

557556
class TestSignedInfo:
@@ -574,7 +573,7 @@ def testAccessors(self):
574573
ds.CanonicalizationMethod)
575574
assert isinstance(new_si.signature_method, ds.SignatureMethod)
576575
assert isinstance(new_si.reference[0], ds.Reference)
577-
576+
578577
def testUsingTestData(self):
579578
"""Test for signed_info_from_string() using test data"""
580579
new_si = ds.signed_info_from_string(ds_data.TEST_SIGNED_INFO)
@@ -597,7 +596,7 @@ def testAccessors(self):
597596
self.signature_value.to_string())
598597
assert new_signature_value.id == "id"
599598
assert new_signature_value.text.strip() == "signature value"
600-
599+
601600
def testUsingTestData(self):
602601
"""Test for signature_value_from_string() using test data"""
603602
new_signature_value = ds.signature_value_from_string(
@@ -627,7 +626,7 @@ def testAccessors(self):
627626
assert isinstance(new_signature.signature_value, ds.SignatureValue)
628627
assert isinstance(new_signature.key_info, ds.KeyInfo)
629628
assert isinstance(new_signature.object[0], ds.Object)
630-
629+
631630
def testUsingTestData(self):
632631
"""Test for signature_value_from_string() using test data"""
633632
new_signature = ds.signature_from_string(ds_data.TEST_SIGNATURE)

0 commit comments

Comments
 (0)