Skip to content

Commit 9c827a9

Browse files
committed
Implement SHA1 deprecation policy
1 parent 5df7bfa commit 9c827a9

File tree

4 files changed

+76
-33
lines changed

4 files changed

+76
-33
lines changed

signxml/algorithms.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class InvalidInputErrorMixin:
4949
def _missing_(cls, value):
5050
raise InvalidInput(f"Unrecognized {cls.__name__}: {value}")
5151

52+
def __repr__(self):
53+
return f"{self.__class__.__name__}.{self.name}"
54+
5255

5356
class DigestAlgorithm(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
5457
"""
@@ -160,7 +163,7 @@ class CanonicalizationMethod(InvalidInputErrorMixin, Enum):
160163
EXCLUSIVE_XML_CANONICALIZATION_1_0_WITH_COMMENTS = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
161164

162165
# The identifier for Canonical XML 2.0 is "http://www.w3.org/2010/xml-c14n2", but it is not a W3C standard.
163-
# While it is supported by lxml, it's not in general use and not supported by SignXML as a matter of policy
166+
# While it is supported by lxml, it's not in general use and not supported by SignXML
164167

165168

166169
digest_algorithm_implementations = {

signxml/signer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,17 @@ def __init__(
111111
self.digest_alg = DigestAlgorithm.from_fragment(digest_algorithm)
112112
else:
113113
self.digest_alg = DigestAlgorithm(digest_algorithm)
114+
self.check_deprecated_methods()
114115
self.c14n_alg = CanonicalizationMethod(c14n_algorithm)
115116
self.namespaces = dict(ds=namespaces.ds)
116117
self._parser = None
117118
self.signature_annotators = [self._add_key_info]
118119

120+
def check_deprecated_methods(self):
121+
if "SHA1" in self.sign_alg.name or "SHA1" in self.digest_alg.name:
122+
msg = "SHA1-based algorithms are not supported in the default configuration because they are not secure"
123+
raise InvalidInput(msg)
124+
119125
def sign(
120126
self,
121127
data,

signxml/verifier.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ class SignatureConfiguration:
6060
If set to a non-integer, any number of references is accepted (otherwise a mismatch raises an error).
6161
"""
6262

63-
signature_methods: FrozenSet[SignatureMethod] = frozenset()
63+
signature_methods: FrozenSet[SignatureMethod] = frozenset(sm for sm in SignatureMethod if "SHA1" not in sm.name)
6464
"""
6565
Set of acceptable signature methods (signature algorithms). Any signature generated using an algorithm not listed
6666
here will fail verification (but if this set is left empty, then all supported algorithms are accepted).
6767
"""
6868

69-
digest_algorithms: FrozenSet[DigestAlgorithm] = frozenset()
69+
digest_algorithms: FrozenSet[DigestAlgorithm] = frozenset(da for da in DigestAlgorithm if "SHA1" not in da.name)
7070
"""
7171
Set of acceptable digest algorithms. Any signature or reference transform generated using an algorithm not listed
7272
here will cause verification to fail (but if this set is left empty, then all supported algorithms are accepted).
@@ -384,6 +384,7 @@ def verify(
384384

385385
if signature_alg.name.startswith("ECDSA"):
386386
raw_signature = self._encode_dss_signature(raw_signature, signing_cert.get_pubkey().bits())
387+
387388
try:
388389
digest_alg_name = str(digest_algorithm_implementations[signature_alg].name)
389390
openssl_verify(signing_cert, raw_signature, signed_info_c14n, digest_alg_name)

test/test.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,25 @@
3535
)
3636
from signxml.xades import ( # noqa:E402
3737
XAdESDataObjectFormat,
38+
XAdESSignatureConfiguration,
3839
XAdESSignaturePolicy,
3940
XAdESSigner,
4041
XAdESVerifier,
4142
XAdESVerifyResult,
4243
)
4344

4445

46+
class XMLSignerWithSHA1(XMLSigner):
47+
def check_deprecated_methods(self):
48+
pass
49+
50+
51+
sha1_ok = SignatureConfiguration(signature_methods=list(SignatureMethod), digest_algorithms=list(DigestAlgorithm))
52+
xades_sha1_ok = XAdESSignatureConfiguration(
53+
signature_methods=list(SignatureMethod), digest_algorithms=list(DigestAlgorithm)
54+
)
55+
56+
4557
def reset_tree(t, method):
4658
if not isinstance(t, str):
4759
for s in t.findall(".//ds:Signature", namespaces=namespaces):
@@ -101,7 +113,7 @@ def test_basic_signxml_statements(self):
101113
self.assertEqual(SignatureMethod.RSA_SHA256.value, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
102114
self.assertEqual(SignatureConstructionMethod.enveloped, methods.enveloped)
103115
with self.assertRaisesRegex(InvalidInput, "Unknown signature construction method"):
104-
signer = XMLSigner(method=None)
116+
signer = XMLSignerWithSHA1(method=None)
105117

106118
with self.assertRaisesRegex(InvalidInput, "must be an XML element"):
107119
XMLSigner(signature_algorithm="hmac-sha256").sign("x", key=b"abc")
@@ -124,7 +136,7 @@ def test_basic_signxml_statements(self):
124136
continue
125137
print(digest_alg.name, sig_alg.name, c14n_alg, method, type(d))
126138
reset_tree(d, method)
127-
signer = XMLSigner(
139+
signer = XMLSignerWithSHA1(
128140
method=method,
129141
signature_algorithm=sig_alg,
130142
digest_algorithm=digest_alg,
@@ -136,7 +148,7 @@ def test_basic_signxml_statements(self):
136148
)
137149
# print(etree.tostring(signed))
138150
hmac_key = self.keys["hmac"] if sig_alg_type == "hmac" else None
139-
verify_kwargs = dict(require_x509=False, hmac_key=hmac_key, validate_schema=True)
151+
verify_kwargs = dict(require_x509=False, hmac_key=hmac_key, validate_schema=True, expect_config=sha1_ok)
140152

141153
if method == methods.detached:
142154

@@ -169,7 +181,12 @@ def resolver(uri):
169181
XMLVerifier().verify(signed_data, id_attribute="X", **verify_kwargs)
170182

171183
with self.assertRaisesRegex(InvalidInput, "Expected a X.509 certificate based signature"):
172-
XMLVerifier().verify(signed_data, hmac_key=hmac_key, uri_resolver=verify_kwargs.get("uri_resolver"))
184+
XMLVerifier().verify(
185+
signed_data,
186+
hmac_key=hmac_key,
187+
uri_resolver=verify_kwargs.get("uri_resolver"),
188+
expect_config=sha1_ok,
189+
)
173190

174191
if method != methods.detached:
175192
with self.assertRaisesRegex(InvalidSignature, "Digest mismatch"):
@@ -205,36 +222,34 @@ def test_x509_certs(self):
205222
tree = etree.parse(self.example_xml_files[0])
206223
ca_pem_file = os.path.join(os.path.dirname(__file__), "example-ca.pem").encode("utf-8")
207224
crt, key = self.load_example_keys()
208-
for hash_alg in "sha1", "sha256":
209-
for method in methods.enveloped, methods.enveloping:
210-
print(hash_alg, method)
211-
data = tree.getroot()
212-
reset_tree(data, method)
213-
signer = XMLSigner(method=method, signature_algorithm="rsa-" + hash_alg)
214-
signed = signer.sign(data, key=key, cert=crt)
215-
signed_data = etree.tostring(signed)
216-
XMLVerifier().verify(signed_data, ca_pem_file=ca_pem_file)
217-
XMLVerifier().verify(signed_data, x509_cert=crt)
218-
XMLVerifier().verify(signed_data, x509_cert=load_certificate(FILETYPE_PEM, crt))
219-
XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="*.example.com")
220-
221-
with self.assertRaises(OpenSSLCryptoError):
222-
XMLVerifier().verify(signed_data, x509_cert=crt[::-1])
223-
224-
with self.assertRaises(InvalidSignature):
225-
XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="test")
226-
227-
with self.assertRaisesRegex(InvalidCertificate, "unable to get local issuer certificate"):
228-
XMLVerifier().verify(signed_data)
229-
# TODO: negative: verify with wrong cert, wrong CA
225+
for method in methods.enveloped, methods.enveloping:
226+
data = tree.getroot()
227+
reset_tree(data, method)
228+
signer = XMLSigner(method=method, signature_algorithm=SignatureMethod.RSA_SHA256)
229+
signed = signer.sign(data, key=key, cert=crt)
230+
signed_data = etree.tostring(signed)
231+
XMLVerifier().verify(signed_data, ca_pem_file=ca_pem_file)
232+
XMLVerifier().verify(signed_data, x509_cert=crt)
233+
XMLVerifier().verify(signed_data, x509_cert=load_certificate(FILETYPE_PEM, crt))
234+
XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="*.example.com")
235+
236+
with self.assertRaises(OpenSSLCryptoError):
237+
XMLVerifier().verify(signed_data, x509_cert=crt[::-1])
238+
239+
with self.assertRaises(InvalidSignature):
240+
XMLVerifier().verify(signed_data, x509_cert=crt, cert_subject_name="test")
241+
242+
with self.assertRaisesRegex(InvalidCertificate, "unable to get local issuer certificate"):
243+
XMLVerifier().verify(signed_data)
244+
# TODO: negative: verify with wrong cert, wrong CA
230245

231246
def test_xmldsig_interop_examples(self):
232247
ca_pem_file = os.path.join(os.path.dirname(__file__), "interop", "cacert.pem").encode("utf-8")
233248
for signature_file in glob(os.path.join(os.path.dirname(__file__), "interop", "*.xml")):
234249
print("Verifying", signature_file)
235250
with open(signature_file, "rb") as fh:
236251
with self.assertRaisesRegex(InvalidCertificate, "certificate has expired"):
237-
XMLVerifier().verify(fh.read(), ca_pem_file=ca_pem_file)
252+
XMLVerifier().verify(fh.read(), ca_pem_file=ca_pem_file, expect_config=sha1_ok)
238253

239254
def test_xmldsig_interop_TR2012(self):
240255
def get_x509_cert(**kwargs):
@@ -256,6 +271,7 @@ def get_x509_cert(**kwargs):
256271
hmac_key="testkey",
257272
validate_schema=True,
258273
cert_resolver=get_x509_cert if "x509digest" in signature_file else None,
274+
expect_config=sha1_ok,
259275
)
260276
sig.decode("utf-8")
261277
except Exception as e:
@@ -325,6 +341,7 @@ def cert_resolver(x509_issuer_name, x509_serial_number, x509_digest):
325341
x509_cert=get_x509_cert(signature_file),
326342
cert_resolver=cert_resolver if "issuer-serial" in signature_file else None,
327343
ca_pem_file=get_ca_pem_file(signature_file),
344+
expect_config=sha1_ok,
328345
)
329346
decoded_sig = sig.decode("utf-8")
330347
if "HMACOutputLength" in decoded_sig or "bad" in signature_file or "expired" in signature_file:
@@ -543,13 +560,13 @@ def test_ws_security(self):
543560
with open(os.path.join(wsse_dir, "examples", "server_public.pem"), "rb") as fh:
544561
crt = fh.read()
545562
data = etree.parse(os.path.join(wsse_dir, "test", "unit", "client", "files", "valid wss resp.xml"))
546-
XMLVerifier().verify(data, x509_cert=crt, validate_schema=False, expect_references=2)
563+
XMLVerifier().verify(data, x509_cert=crt, validate_schema=False, expect_references=2, expect_config=sha1_ok)
547564

548565
data = etree.parse(
549566
os.path.join(wsse_dir, "test", "unit", "client", "files", "invalid wss resp - changed content.xml")
550567
)
551568
with self.assertRaisesRegex(InvalidDigest, "Digest mismatch for reference 0"):
552-
XMLVerifier().verify(data, x509_cert=crt, validate_schema=False, expect_references=2)
569+
XMLVerifier().verify(data, x509_cert=crt, validate_schema=False, expect_references=2, expect_config=sha1_ok)
553570

554571
def test_psha1(self):
555572
from signxml.util import p_sha1
@@ -620,6 +637,18 @@ def test_verify_config(self):
620637
with self.assertRaisesRegex(InvalidInput, "Digest algorithm SHA256 forbidden by configuration"):
621638
verifier.verify(signed, x509_cert=cert, expect_config=config)
622639

640+
def test_sha1_policy(self):
641+
data = etree.parse(self.example_xml_files[0]).getroot()
642+
cert, key = self.load_example_keys()
643+
with self.assertRaisesRegex(InvalidInput, "SHA1-based algorithms are not supported"):
644+
XMLSigner(signature_algorithm=SignatureMethod.RSA_SHA1)
645+
signer = XMLSignerWithSHA1(signature_algorithm=SignatureMethod.RSA_SHA1, digest_algorithm=DigestAlgorithm.SHA1)
646+
signed = signer.sign(data, cert=cert, key=key)
647+
verifier = XMLVerifier()
648+
with self.assertRaisesRegex(InvalidInput, "Signature method RSA_SHA1 forbidden by configuration"):
649+
verifier.verify(signed, x509_cert=cert)
650+
verifier.verify(signed, x509_cert=cert, expect_config=sha1_ok)
651+
623652

624653
class TestXAdES(unittest.TestCase, LoadExampleKeys):
625654
expect_references = {
@@ -681,7 +710,11 @@ def test_xades_interop_examples(self):
681710
with open(sig_file, "rb") as fh:
682711
doc = etree.parse(fh)
683712
cert = doc.find("//{http://www.w3.org/2000/09/xmldsig#}X509Certificate").text
684-
kwargs = dict(x509_cert=cert, expect_references=self.expect_references.get(os.path.basename(sig_file), 2))
713+
kwargs = dict(
714+
x509_cert=cert,
715+
expect_references=self.expect_references.get(os.path.basename(sig_file), 2),
716+
expect_config=xades_sha1_ok,
717+
)
685718
if "nonconformant" in sig_file:
686719
kwargs.update(validate_schema=False)
687720
if "sigPolStore" in sig_file:

0 commit comments

Comments
 (0)