Skip to content

Instantly share code, notes, and snippets.

@nobuto-m
Last active July 25, 2023 17:34
Show Gist options
  • Save nobuto-m/e7b2a7ab9f7206254b4dbe2f5aa48edd to your computer and use it in GitHub Desktop.
Save nobuto-m/e7b2a7ab9f7206254b4dbe2f5aa48edd to your computer and use it in GitHub Desktop.
#!/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,
)
@nobuto-m
Copy link
Author

Good.

subject=Common Name: *.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

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.

OK

Missing an intermediate certificate.

subject=Common Name: *.example.com
issuer=Common Name: R3, Organization: Let's Encrypt, Country: US

ERROR: Unable to build a validation path for the certificate "Common Name: *.example.com" - no issuer matching "Common Name: R3, Organization: Let's Encrypt, Country: US" was found

A cert is expired.

subject=Common Name: *.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

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 path could not be validated because the end-entity certificate expired 2021-04-05 06:24:57Z

Invalid domain.

subject=Common Name: *.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

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 myhost.example.org. Valid hostnames include: *.example.com

Invalid level of subdomains to a wildcard certificate.

subject=Common Name: *.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

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 invalid-level.subdomain.example.com. Valid hostnames include: *.example.com

@nobuto-m
Copy link
Author

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

@nobuto-m
Copy link
Author

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