Skip to content

Commit af3207f

Browse files
committed
Use enum for c14n methods
Also continue type hints and add a test case for inclusivenamespaces prefixlist
1 parent db38ca1 commit af3207f

File tree

8 files changed

+140
-101
lines changed

8 files changed

+140
-101
lines changed

signxml/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# isort: skip_file
22
from .signer import XMLSigner # noqa:F401
33
from .verifier import XMLVerifier, VerifyResult # noqa:F401
4-
from .algorithms import DigestAlgorithm, SignatureMethod, SignatureType # noqa:F401
4+
from .algorithms import DigestAlgorithm, SignatureMethod, CanonicalizationMethod, SignatureType # noqa:F401
55
from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature # noqa:F401
66
from .processor import XMLSignatureProcessor # noqa:F401
77
from .util import SigningSettings, namespaces # noqa:F401

signxml/algorithms.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ class SignatureMethod(FragmentLookupMixin, InvalidInputErrorMixin, Enum):
9999
EDDSA_ED448 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"
100100

101101

102+
class CanonicalizationMethod(InvalidInputErrorMixin, Enum):
103+
CANONICAL_XML_1_0 = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
104+
CANONICAL_XML_1_0_WITH_COMMENTS = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
105+
CANONICAL_XML_1_1 = "http://www.w3.org/2006/12/xmlc14n11#"
106+
CANONICAL_XML_1_1_DEPRECATED_URI = "http://www.w3.org/2006/12/xml-c14n11"
107+
CANONICAL_XML_1_1_WITH_COMMENTS = "http://www.w3.org/2006/12/xmlc14n11#WithComments"
108+
EXCLUSIVE_XML_CANONICALIZATION_1_0 = "http://www.w3.org/2001/10/xml-exc-c14n#"
109+
EXCLUSIVE_XML_CANONICALIZATION_1_0_WITH_COMMENTS = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
110+
111+
102112
digest_algorithm_implementations = {
103113
DigestAlgorithm.SHA1: hashes.SHA1,
104114
DigestAlgorithm.SHA224: hashes.SHA224,

signxml/processor.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from cryptography.hazmat.primitives.hashes import Hash
77
from lxml import etree
88

9-
from .algorithms import DigestAlgorithm, digest_algorithm_implementations
9+
from .algorithms import CanonicalizationMethod, DigestAlgorithm, digest_algorithm_implementations
1010
from .exceptions import InvalidInput
1111
from .util import namespaces
1212

@@ -75,15 +75,6 @@ class XMLSignatureProcessor(XMLProcessor):
7575
}
7676
known_ecdsa_curve_oids = {ec().name: oid for oid, ec in known_ecdsa_curves.items()} # type: ignore
7777

78-
known_c14n_algorithms = {
79-
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
80-
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
81-
"http://www.w3.org/2001/10/xml-exc-c14n#",
82-
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
83-
"http://www.w3.org/2006/12/xml-c14n11",
84-
"http://www.w3.org/2006/12/xml-c14n11#WithComments",
85-
}
86-
default_c14n_algorithm = "http://www.w3.org/2006/12/xml-c14n11"
8778
excise_empty_xmlns_declarations = False
8879

8980
id_attributes: Tuple[str, ...] = ("Id", "ID", "id", "xml:id")
@@ -104,7 +95,7 @@ def _find(self, element, query, require=True, anywhere=False):
10495
result = element.find(namespace + ":" + query, namespaces=namespaces)
10596

10697
if require and result is None:
107-
raise InvalidInput("Expected to find XML element {} in {}".format(query, element.tag))
98+
raise InvalidInput(f"Expected to find XML element {query} in {element.tag}")
10899
return result
109100

110101
def _findall(self, element, query, anywhere=False):
@@ -116,12 +107,12 @@ def _findall(self, element, query, anywhere=False):
116107
else:
117108
return element.findall(namespace + ":" + query, namespaces=namespaces)
118109

119-
def _c14n(self, nodes, algorithm, inclusive_ns_prefixes=None):
110+
def _c14n(self, nodes, algorithm: CanonicalizationMethod, inclusive_ns_prefixes=None):
120111
exclusive, with_comments = False, False
121112

