Skip to content

Commit e3c0c2b

Browse files
authored
Merge commit from fork
* Ensure HMAC is used when hmac_key is set * Fix types * Clarify comment
1 parent 1b501fa commit e3c0c2b

File tree

2 files changed

+60
-21
lines changed

2 files changed

+60
-21
lines changed

signxml/verifier.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from base64 import b64decode
22
from dataclasses import dataclass, replace
3-
from typing import Callable, FrozenSet, List, Optional, Union
3+
from typing import Callable, FrozenSet, List, Optional, Union, Tuple
44
from warnings import warn
55

66
import cryptography.exceptions
77
from cryptography import x509
88
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, utils
99
from cryptography.hazmat.primitives.asymmetric.padding import MGF1, PSS, AsymmetricPadding, PKCS1v15
1010
from cryptography.hazmat.primitives.hmac import HMAC
11-
from cryptography.hazmat.primitives.serialization import load_der_public_key
11+
from cryptography.hazmat.primitives.serialization import load_der_public_key, Encoding, PublicFormat
12+
1213
from lxml import etree
1314

1415
from .algorithms import (
@@ -27,7 +28,6 @@
2728
bits_to_bytes_unit,
2829
bytes_to_long,
2930
ds_tag,
30-
ensure_bytes,
3131
namespaces,
3232
)
3333

@@ -42,6 +42,11 @@ class SignatureConfiguration:
4242
"""
4343
If ``True``, a valid X.509 certificate-based signature with an established chain of trust is required to
4444
pass validation. If ``False``, other types of valid signatures (e.g. HMAC or RSA public key) are accepted.
45+
46+
Ensure you establish trust: to specify an HMAC shared secret that will be required to be used by the signature, use
47+
the ``hmac_key`` parameter. If ``require_x509`` is ``False`` and no ``hmac_key`` is set, any valid XML signature
48+
satisfying the other SignatureConfiguration parameters will be accepted, regardless of the key used. You must then
49+
establish trust by examining the ``VerifyResult`` contents, in particular asserting on the value of ``signature_key``.
4550
"""
4651

4752
location: str = ".//"
@@ -62,13 +67,15 @@ class SignatureConfiguration:
6267
signature_methods: FrozenSet[SignatureMethod] = frozenset(sm for sm in SignatureMethod if "SHA1" not in sm.name)
6368
"""
6469
Set of acceptable signature methods (signature algorithms). Any signature generated using an algorithm not listed
65-
here will fail verification.
70+
here will fail verification. It is recommended that you restrict this set to only methods expected to be used in
71+
signatures handled by your application.
6672
"""
6773

6874
digest_algorithms: FrozenSet[DigestAlgorithm] = frozenset(da for da in DigestAlgorithm if "SHA1" not in da.name)
6975
"""
7076
Set of acceptable digest algorithms. Any signature or reference transform generated using an algorithm not listed
71-
here will cause verification to fail.
77+
here will cause verification to fail. It is recommended that you restrict this set to only methods expected to be
78+
used in signatures handled by your application.
7279
"""
7380

7481
ignore_ambiguous_key_info: bool = False
@@ -86,7 +93,8 @@ class SignatureConfiguration:
8693
class VerifyResult:
8794
"""
8895
This is a dataclass representing structured data returned by :func:`signxml.XMLVerifier.verify`. The results of a
89-
verification contain the signed bytes, the parsed signed XML, and the parsed signature XML. Example usage:
96+
verification contain the signed bytes, the parsed signed XML, the parsed signature XML, and the key that was used
97+
to verify the signature. Example usage:
9098
9199
verified_data = signxml.XMLVerifier().verify(input_data).signed_xml
92100
"""
@@ -100,6 +108,9 @@ class VerifyResult:
100108
signature_xml: etree._Element
101109
"The signature element parsed as XML"
102110

111+
signature_key: bytes
112+
"The cryptographic key that was used to verify the signature, in PEM format (for asymmetric keys) or raw secret bytes (for HMAC keys)"
113+
103114

