Skip to content

Commit

Permalink
Disable ca_path; factor out _match_key_values; roundtrip signed_info
Browse files Browse the repository at this point in the history
  • Loading branch information
kislyuk committed Aug 13, 2024
1 parent fcf30d9 commit ffd7592
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 38 deletions.
11 changes: 10 additions & 1 deletion signxml/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,22 @@ class X509CertChainVerifier:
"All certificates appearing in an X509Data element must relate to the validation key by either containing it
or being part of a certification chain that terminates in a certificate containing the validation key.
No ordering is implied by the above constraints"
Note: SignXML no longer uses OpenSSL for certificate chain verificaiton. The CApath parameter supported by OpenSSL
is not supported by cryptography. The CApath parameter is used to specify a directory containing CA certificates in
PEM format. The files each contain one CA certificate. The files are looked up by the CA subject name hash value.
See https://docs.openssl.org/master/man3/SSL_CTX_load_verify_locations/#notes. If you need CApath support, please
contact SignXML maintainers.
"""

def __init__(self, ca_pem_file=None, ca_path=None, verification_time=None):
if ca_pem_file is None:
ca_pem_file = certifi.where()
self.ca_pem_file = ca_pem_file
self.ca_path = ca_path # FIXME: determine and replicate openssl capath semantics
if ca_path is not None:
msg = "CApath is not supported. If you need this feature, please contact SignXML maintainers."
raise NotImplementedError(msg)

self.verification_time = verification_time

@property
Expand Down
79 changes: 42 additions & 37 deletions signxml/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def _verify_signature_with_pubkey(
key_value: Optional[etree._Element] = None,
der_encoded_key_value: Optional[etree._Element] = None,
signing_certificate: Optional[x509.Certificate] = None,
) -> None:
) -> bytes:
if der_encoded_key_value is not None:
assert der_encoded_key_value.text is not None
key = load_der_public_key(b64decode(der_encoded_key_value.text))
Expand Down Expand Up @@ -176,6 +176,7 @@ def _verify_signature_with_pubkey(
key.verify(raw_signature, data=signed_info_c14n, padding=padding, algorithm=digest_alg_impl)
else:
raise InvalidInput(f"Unsupported signature algorithm {signature_alg}")
return signed_info_c14n

def _encode_dss_signature(self, raw_signature: bytes, key_size_bits: int) -> bytes:
want_raw_signature_len = bits_to_bytes_unit(key_size_bits) * 2
Expand Down Expand Up @@ -234,6 +235,33 @@ def _apply_transforms(self, payload, *, transforms_node: etree._Element, signatu
def get_cert_chain_verifier(self, ca_pem_file, ca_path):
return X509CertChainVerifier(ca_pem_file=ca_pem_file, ca_path=ca_path)

def _match_key_values(self, key_value, der_encoded_key_value, signing_cert, signature_alg):
if self.config.ignore_ambiguous_key_info is False:
return
cert_pub_key = signing_cert.public_key()
# If both X509Data and KeyValue are present, match one against the other and raise an error on mismatch
if key_value is not None:
match_result = self._check_key_value_matches_cert_public_key(key_value, cert_pub_key, signature_alg)
if match_result is False:
raise InvalidInput(
"Both X509Data and KeyValue found and they represent different public keys. "
"Use verify(ignore_ambiguous_key_info=True) to ignore KeyValue and validate "
"using X509Data only."
)

# If both X509Data and DEREncodedKeyValue are present, match one against the other and raise an error on
# mismatch
if der_encoded_key_value is not None:
match_result = self._check_der_key_value_matches_cert_public_key(
der_encoded_key_value, signing_cert.public_key(), signature_alg
)
if match_result is False:
raise InvalidInput(
"Both X509Data and DEREncodedKeyValue found and they represent different "
"public keys. Use verify(ignore_ambiguous_key_info=True) to ignore "
"DEREncodedKeyValue and validate using X509Data only."
)

def verify(
self,
data,
Expand Down Expand Up @@ -405,7 +433,7 @@ def verify(
raise InvalidSignature("Certificate subject common name mismatch")

try:
self._verify_signature_with_pubkey(
verified_signed_info_c14n = self._verify_signature_with_pubkey(
signed_info_c14n=signed_info_c14n,
raw_signature=raw_signature,
signing_certificate=signing_cert,
Expand All @@ -414,60 +442,37 @@ def verify(
except cryptography.exceptions.InvalidSignature as e:
raise InvalidSignature(f"Signature verification failed: {e}")

# If both X509Data and KeyValue are present, match one against the other and raise an error on mismatch
if key_value is not None:
if (
self._check_key_value_matches_cert_public_key(key_value, signing_cert.public_key(), signature_alg)
is False
):
if self.config.ignore_ambiguous_key_info is False:
raise InvalidInput(
"Both X509Data and KeyValue found and they represent different public keys. "
"Use verify(ignore_ambiguous_key_info=True) to ignore KeyValue and validate "
"using X509Data only."
)

# If both X509Data and DEREncodedKeyValue are present, match one against the other and raise an error on
# mismatch
if der_encoded_key_value is not None:
if (
self._check_der_key_value_matches_cert_public_key(
der_encoded_key_value, signing_cert.public_key(), signature_alg
)
is False
):
if self.config.ignore_ambiguous_key_info is False:
raise InvalidInput(
"Both X509Data and DEREncodedKeyValue found and they represent different "
"public keys. Use verify(ignore_ambiguous_key_info=True) to ignore "
"DEREncodedKeyValue and validate using X509Data only."
)

# TODO: CN verification goes here
# TODO: require one of the following to be set: either x509_cert or (ca_pem_file or ca_path) or common_name
# Use ssl.match_hostname or code from it to perform match
self._match_key_values(
key_value=key_value,
der_encoded_key_value=der_encoded_key_value,
signing_cert=signing_cert,
signature_alg=signature_alg,
)
elif signature_alg.name.startswith("HMAC_"):
if self.hmac_key is None:
raise InvalidInput('Parameter "hmac_key" is required when verifying a HMAC signature')

signer = HMAC(key=ensure_bytes(self.hmac_key), algorithm=digest_algorithm_implementations[signature_alg]())
signer.update(signed_info_c14n)
if raw_signature != signer.finalize():
if raw_signature == signer.finalize():
verified_signed_info_c14n = signed_info_c14n
else:
raise InvalidSignature("Signature mismatch (HMAC)")
else:
if key_value is None and der_encoded_key_value is None:
raise InvalidInput("Expected to find either KeyValue or X509Data XML element in KeyInfo")

self._verify_signature_with_pubkey(
verified_signed_info_c14n = self._verify_signature_with_pubkey(
signed_info_c14n=signed_info_c14n,
raw_signature=raw_signature,
key_value=key_value,
der_encoded_key_value=der_encoded_key_value,
signature_alg=signature_alg,
)

verified_signed_info = self._fromstring(verified_signed_info_c14n)
verify_results: List[VerifyResult] = []
for idx, reference in enumerate(self._findall(signed_info, "Reference")):
for idx, reference in enumerate(self._findall(verified_signed_info, "Reference")):
verify_results.append(self._verify_reference(reference, idx, root, uri_resolver, c14n_algorithm, signature))

if type(self.config.expect_references) is int and len(verify_results) != self.config.expect_references:
Expand Down

0 comments on commit ffd7592

Please sign in to comment.