Last active
July 25, 2023 17:34
-
-
Save nobuto-m/e7b2a7ab9f7206254b4dbe2f5aa48edd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# $ virtualenv -p python3 .venv | |
# $ source .venv/bin/activate | |
# $ pip3 install certvalidator | |
# $ python3 ./certvalidator_poc.py -h | |
# usage: certvalidator_poc.py [-h] --ssl-cert SSL_CERT [--ssl-key SSL_KEY] | |
# [--ssl-ca SSL_CA] | |
# [hostname ...] | |
# | |
# Validate X.509 Certificate Path/Chain | |
# | |
# positional arguments: | |
# hostname Hostname to be checked against the certificate. | |
# Multiple hostnames can be passed. | |
# | |
# options: | |
# -h, --help show this help message and exit | |
# --ssl-cert SSL_CERT SSL certificate file. Expected format is mod_ssl's | |
# SSLCertificateFile. Please refer to: | |
# https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile | |
# --ssl-key SSL_KEY SSL certificate key file. No check will be made | |
# if it is not RSA key. | |
# --ssl-ca SSL_CA SSL CA file | |
import argparse | |
import hashlib | |
from asn1crypto import keys, pem, x509 | |
from certvalidator import CertificateValidator, ValidationContext | |
def show_human_friendly_header(der_bytes, modulus_digest=None): | |
cert = x509.Certificate.load(der_bytes) | |
print("subject={}".format(cert.subject.human_friendly)) | |
if cert.subject_alt_name_value: | |
print("subject_alt_name={}".format(cert.subject_alt_name_value.native)) | |
if modulus_digest: | |
print("rsa_modulus_digest_sha256={}".format(modulus_digest)) | |
print("issuer={}\n".format(cert.issuer.human_friendly)) | |
def rsa_cert_modulus_digest_sha256(der_bytes): | |
cert = x509.Certificate.load(der_bytes) | |
tbs_certificate = cert["tbs_certificate"] | |
subject_public_key_info = tbs_certificate["subject_public_key_info"] | |
subject_public_key_algorithm = subject_public_key_info["algorithm"] | |
if subject_public_key_algorithm["algorithm"].native != "rsa": | |
# https://security.stackexchange.com/a/73131 | |
return | |
subject_public_key = subject_public_key_info["public_key"].parsed | |
modulus = str(subject_public_key["modulus"].native) | |
modulus_digest_sha256 = hashlib.sha256(modulus.encode()).hexdigest() | |
return modulus_digest_sha256 | |
def rsa_key_modulus_digest_sha256(der_bytes): | |
key_info = keys.PrivateKeyInfo.load(der_bytes) | |
try: | |
algorithm = key_info["private_key_algorithm"]["algorithm"].native | |
if algorithm != "rsa": | |
return | |
except ValueError: | |
print( | |
"WARNING: Failed to get " | |
'key_info["private_key_algorithm"]["algorithm"]' | |
) | |
return | |
key = key_info["private_key"].parsed | |
modulus = str(key["modulus"].native) | |
modulus_digest_sha256 = hashlib.sha256(modulus.encode()).hexdigest() | |
return modulus_digest_sha256 | |
def validate(cert_path, key_path, ca_path, hostnames): | |
end_entity_cert = None | |
intermediates = [] | |
with open(cert_path, "rb") as f: | |
print("[ssl_cert]\n") | |
for _, _, der_bytes in pem.unarmor(f.read(), multiple=True): | |
if end_entity_cert is None: | |
end_entity_cert = der_bytes | |
cert_modulus_digest = rsa_cert_modulus_digest_sha256(der_bytes) | |
show_human_friendly_header(der_bytes, cert_modulus_digest) | |
else: | |
intermediates.append(der_bytes) | |
show_human_friendly_header(der_bytes) | |
key_modulus_digest = None | |
if key_path: | |
print("[ssl_key]\n") | |
with open(key_path, "rb") as f: | |
_, _, der_bytes = pem.unarmor(f.read()) | |
key_modulus_digest = rsa_key_modulus_digest_sha256(der_bytes) | |
print("rsa_modulus_digest_sha256={}\n".format(key_modulus_digest)) | |
extra_trust_roots = [] | |
if ca_path: | |
with open(ca_path, "rb") as f: | |
print("[ssl_ca]\n") | |
for _, _, der_bytes in pem.unarmor(f.read(), multiple=True): | |
extra_trust_roots.append(der_bytes) | |
show_human_friendly_header(der_bytes) | |
context = ValidationContext(extra_trust_roots=extra_trust_roots) | |
validator = CertificateValidator( | |
end_entity_cert, intermediates, validation_context=context | |
) | |
try: | |
if hostnames: | |
for hostname in hostnames: | |
validator.validate_tls(hostname=hostname) | |
else: | |
validator.validate_usage( | |
set(["digital_signature"]), set(["server_auth"]) | |
) | |
print("OK: SSL certificate validation passed.") | |
except Exception as e: | |
print("ERROR: {}".format(e)) | |
if ( | |
cert_modulus_digest | |
and key_modulus_digest | |
and cert_modulus_digest != key_modulus_digest | |
): | |
print( | |
"\nERROR: modulus of the SSL certificate and key didn't match. " | |
"Please double check if the cert and key pair is valid." | |
) | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser( | |
description="Validate X.509 Certificate Path/Chain" | |
) | |
parser.add_argument( | |
"--ssl-cert", | |
required=True, | |
help=( | |
"SSL certificate file. Expected format is mod_ssl's " | |
"SSLCertificateFile. Please refer to: " | |
"https://httpd.apache.org/docs/2.4/mod/mod_ssl.html" | |
"#sslcertificatefile" | |
), | |
) | |
parser.add_argument( | |
"--ssl-key", | |
help="SSL certificate key file. No check will be made " | |
"if it is not RSA key.", | |
) | |
parser.add_argument("--ssl-ca", help="SSL CA file") | |
parser.add_argument( | |
"hostname", | |
nargs="*", | |
help=( | |
"Hostname to be checked against the certificate. " | |
"Multiple hostnames can be passed." | |
), | |
) | |
args = parser.parse_args() | |
validate( | |
cert_path=args.ssl_cert, | |
key_path=args.ssl_key, | |
ca_path=args.ssl_ca, | |
hostnames=args.hostname, | |
) |
Another examples after some updates:
No intermediate CA.
[ssl_cert]
subject=Common Name: www.example.com
subject_alt_name=['example.com', 'www.example.com']
issuer=Common Name: R3, Organization: Let's Encrypt, Country: US
[ssl_ca]
subject=Common Name: ISRG Root X1, Organization: Internet Security Research Group, Country: US
issuer=Common Name: DST Root CA X3, Organization: Digital Signature Trust Co.
ERROR: Unable to build a validation path for the certificate "Common Name: www.example.com" - no issuer matching "Common Name: R3, Organization: Let's Encrypt, Country: US" was found
Invalid subdomain.
[ssl_cert]
subject=Common Name: www.example.com
subject_alt_name=['example.com', 'www.example.com']
issuer=Common Name: R3, Organization: Let's Encrypt, Country: US
subject=Common Name: R3, Organization: Let's Encrypt, Country: US
issuer=Common Name: ISRG Root X1, Organization: Internet Security Research Group, Country: US
[ssl_ca]
subject=Common Name: ISRG Root X1, Organization: Internet Security Research Group, Country: US
issuer=Common Name: DST Root CA X3, Organization: Digital Signature Trust Co.
ERROR: The X.509 certificate provided is not valid for blog.example.com. Valid hostnames include: example.com, www.example.com
When certificates passed the checks, but the pair of cert and key was wrong.
[ssl_cert]
subject=Common Name: www.example.com
subject_alt_name=['example.com', 'www.example.com']
modulus_digest_sha256=e2fc6a7d8fe55f84bace9363db6a83759112b0fc63f21fe613970646b71ecdd0
issuer=Common Name: R3, Organization: Let's Encrypt, Country: US
subject=Common Name: R3, Organization: Let's Encrypt, Country: US
issuer=Common Name: ISRG Root X1, Organization: Internet Security Research Group, Country: US
[ssl_key]
modulus_digest_sha256=52c9aa48eeab4c5d83f4712fa671c4ac7495738f71f802299de99914bfc1b989
OK: SSL certificate validation passed.
ERROR: modulus of the SSL certificate and key didn't match. Please double check if the cert and key pair is valid.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Good.
Missing an intermediate certificate.
A cert is expired.
Invalid domain.
Invalid level of subdomains to a wildcard certificate.