122-
if algorithm.startswith("http://www.w3.org/2001/10/xml-exc-c14n#"):
113+
if algorithm.value.startswith("http://www.w3.org/2001/10/xml-exc-c14n#"):
123114
exclusive = True
124-
if algorithm.endswith("#WithComments"):
115+
if algorithm.value.endswith("#WithComments"):
125116
with_comments = True
126117

127118
if not isinstance(nodes, list):
@@ -152,17 +143,17 @@ def _resolve_reference(self, doc_root, reference, uri_resolver=None):
152143
# doc_root.xpath(uri.lstrip("#"))[0]
153144
elif uri.startswith("#"):
154145
for id_attribute in self.id_attributes:
155-
xpath_query = "//*[@*[local-name() = '{}']=$uri]".format(id_attribute)
146+
xpath_query = f"//*[@*[local-name() = '{id_attribute}']=$uri]"
156147
results = doc_root.xpath(xpath_query, uri=uri.lstrip("#"))
157148
if len(results) > 1:
158-
raise InvalidInput("Ambiguous reference URI {} resolved to {} nodes".format(uri, len(results)))
149+
raise InvalidInput(f"Ambiguous reference URI {uri} resolved to {len(results)} nodes")
159150
elif len(results) == 1:
160151
return results[0]
161-
raise InvalidInput("Unable to resolve reference URI: {}".format(uri))
152+
raise InvalidInput(f"Unable to resolve reference URI: {uri}")
162153
else:
163154
if uri_resolver is None:
164-
raise InvalidInput("External URI dereferencing is not configured: {}".format(uri))
155+
raise InvalidInput(f"External URI dereferencing is not configured: {uri}")
165156
result = uri_resolver(uri)
166157
if result is None:
167-
raise InvalidInput("Unable to resolve reference URI: {}".format(uri))
158+
raise InvalidInput(f"Unable to resolve reference URI: {uri}")
168159
return result

signxml/signer.py

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
from base64 import b64encode
2-
from typing import Union
2+
from typing import List, Optional, Union
33

44
from cryptography.hazmat.primitives.asymmetric import ec, utils
55
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
66
from cryptography.hazmat.primitives.hmac import HMAC
77
from cryptography.hazmat.primitives.serialization import load_pem_private_key
8-
from lxml.etree import Element, SubElement
8+
from lxml.etree import Element, SubElement, _Element
99
from OpenSSL.crypto import FILETYPE_PEM, dump_certificate
1010