104115
class XMLVerifier(XMLSignatureProcessor):
105116
"""
@@ -123,7 +134,7 @@ def _verify_signature_with_pubkey(
123134
key_value: Optional[etree._Element] = None,
124135
der_encoded_key_value: Optional[etree._Element] = None,
125136
signing_certificate: Optional[x509.Certificate] = None,
126-
) -> bytes:
137+
) -> Tuple[bytes, bytes | dsa.DSAPublicKey | rsa.RSAPublicKey | ec.EllipticCurvePublicKey]:
127138
if der_encoded_key_value is not None:
128139
assert der_encoded_key_value.text is not None
129140
key = load_der_public_key(b64decode(der_encoded_key_value.text))
@@ -177,7 +188,7 @@ def _verify_signature_with_pubkey(
177188
key.verify(raw_signature, data=signed_info_c14n, padding=padding, algorithm=digest_alg_impl)
178189
else:
179190
raise InvalidInput(f"Unsupported signature algorithm {signature_alg}")
180-
return signed_info_c14n
191+
return signed_info_c14n, key
181192

182193
def _encode_dss_signature(self, raw_signature: bytes, key_size_bits: int) -> bytes:
183194
want_raw_signature_len = bits_to_bytes_unit(key_size_bits) * 2
@@ -279,7 +290,7 @@ def verify(
279290
cert_subject_name: Optional[str] = None,
280291
cert_resolver: Optional[Callable] = None,
281292
ca_pem_file: Optional[Union[str, bytes]] = None,
282-
hmac_key: Optional[str] = None,
293+
hmac_key: Optional[bytes] = None,
283294
validate_schema: bool = True,
284295
parser=None,
285296
uri_resolver: Optional[Callable] = None,
@@ -366,6 +377,15 @@ def verify(
366377
:raises: :class:`signxml.exceptions.InvalidSignature`
367378
"""
368379
self.hmac_key = hmac_key
380+
if hmac_key is not None:
381+
if expect_config.signature_methods == SignatureConfiguration().signature_methods: # default value
382+
expect_config = replace(
383+
expect_config,
384+
signature_methods=frozenset(sm for sm in SignatureMethod if sm.name.startswith("HMAC")),
385+
)
386+
elif any(not sm.name.startswith("HMAC") for sm in expect_config.signature_methods):
387+
raise InvalidInput("When hmac_key is set, all expected signature methods must use HMAC")
388+
369389
self.config = expect_config
370390
if deprecated_kwargs:
371391
self.config = replace(expect_config, **deprecated_kwargs)
@@ -452,7 +472,7 @@ def verify(
452472
raise InvalidSignature("Certificate subject common name mismatch")
453473

454474
try:
455-
verified_signed_info_c14n = self._verify_signature_with_pubkey(
475+
verified_signed_info_c14n, key_used = self._verify_signature_with_pubkey(
456476
signed_info_c14n=signed_info_c14n,
457477
raw_signature=raw_signature,
458478
signing_certificate=signing_cert,
@@ -471,18 +491,19 @@ def verify(
471491
if self.hmac_key is None:
472492
raise InvalidInput('Parameter "hmac_key" is required when verifying a HMAC signature')
473493

474-
signer = HMAC(key=ensure_bytes(self.hmac_key), algorithm=digest_algorithm_implementations[signature_alg]())
494+
signer = HMAC(key=self.hmac_key, algorithm=digest_algorithm_implementations[signature_alg]())
475495
signer.update(signed_info_c14n)
476496
try:
477497
signer.verify(raw_signature)
478498
verified_signed_info_c14n = signed_info_c14n
479499
except cryptography.exceptions.InvalidSignature:
480500
raise InvalidSignature("Signature mismatch (HMAC)")
501+
key_used = self.hmac_key
481502
else:
482503
if key_value is None and der_encoded_key_value is None:
483504
raise InvalidInput("Expected to find either KeyValue or X509Data XML element in KeyInfo")
484505

485-
verified_signed_info_c14n = self._verify_signature_with_pubkey(
506+
verified_signed_info_c14n, key_used = self._verify_signature_with_pubkey(
486507
signed_info_c14n=signed_info_c14n,
487508
raw_signature=raw_signature,
488509
key_value=key_value,
@@ -493,15 +514,17 @@ def verify(
493514
verified_signed_info = self._fromstring(verified_signed_info_c14n)
494515
verify_results: List[VerifyResult] = []
495516
for idx, reference in enumerate(self._findall(verified_signed_info, "Reference")):
496-
verify_results.append(self._verify_reference(reference, idx, root, uri_resolver, c14n_algorithm, signature))
517+
verify_results.append(
518+
self._verify_reference(reference, idx, root, uri_resolver, c14n_algorithm, signature, key_used)
519+
)
497520

498521
if type(self.config.expect_references) is int and len(verify_results) != self.config.expect_references:
499522
msg = "Expected to find {} references, but found {}"
500523
raise InvalidSignature(msg.format(self.config.expect_references, len(verify_results)))
501524

502525
return verify_results if self.config.expect_references > 1 else verify_results[0]
503526

504-
def _verify_reference(self, reference, index, root, uri_resolver, c14n_algorithm, signature):
527+
def _verify_reference(self, reference, index, root, uri_resolver, c14n_algorithm, signature, signature_key_used):
505528
copied_root = self._fromstring(self._tostring(root))
506529
copied_signature_ref = self._get_signature(copied_root)
507530
transforms = self._find(reference, "Transforms", require=False)
@@ -522,7 +545,12 @@ def _verify_reference(self, reference, index, root, uri_resolver, c14n_algorithm
522545
payload_c14n_xml = self._fromstring(payload_c14n)
523546
except etree.XMLSyntaxError:
524547
payload_c14n_xml = None
525-
return VerifyResult(payload_c14n, payload_c14n_xml, signature)
548+
549+
if isinstance(signature_key_used, bytes):
550+
signature_key = signature_key_used
551+
else:
552+
signature_key = signature_key_used.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
553+
return VerifyResult(payload_c14n, payload_c14n_xml, signature, signature_key=signature_key)
526554

527555
def validate_schema(self, signature):
528556
last_exception = None

test/test.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def check_deprecated_methods(self):
5353

5454

5555
sha1_ok = SignatureConfiguration(signature_methods=list(SignatureMethod), digest_algorithms=list(DigestAlgorithm))
56+
hmac_only = SignatureConfiguration(
57+
signature_methods=[sm for sm in SignatureMethod if "HMAC" in sm.name], digest_algorithms=list(DigestAlgorithm)
58+
)
5659
xades_sha1_ok = XAdESSignatureConfiguration(
5760
signature_methods=list(SignatureMethod), digest_algorithms=list(DigestAlgorithm)
5861
)
@@ -162,7 +165,10 @@ def test_case(case):
162165
)
163166
# print(etree.tostring(signed))
164167
hmac_key = self.keys["hmac"] if sig_alg_type == "hmac" else None
165-
verify_kwargs = dict(require_x509=False, hmac_key=hmac_key, validate_schema=True, expect_config=sha1_ok)
168+
expect_config = hmac_only if sig_alg_type == "hmac" else sha1_ok
169+
verify_kwargs = dict(
170+
require_x509=False, hmac_key=hmac_key, validate_schema=True, expect_config=expect_config
171+
)
166172

167173
if method == methods.detached:
168174

@@ -199,7 +205,7 @@ def resolver(uri):
199205
signed_data,
200206
hmac_key=hmac_key,
201207
uri_resolver=verify_kwargs.get("uri_resolver"),
202-
expect_config=sha1_ok,
208+
expect_config=expect_config,
203209
)
204210

205211
if method != methods.detached:
@@ -287,10 +293,10 @@ def get_x509_cert(**kwargs):
287293
XMLVerifier().verify(
288294
sig,
289295
require_x509=False,
290-
hmac_key="testkey",
296+
hmac_key=b"testkey" if "hmac" in signature_file else None,
291297
validate_schema=True,
292298
cert_resolver=get_x509_cert if "x509digest" in signature_file else None,
293-
expect_config=sha1_ok,
299+
expect_config=hmac_only if "hmac" in signature_file else sha1_ok,
294300
)
295301
sig.decode("utf-8")
296302
except Exception as e:
@@ -346,6 +352,11 @@ def cert_resolver(x509_issuer_name, x509_serial_number, x509_digest):
346352
signature_files += glob(os.path.join(interop_dir, "pyXMLSecurity", "*.xml"))
347353
for signature_file in signature_files:
348354
print("Verifying", signature_file)
355+
hmac_key = None
356+
expect_config = sha1_ok
357+
if "hmac" in signature_file:
358+
hmac_key = b"test" if "phaos" in signature_file else b"secret"
359+
expect_config = hmac_only
349360
with open(signature_file, "rb") as fh:
350361
try:
351362
sig = fh.read()
@@ -354,13 +365,13 @@ def cert_resolver(x509_issuer_name, x509_serial_number, x509_digest):
354365
verifier.verify(
355366
sig,
356367
require_x509=False,
357-
hmac_key="test" if "phaos" in signature_file else "secret",
368+
hmac_key=hmac_key,
358369
validate_schema=True,
359370
uri_resolver=resolver,
360371
x509_cert=get_x509_cert(signature_file),
361372
cert_resolver=cert_resolver if "issuer-serial" in signature_file else None,
362373
ca_pem_file=get_ca_pem_file(signature_file),
363-
expect_config=sha1_ok,
374+
expect_config=expect_config,
364375
)
365376
decoded_sig = sig.decode("utf-8")
366377
if "HMACOutputLength" in decoded_sig or "bad" in signature_file or "expired" in signature_file:

0 commit comments

Comments
 (0)