Skip to content

Commit 81553e5

Browse files
committed
Use enums for signature and digest algorithms
1 parent 449e753 commit 81553e5

File tree

7 files changed

+767
-179
lines changed

7 files changed

+767
-179
lines changed

signxml/__init__.py

Lines changed: 75 additions & 140 deletions
Large diffs are not rendered by default.

signxml/util/__init__.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
import textwrap
1212
from base64 import b64decode, b64encode
1313
from dataclasses import dataclass
14+
from enum import Enum, auto
1415
from typing import Any, List, Optional
1516
from xml.etree import ElementTree as stdlibElementTree
1617

18+
from cryptography.hazmat.primitives import hashes
1719
from lxml import etree
1820

1921
from ..exceptions import InvalidCertificate, InvalidInput, RedundantCert, SignXMLException
@@ -22,6 +24,108 @@
2224
PEM_FOOTER = "-----END CERTIFICATE-----"
2325

2426

27+
class XMLSignatureMethods(Enum):
28+
enveloped = auto()
29+
enveloping = auto()
30+
detached = auto()
31+
32+
33+
class FragmentLookupMixin:
34+
@classmethod
35+
def from_fragment(cls, fragment):
36+
for i in cls: # type: ignore
37+
if i.value.endswith("#" + fragment):
38+
return i
39+
else:
40+
raise InvalidInput(f"Unrecognized {cls.__name__} identifier fragment: {fragment}")
41+
42+
43+
class InvalidInputErrorMixin:
44+
@classmethod
45+
def _missing_(cls, value):
46+
raise InvalidInput(f"Unrecognized {cls.__name__}: {value}")
47+
48+
49+
class XMLSecurityDigestAlgorithm(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
50+
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
51+
SHA224 = "http://www.w3.org/2001/04/xmldsig-more#sha224"
52+
SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
53+
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
54+
SHA512 = "http://www.w3.org/2001/04/xmlenc#sha512"
55+
SHA3_224 = "http://www.w3.org/2007/05/xmldsig-more#sha3-224"
56+
SHA3_256 = "http://www.w3.org/2007/05/xmldsig-more#sha3-256"
57+
SHA3_384 = "http://www.w3.org/2007/05/xmldsig-more#sha3-384"
58+
SHA3_512 = "http://www.w3.org/2007/05/xmldsig-more#sha3-512"
59+
60+
@property
61+
def implementation(self):
62+
return digest_algorithm_implementations[self]
63+
64+
65+
# TODO: check if padding errors are fixed by using padding=MGF1
66+
class XMLSecuritySignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
67+
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
68+
HMAC_SHA1 = "http://www.w3.org/2000/09/xmldsig#hmac-sha1"
69+
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
70+
ECDSA_SHA1 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"
71+
ECDSA_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224"
72+
ECDSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"
73+
ECDSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"
74+
ECDSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"
75+
HMAC_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha224"
76+
HMAC_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"
77+
HMAC_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384"
78+
HMAC_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512"
79+
RSA_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224"
80+
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
81+
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
82+
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
83+
RSA_PSS = "http://www.w3.org/2007/05/xmldsig-more#rsa-pss"
84+
DSA_SHA256 = "http://www.w3.org/2009/xmldsig11#dsa-sha256"
85+
ECDSA_SHA3_224 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-224"
86+
ECDSA_SHA3_256 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-256"
87+
ECDSA_SHA3_384 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-384"
88+
ECDSA_SHA3_512 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-512"
89+
EDDSA_ED25519 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519"
90+
EDDSA_ED448 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"
91+
92+
93+
digest_algorithm_implementations = {
94+
XMLSecurityDigestAlgorithm.SHA1: hashes.SHA1,
95+
XMLSecurityDigestAlgorithm.SHA224: hashes.SHA224,
96+
XMLSecurityDigestAlgorithm.SHA384: hashes.SHA384,
97+
XMLSecurityDigestAlgorithm.SHA256: hashes.SHA256,
98+
XMLSecurityDigestAlgorithm.SHA512: hashes.SHA512,
99+
XMLSecurityDigestAlgorithm.SHA3_224: hashes.SHA3_224,
100+
XMLSecurityDigestAlgorithm.SHA3_256: hashes.SHA3_256,
101+
XMLSecurityDigestAlgorithm.SHA3_384: hashes.SHA3_384,
102+
XMLSecurityDigestAlgorithm.SHA3_512: hashes.SHA3_512,
103+
XMLSecuritySignatureMethod.DSA_SHA1: hashes.SHA1,
104+
XMLSecuritySignatureMethod.HMAC_SHA1: hashes.SHA1,
105+
XMLSecuritySignatureMethod.RSA_SHA1: hashes.SHA1,
106+
XMLSecuritySignatureMethod.ECDSA_SHA1: hashes.SHA1,
107+
XMLSecuritySignatureMethod.ECDSA_SHA224: hashes.SHA224,
108+
XMLSecuritySignatureMethod.ECDSA_SHA256: hashes.SHA256,
109+
XMLSecuritySignatureMethod.ECDSA_SHA384: hashes.SHA384,
110+
XMLSecuritySignatureMethod.ECDSA_SHA512: hashes.SHA512,
111+
XMLSecuritySignatureMethod.HMAC_SHA224: hashes.SHA224,
112+
XMLSecuritySignatureMethod.HMAC_SHA256: hashes.SHA256,
113+
XMLSecuritySignatureMethod.HMAC_SHA384: hashes.SHA384,
114+
XMLSecuritySignatureMethod.HMAC_SHA512: hashes.SHA512,
115+
XMLSecuritySignatureMethod.RSA_SHA224: hashes.SHA224,
116+
XMLSecuritySignatureMethod.RSA_SHA256: hashes.SHA256,
117+
XMLSecuritySignatureMethod.RSA_SHA384: hashes.SHA384,
118+
XMLSecuritySignatureMethod.RSA_SHA512: hashes.SHA512,
119+
XMLSecuritySignatureMethod.DSA_SHA256: hashes.SHA256,
120+
XMLSecuritySignatureMethod.ECDSA_SHA3_224: hashes.SHA1,
121+
XMLSecuritySignatureMethod.ECDSA_SHA3_256: hashes.SHA1,
122+
XMLSecuritySignatureMethod.ECDSA_SHA3_384: hashes.SHA1,
123+
XMLSecuritySignatureMethod.ECDSA_SHA3_512: hashes.SHA1,
124+
XMLSecuritySignatureMethod.EDDSA_ED25519: hashes.SHA512,
125+
XMLSecuritySignatureMethod.EDDSA_ED448: hashes.SHAKE256,
126+
}
127+
128+
25129
class Namespace(dict):
26130
def __getattr__(self, a):
27131
return dict.__getitem__(self, a)

signxml/xades/__init__.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@
5151

5252
from .. import VerifyResult, XMLSignatureProcessor, XMLSigner, XMLVerifier
5353
from ..exceptions import InvalidDigest, InvalidInput
54-
from ..util import SigningSettings, add_pem_header, ds_tag, namespaces, xades_tag
54+
from ..util import SigningSettings
55+
from ..util import XMLSecurityDigestAlgorithm as digest_algorithms
56+
from ..util import add_pem_header, ds_tag, namespaces, xades_tag
5557

5658
# TODO: make this a dataclass
5759
default_data_object_format = {"Description": "Default XAdES payload description", "MimeType": "text/xml"}
@@ -69,7 +71,7 @@ class XAdESProcessor(XMLSignatureProcessor):
6971

7072
class XAdESSigner(XAdESProcessor, XMLSigner):
7173
"""
72-
- assert signature algorithm is not sha1
74+
TODO: docs and signature forwarding for autodocs
7375
"""
7476

7577
def __init__(
@@ -80,7 +82,7 @@ def __init__(
8082
**kwargs,
8183
) -> None:
8284
super().__init__(**kwargs)
83-
if self.sign_alg.startswith("hmac-"):
85+
if self.sign_alg.name.startswith("HMAC_"):
8486
raise Exception("HMAC signatures are not supported by XAdES")
8587
self.signature_annotators.append(self._build_xades_ds_object)
8688
self._tokens_used: Dict[str, bool] = {}
@@ -145,11 +147,10 @@ def _add_reference_to_signed_info(self, sig_root, node_to_reference):
145147
signed_info = self._find(sig_root, "SignedInfo")
146148
reference = SubElement(signed_info, ds_tag("Reference"), nsmap=self.namespaces)
147149
reference.set("URI", f"#{node_to_reference.get('Id')}")
148-
digest_alg = self.known_digest_tags[self.digest_alg]
149-
SubElement(reference, ds_tag("DigestMethod"), nsmap=self.namespaces, Algorithm=digest_alg)
150+
SubElement(reference, ds_tag("DigestMethod"), nsmap=self.namespaces, Algorithm=self.digest_alg.value)
150151
digest_value_node = SubElement(reference, ds_tag("DigestValue"), nsmap=self.namespaces)
151152
node_to_reference_c14n = self._c14n(node_to_reference, algorithm=self.c14n_alg)
152-
digest = self._get_digest(node_to_reference_c14n, self._get_digest_method_by_tag(self.digest_alg))
153+
digest = self._get_digest(node_to_reference_c14n, algorithm=self.digest_alg)
153154
digest_value_node.text = b64encode(digest).decode()
154155

155156
def add_signing_time(self, signed_signature_properties, sig_root, signing_settings: SigningSettings):
@@ -169,11 +170,10 @@ def add_signing_certificate(self, signed_signature_properties, sig_root, signing
169170
else:
170171
loaded_cert = load_certificate(FILETYPE_PEM, add_pem_header(cert))
171172
der_encoded_cert = dump_certificate(FILETYPE_ASN1, loaded_cert)
172-
digest_alg = self.known_digest_tags[self.digest_alg]
173-
cert_digest_bytes = self._get_digest(der_encoded_cert, self._get_digest_method(digest_alg))
173+
cert_digest_bytes = self._get_digest(der_encoded_cert, algorithm=self.digest_alg)
174174
cert_node = SubElement(signing_cert_v2, xades_tag("Cert"), nsmap=self.namespaces)
175175
cert_digest = SubElement(cert_node, xades_tag("CertDigest"), nsmap=self.namespaces)
176-
SubElement(cert_digest, ds_tag("DigestMethod"), nsmap=self.namespaces, Algorithm=digest_alg)
176+
SubElement(cert_digest, ds_tag("DigestMethod"), nsmap=self.namespaces, Algorithm=self.digest_alg.value)
177177
digest_value_node = SubElement(cert_digest, ds_tag("DigestValue"), nsmap=self.namespaces)
178178
digest_value_node.text = b64encode(cert_digest_bytes).decode()
179179

@@ -196,10 +196,10 @@ def add_signature_policy_identifier(self, signed_signature_properties, sig_root,
196196
description = SubElement(sig_policy_id, xades_tag("Description"), nsmap=self.namespaces)
197197
description.text = self.signature_policy["Description"]
198198
sig_policy_hash = SubElement(signature_policy_id, xades_tag("SigPolicyHash"), nsmap=self.namespaces)
199-
digest_alg = self.known_digest_tags[self.signature_policy["DigestMethod"]]
200-
SubElement(sig_policy_hash, ds_tag("DigestMethod"), nsmap=self.namespaces, Algorithm=digest_alg)
199+
digest_alg = digest_algorithms(self.signature_policy["DigestMethod"])
200+
SubElement(sig_policy_hash, ds_tag("DigestMethod"), nsmap=self.namespaces, Algorithm=digest_alg.value)
201201
digest_value_node = SubElement(sig_policy_hash, ds_tag("DigestValue"), nsmap=self.namespaces)
202-
digest_value_node.text = b64encode(self.signature_policy["DigestValue"]).decode()
202+
digest_value_node.text = self.signature_policy["DigestValue"]
203203

204204
def add_signature_production_place(self, signed_signature_properties, sig_root, signing_settings: SigningSettings):
205205
# SignatureProductionPlace or SignatureProductionPlaceV2
@@ -234,19 +234,24 @@ def add_data_object_format(self, signed_data_object_properties, sig_root, signin
234234

235235
class XAdESVerifier(XAdESProcessor, XMLVerifier):
236236
"""
237-
- implement registry of assertion callbacks
238-
- assert signature algorithm is not hmac
237+
FIXME: add docs
239238
"""
240239

240+
# TODO: document/support SignatureTimeStamp / timestamp attestation
241+
# SignatureTimeStamp is required by certain profiles but is an unsigned property
242+
243+
def _verify_signing_time(self, verify_result: VerifyResult):
244+
pass
245+
241246
def _verify_cert_digest(self, signing_cert_node, expect_cert):
242247
for cert in self._findall(signing_cert_node, "xades:Cert"):
243248
cert_digest = self._find(cert, "xades:CertDigest")
244-
digest_alg = self._find(cert_digest, "DigestMethod").get("Algorithm")
249+
digest_alg = digest_algorithms(self._find(cert_digest, "DigestMethod").get("Algorithm"))
245250
digest_value = self._find(cert_digest, "DigestValue")
246251
# check spec for specific method of retrieving cert
247252
der_encoded_cert = dump_certificate(FILETYPE_ASN1, expect_cert)
248253

249-
if b64decode(digest_value.text) != self._get_digest(der_encoded_cert, self._get_digest_method(digest_alg)):
254+
if b64decode(digest_value.text) != self._get_digest(der_encoded_cert, algorithm=digest_alg):
250255
raise InvalidDigest("Digest mismatch for certificate digest")
251256

252257
def _verify_cert_digests(self, verify_result: VerifyResult):
@@ -272,23 +277,35 @@ def _verify_signature_policy(self, verify_result: VerifyResult):
272277
"xades:SignaturePolicyIdentifier/xades:SignaturePolicyId", namespaces=namespaces
273278
)
274279
if signature_policy_id is not None:
280+
# FIXME: assert on all elements of self.expect_signature_policy
281+
# FIXME: make signature policy into a dataclass
275282
sig_policy_id = self._find(signature_policy_id, "xades:SigPolicyId")
276283
identifier = self._find(sig_policy_id, "xades:Identifier")
284+
if identifier.text != self.expect_signature_policy["Identifier"]:
285+
raise InvalidInput(
286+
f"Expected to find signature policy identifier {self.expect_signature_policy['Identifier']}, "
287+
f"but found {identifier.text}"
288+
)
277289
sig_policy_hash = self._find(signature_policy_id, "xades:SigPolicyHash")
278-
digest_alg = self._find(sig_policy_hash, "DigestMethod").get("Algorithm")
290+
digest_alg = digest_algorithms(self._find(sig_policy_hash, "DigestMethod").get("Algorithm"))
291+
if digest_alg != self.expect_signature_policy["DigestMethod"]:
292+
raise InvalidInput(
293+
f"Expected to find signature digest algorithm {self.expect_signature_policy['DigestMethod']}, "
294+
f"but found {digest_alg}"
295+
)
279296
digest_value = self._find(sig_policy_hash, "DigestValue")
280-
if b64decode(digest_value.text) != self._get_digest(
281-
identifier.text.encode(), self._get_digest_method(digest_alg)
282-
):
283-
pass # FIXME
284-
# raise InvalidDigest("Digest mismatch for signature policy hash")
297+
if b64decode(digest_value.text) != b64decode(self.expect_signature_policy["DigestValue"]):
298+
raise InvalidInput("Digest mismatch for signature policy hash")
285299

286300
def _verify_signed_properties(self, verify_result):
301+
self._verify_signing_time(verify_result)
287302
self._verify_cert_digests(verify_result)
288-
self._verify_signature_policy(verify_result)
303+
if self.expect_signature_policy:
304+
self._verify_signature_policy(verify_result)
289305
return self._find(verify_result.signed_xml, "xades:SignedSignatureProperties")
290306

291-
def verify(self, data, expect_references=3, **kwargs):
307+
def verify(self, data, expect_signature_policy=None, expect_references=3, **kwargs):
308+
self.expect_signature_policy = expect_signature_policy
292309
verify_results = super().verify(data, expect_references=expect_references, **kwargs)
293310
for i, verify_result in enumerate(verify_results):
294311
if verify_result.signed_xml is None:
@@ -302,5 +319,4 @@ def verify(self, data, expect_references=3, **kwargs):
302319
raise InvalidInput("Expected to find a xades:SignedProperties element")
303320

304321
# TODO: assert all mandatory signed properties are set
305-
# TODO: add signed properties to verify_result
306322
return verify_results

test/test.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
XMLSignatureProcessor,
2525
XMLSigner,
2626
XMLVerifier,
27+
digest_algorithms,
2728
methods,
2829
namespaces,
2930
)
@@ -139,17 +140,17 @@ def resolver(uri):
139140
XMLVerifier().verify(signed_data, **verify_kwargs)
140141
XMLVerifier().verify(signed_data, parser=parser, **verify_kwargs)
141142
res = XMLVerifier().verify(signed_data, id_attribute="Id", **verify_kwargs)
142-
self.assertIsInstance(res, VerifyResult)
143+
self.assertIsInstance(res[0], VerifyResult)
143144
for attr in "signed_data", "signed_xml", "signature_xml":
144-
self.assertTrue(hasattr(res, attr))
145+
self.assertTrue(hasattr(res[0], attr))
145146

146-
if res.signed_xml is not None:
147+
if res[0].signed_xml is not None:
147148
# Ensure the signature is not part of the signed data
148-
self.assertIsNone(res.signed_xml.find(".//{http://www.w3.org/2000/09/xmldsig#}Signature"))
149-
self.assertNotEqual(res.signed_xml.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature")
149+
self.assertIsNone(res[0].signed_xml.find(".//{http://www.w3.org/2000/09/xmldsig#}Signature"))
150+
self.assertNotEqual(res[0].signed_xml.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature")
150151

151152
# Ensure the signature was returned
152-
self.assertEqual(res.signature_xml.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature")
153+
self.assertEqual(res[0].signature_xml.tag, "{http://www.w3.org/2000/09/xmldsig#}Signature")
153154

154155
if method == methods.enveloping:
155156
with self.assertRaisesRegex(InvalidInput, "Unable to resolve reference URI"):
@@ -435,7 +436,7 @@ def test_reference_uris_and_custom_key_info(self):
435436
reference_uri = ["assertionId", "assertion2"] if "assertion2" in d else "assertionId"
436437
signed_root = XMLSigner().sign(data, reference_uri=reference_uri, key=key, cert=crt)
437438
res = XMLVerifier().verify(etree.tostring(signed_root), x509_cert=crt, expect_references=True)
438-
signed_data_root = res.signed_xml
439+
signed_data_root = res[0].signed_xml
439440
ref = signed_root.xpath(
440441
"/samlp:Response/saml:Assertion/ds:Signature/ds:SignedInfo/ds:Reference",
441442
namespaces={
@@ -569,12 +570,10 @@ class TestXAdES(unittest.TestCase, LoadExampleKeys):
569570
"nonconformant-dss1770.xml": 3,
570571
}
571572
signature_policy = {
572-
"Identifier": (
573-
"http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf"
574-
),
575-
"Description": "Política de Firma FacturaE v3.1",
576-
"DigestMethod": "sha1",
577-
"DigestValue": b":\x18\xb1\x97\xab\xa9\x0f\xa6\xaf\xf0\xde\xe9\x12\xf0\xc0\x06\x11\x0b\xea\x13",
573+
"Identifier": "urn:sbr:signature-policy:xml:2.0",
574+
"Description": "Test description",
575+
"DigestMethod": digest_algorithms.SHA256,
576+
"DigestValue": "sVHhN1eqNH/PZ1B6h//ehyC1OwRQOrz/tJ3ZYaRrBgA=",
578577
}
579578
claimed_roles = ["signer"]
580579
data_object_format = {"Description": "Important Document", "MimeType": "text/xml"}
@@ -590,7 +589,9 @@ def test_xades_roundtrip(self):
590589
)
591590
signed_doc = signer.sign(doc, key=key, cert=cert)
592591
verifier = XAdESVerifier()
593-
verify_results = verifier.verify(signed_doc, x509_cert=cert, expect_references=3)
592+
verify_results = verifier.verify(
593+
signed_doc, x509_cert=cert, expect_references=3, expect_signature_policy=self.signature_policy
594+
)
594595
self.assertIsInstance(verify_results[1], XAdESVerifyResult)
595596
self.assertTrue(hasattr(verify_results[1], "signed_properties"))
596597

@@ -613,6 +614,8 @@ def test_xades_interop_examples(self):
613614
kwargs = dict(x509_cert=cert, expect_references=self.expect_references.get(os.path.basename(sig_file), 2))
614615
if "nonconformant" in sig_file:
615616
kwargs.update(validate_schema=False)
617+
if "sigPolStore" in sig_file:
618+
kwargs.update(expect_signature_policy=self.signature_policy)
616619
for condition, error in error_conditions.items():
617620
if condition in sig_file:
618621
with self.assertRaises(error):

0 commit comments

Comments
 (0)