11-
from .algorithms import DigestAlgorithm, SignatureMethod, SignatureType, digest_algorithm_implementations
11+
from .algorithms import (
12+
CanonicalizationMethod,
13+
DigestAlgorithm,
14+
SignatureMethod,
15+
SignatureType,
16+
digest_algorithm_implementations,
17+
)
1218
from .exceptions import InvalidInput
1319
from .processor import XMLSignatureProcessor
1420
from .util import (
@@ -50,10 +56,10 @@ def __init__(
5056
method: SignatureType = SignatureType.enveloped,
5157
signature_algorithm: Union[SignatureMethod, str] = SignatureMethod.RSA_SHA256,
5258
digest_algorithm: Union[DigestAlgorithm, str] = DigestAlgorithm.SHA256,
53-
c14n_algorithm=XMLSignatureProcessor.default_c14n_algorithm,
59+
c14n_algorithm=CanonicalizationMethod.CANONICAL_XML_1_1,
5460
):
5561
if method is None or method not in SignatureType:
56-
raise InvalidInput("Unknown signature method {}".format(method))
62+
raise InvalidInput(f"Unknown signature method {method}")
5763
self.signature_type = method
5864
if isinstance(signature_algorithm, str) and "#" not in signature_algorithm:
5965
self.sign_alg = SignatureMethod.from_fragment(signature_algorithm)
@@ -63,8 +69,7 @@ def __init__(
6369
self.digest_alg = DigestAlgorithm.from_fragment(digest_algorithm)
6470
else:
6571
self.digest_alg = DigestAlgorithm(digest_algorithm)
66-
assert c14n_algorithm in self.known_c14n_algorithms
67-
self.c14n_alg = c14n_algorithm
72+
self.c14n_alg = CanonicalizationMethod(c14n_algorithm)
6873
self.namespaces = dict(ds=namespaces.ds)
6974
self._parser = None
7075
self.signature_annotators = [self._add_key_info]
@@ -73,15 +78,15 @@ def sign(
7378
self,
7479
data,
7580
key=None,
76-
passphrase=None,
81+
passphrase: Optional[bytes] = None,
7782
cert=None,
78-
reference_uri=None,
79-
key_name=None,
80-
key_info=None,
81-
id_attribute=None,
82-
always_add_key_value=False,
83-
payload_inclusive_ns_prefixes=frozenset(),
84-
signature_inclusive_ns_prefixes=frozenset(),
83+
reference_uri: Optional[Union[str, List[str]]] = None,
84+
key_name: Optional[str] = None,
85+
key_info: Optional[_Element] = None,
86+
id_attribute: Optional[str] = None,
87+
always_add_key_value: bool = False,
88+
payload_inclusive_ns_prefixes: Optional[List[str]] = None,
89+
signature_inclusive_ns_prefixes: Optional[List[str]] = None,
8590
signature_properties=None,
8691
):
8792
"""
@@ -100,7 +105,6 @@ def sign(
100105
:py:class:`cryptography.hazmat.primitives.interfaces.DSAPrivateKey`, or
101106
:py:class:`cryptography.hazmat.primitives.interfaces.EllipticCurvePrivateKey` object
102107
:param passphrase: Passphrase to use to decrypt the key, if any.
103-
:type passphrase: string
104108
:param cert:
105109
X.509 certificate to use for signing. This should be a string containing a PEM-formatted certificate, or an
106110
array of strings or OpenSSL.crypto.X509 objects containing the certificate and a chain of intermediate
@@ -110,30 +114,24 @@ def sign(
110114
Custom reference URI or list of reference URIs to incorporate into the signature. When ``method`` is set to
111115
``detached`` or ``enveloped``, reference URIs are set to this value and only the referenced elements are
112116
signed.
113-
:type reference_uri: string or list
114117
:param key_name: Add a KeyName element in the KeyInfo element that may be used by the signer to communicate a
115118
key identifier to the recipient. Typically, KeyName contains an identifier related to the key pair used to
116119
sign the message.
117-
:type key_name: string
118120
:param key_info:
119121
A custom KeyInfo element to insert in the signature. Use this to supply ``<wsse:SecurityTokenReference>``
120122
or other custom key references. An example value can be found here:
121123
https://github.com/XML-Security/signxml/blob/master/test/wsse_keyinfo.xml
122-
:type key_info: :py:class:`lxml.etree.Element`
123124
:param id_attribute:
124125
Name of the attribute whose value ``URI`` refers to. By default, SignXML will search for "Id", then "ID".
125-
:type id_attribute: string
126126
:param always_add_key_value:
127127
Write the key value to the KeyInfo element even if a X509 certificate is present. Use of this parameter
128128
is discouraged, as it introduces an ambiguity and a security hazard. The public key used to sign the
129129
document is already encoded in the certificate (which is in X509Data), so the verifier must either ignore
130130
KeyValue or make sure it matches what's in the certificate. This parameter is provided for compatibility
131131
purposes only.
132-
:type always_add_key_value: boolean
133132
:param payload_inclusive_ns_prefixes:
134133
Provide a list of XML namespace prefixes whose declarations should be preserved when canonicalizing the
135134
content referenced by the signature (**InclusiveNamespaces PrefixList**).
136-
:type inclusive_ns_prefixes: list
137135
:param signature_inclusive_ns_prefixes:
138136
Provide a list of XML namespace prefixes whose declarations should be preserved when canonicalizing the
139137
signature itself (**InclusiveNamespaces PrefixList**).
@@ -161,9 +159,9 @@ def sign(
161159
cert_chain = cert
162160

163161
if isinstance(reference_uri, (str, bytes)):
164-
reference_uris = [reference_uri]
162+
input_reference_uris = [reference_uri]
165163
else:
166-
reference_uris = reference_uri
164+
input_reference_uris = reference_uri # type: ignore
167165

168166
signing_settings = SigningSettings(
169167
key=None,
@@ -181,7 +179,7 @@ def sign(
181179
else:
182180
signing_settings.key = key
183181

184-
sig_root, doc_root, c14n_inputs, reference_uris = self._unpack(data, reference_uris)
182+
sig_root, doc_root, c14n_inputs, reference_uris = self._unpack(data, input_reference_uris)
185183

186184
if self.signature_type == SignatureType.detached and signature_properties is not None:
187185
reference_uris.append("#prop")
@@ -324,7 +322,7 @@ def _unpack(self, data, reference_uris):
324322

325323
def _build_sig(self, sig_root, reference_uris, c14n_inputs, sig_insp, payload_insp):
326324
signed_info = SubElement(sig_root, ds_tag("SignedInfo"), nsmap=self.namespaces)
327-
sig_c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg)
325+
sig_c14n_method = SubElement(signed_info, ds_tag("CanonicalizationMethod"), Algorithm=self.c14n_alg.value)
328326
if sig_insp:
329327
SubElement(sig_c14n_method, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(sig_insp))
330328

@@ -334,9 +332,9 @@ def _build_sig(self, sig_root, reference_uris, c14n_inputs, sig_insp, payload_in
334332
transforms = SubElement(reference, ds_tag("Transforms"))
335333
if self.signature_type == SignatureType.enveloped:
336334
SubElement(transforms, ds_tag("Transform"), Algorithm=namespaces.ds + "enveloped-signature")
337-
SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg)
335+
SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg.value)
338336
else:
339-
c14n_xform = SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg)
337+
c14n_xform = SubElement(transforms, ds_tag("Transform"), Algorithm=self.c14n_alg.value)
340338
if payload_insp:
341339
SubElement(c14n_xform, ec_tag("InclusiveNamespaces"), PrefixList=" ".join(payload_insp))
342340

@@ -356,8 +354,8 @@ def _build_signature_properties(self, signature_properties):
356354
signature_property = Element(
357355
ds_tag("SignatureProperty"),
358356
attrib={
359-
"Id": el.attrib.pop("Id", "sigprop{}".format(i)),
360-
"Target": el.attrib.pop("Target", "#sigproptarget{}".format(i)),
357+
"Id": el.attrib.pop("Id", f"sigprop{i}"),
358+
"Target": el.attrib.pop("Target", f"#sigproptarget{i}"),
361359
},
362360
)
363361
signature_property.append(el)

signxml/util/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def xades141_tag(tag):
6161
@dataclass
6262
class SigningSettings:
6363
key: Any
64-
key_name: str
64+
key_name: Optional[str]
6565
key_info: Any
6666
always_add_key_value: bool
6767
cert_chain: Optional[List]

signxml/verifier.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from OpenSSL.crypto import load_certificate
1313
from OpenSSL.crypto import verify as openssl_verify
1414

15-
from .algorithms import DigestAlgorithm, SignatureMethod, digest_algorithm_implementations
16-
from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature # noqa
15+
from .algorithms import CanonicalizationMethod, DigestAlgorithm, SignatureMethod, digest_algorithm_implementations
16+
from .exceptions import InvalidCertificate, InvalidDigest, InvalidInput, InvalidSignature
1717
from .processor import XMLSignatureProcessor
1818
from .util import (
1919
_remove_sig,
@@ -59,10 +59,15 @@ def _get_signature(self, root):
5959
return self._find(root, "Signature", anywhere=True)
6060

6161
def _verify_signature_with_pubkey(
62-
self, signed_info_c14n, raw_signature, key_value, der_encoded_key_value, signature_alg
63-
):
62+
self,
63+
signed_info_c14n: bytes,
64+
raw_signature: bytes,
65+
key_value: etree._Element,
66+
der_encoded_key_value: Optional[etree._Element],
67+
signature_alg: SignatureMethod,
68+
) -> None:
6469
if der_encoded_key_value is not None:
65-
key = load_der_public_key(b64decode(der_encoded_key_value.text))
70+
key = load_der_public_key(b64decode(der_encoded_key_value.text)) # type: ignore
6671

6772
digest_algorithm_implementation = digest_algorithm_implementations[signature_alg]()
6873
if signature_alg.name.startswith("ECDSA_"):
@@ -94,7 +99,7 @@ def _verify_signature_with_pubkey(
9499
elif not isinstance(key, dsa.DSAPublicKey):
95100
raise InvalidInput("DER encoded key value does not match specified signature algorithm")
96101
# TODO: supply meaningful key_size_bits for signature length assertion
97-
dss_signature = self._encode_dss_signature(raw_signature, len(raw_signature) * 8 / 2)
102+
dss_signature = self._encode_dss_signature(raw_signature, len(raw_signature) * 8 // 2)
98103
key.verify(dss_signature, data=signed_info_c14n, algorithm=digest_algorithm_implementation)
99104
elif signature_alg.name.startswith("RSA_"):
100105
if key_value is not None:
@@ -113,7 +118,7 @@ def _verify_signature_with_pubkey(
113118
else:
114119
raise NotImplementedError()
115120

116-
def _encode_dss_signature(self, raw_signature, key_size_bits):
121+
def _encode_dss_signature(self, raw_signature: bytes, key_size_bits: int) -> bytes:
117122
want_raw_signature_len = bits_to_bytes_unit(key_size_bits) * 2
118123
if len(raw_signature) != want_raw_signature_len:
119124
raise InvalidSignature(
@@ -131,7 +136,7 @@ def _get_inclusive_ns_prefixes(self, transform_node):
131136
else:
132137
return inclusive_namespaces.get("PrefixList").split(" ")
133138

134-
def _apply_transforms(self, payload, transforms_node, signature, c14n_algorithm):
139+
def _apply_transforms(self, payload, transforms_node, signature, c14n_algorithm: CanonicalizationMethod):
135140
transforms, c14n_applied = [], False
136141
if transforms_node is not None:
137142
transforms = self._findall(transforms_node, "Transform")
@@ -146,10 +151,15 @@ def _apply_transforms(self, payload, transforms_node, signature, c14n_algorithm)
146151

147152
for transform in transforms:
148153
algorithm = transform.get("Algorithm")
149-
if algorithm in self.known_c14n_algorithms:
150-
inclusive_ns_prefixes = self._get_inclusive_ns_prefixes(transform)
151-
payload = self._c14n(payload, algorithm=algorithm, inclusive_ns_prefixes=inclusive_ns_prefixes)
152-
c14n_applied = True
154+
try:
155+
c14n_algorithm_from_transform = CanonicalizationMethod(algorithm)
156+
except ValueError:
157+
continue
158+
inclusive_ns_prefixes = self._get_inclusive_ns_prefixes(transform)
159+
payload = self._c14n(
160+
payload, algorithm=c14n_algorithm_from_transform, inclusive_ns_prefixes=inclusive_ns_prefixes
161+
)
162+
c14n_applied = True
153163

154164
if not c14n_applied and not isinstance(payload, (str, bytes)):
155165
payload = self._c14n(payload, algorithm=c14n_algorithm)
@@ -172,7 +182,7 @@ def verify(
172182
id_attribute: Optional[str] = None,
173183
expect_references: Union[int, bool] = 1,
174184
ignore_ambiguous_key_info: bool = False,
175-
) -> List[VerifyResult]:
185+
) -> Union[VerifyResult, List[VerifyResult]]:
176186
"""
177187
Verify the XML signature supplied in the data and return a list of **VerifyResult** data structures
178188
representing the data signed by the signature, or raise an exception if the signature is not valid. By default,
@@ -274,7 +284,7 @@ def verify(
274284

275285
signed_info = self._find(signature, "SignedInfo")
276286
c14n_method = self._find(signed_info, "CanonicalizationMethod")
277-
c14n_algorithm = c14n_method.get("Algorithm")
287+
c14n_algorithm = CanonicalizationMethod(c14n_method.get("Algorithm"))
278288
inclusive_ns_prefixes = self._get_inclusive_ns_prefixes(c14n_method)
279289
signature_method = self._find(signed_info, "SignatureMethod")
280290
signature_value = self._find(signature, "SignatureValue")
@@ -331,7 +341,7 @@ def verify(
331341
lib, func, reason = e.args[0][0]
332342
except Exception:
333343
reason = e
334-
raise InvalidSignature("Signature verification failed: {}".format(reason))
344+
raise InvalidSignature(f"Signature verification failed: {reason}")
335345

336346
# If both X509Data and KeyValue are present, match one against the other and raise an error on mismatch
337347
if key_value is not None:
@@ -408,7 +418,7 @@ def verify(
408418
msg = "Expected to find {} references, but found {}"
409419
raise InvalidSignature(msg.format(expect_references, len(verify_results)))
410420

411-
return verify_results
421+
return verify_results if expect_references > 1 else verify_results[0]
412422

413423
def validate_schema(self, signature):
414424
last_exception = None

0 commit comments

Comments
 (0)