Skip to content

Commit c039f58

Browse files
committed
feat: Advanced certificate check script
Supports expiration, key-cert mismatch check, revocation check Signed-off-by: Jindrich Luza <jluza@redhat.com>
1 parent 5462606 commit c039f58

File tree

2 files changed

+475
-0
lines changed

2 files changed

+475
-0
lines changed

utils/check-cert.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import requests
5+
import os
6+
import sys
7+
import datetime
8+
import traceback
9+
from cryptography import x509
10+
from cryptography.hazmat.backends import default_backend
11+
from cryptography.hazmat.primitives import serialization, hashes
12+
from cryptography.hazmat.primitives.asymmetric import rsa
13+
from cryptography.x509 import ocsp
14+
15+
def load_cert(path):
16+
with open(path, "rb") as f:
17+
return x509.load_pem_x509_certificate(f.read(), default_backend())
18+
19+
20+
def cert_info(cert_path, cert_key_path=None, issuer_path=None):
21+
try:
22+
# 1. Load and Validate Certificate
23+
cert = load_cert(cert_path)
24+
# 2. Load and Validate Private Key
25+
26+
cert_key_match = None
27+
cert_status = None
28+
cert_status_details = {}
29+
expired = cert.not_valid_after_utc < datetime.datetime.now(tz=datetime.timezone.utc)
30+
#alredy_valid = cert.not_valid_before_utc > datetime.datetime.now()
31+
already_valid = True
32+
33+
if cert_key_path:
34+
with open(cert_key_path, "rb") as f:
35+
key_data = f.read()
36+
# If your key has a password, provide it in 'password='
37+
private_key = serialization.load_pem_private_key(key_data, password=None)
38+
# 3. Check if they match
39+
# We compare the public key derived from the cert vs the one from the private key
40+
cert_pub_key = cert.public_key().public_bytes(
41+
encoding=serialization.Encoding.PEM,
42+
format=serialization.PublicFormat.SubjectPublicKeyInfo
43+
)
44+
key_pub_key = private_key.public_key().public_bytes(
45+
encoding=serialization.Encoding.PEM,
46+
format=serialization.PublicFormat.SubjectPublicKeyInfo
47+
)
48+
if cert_pub_key == key_pub_key:
49+
cert_key_match = True
50+
else:
51+
cert_key_match = False
52+
53+
if issuer_path: # and ca_path:
54+
# if issuer and CA was provided, we can also check OCSP status (revocation)
55+
56+
with open(issuer_path, "rb") as f:
57+
issuer_cert = load_cert(issuer_path)
58+
#with open(ca_path, "rb") as f:
59+
# ca_cert = load_cert(ca_path)
60+
61+
builder = ocsp.OCSPRequestBuilder()
62+
builder = builder.add_certificate(cert, issuer_cert, hashes.SHA1())
63+
ocsp_request = builder.build()
64+
65+
# Extract OCSP URL from the certificate
66+
try:
67+
ocsp_urls = cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess).value
68+
except x509.extensions.ExtensionNotFound:
69+
ocsp_urls = []
70+
if ocsp_urls:
71+
ocsp_url = [access.access_location.value for access in ocsp_urls
72+
if access.access_method == x509.AuthorityInformationAccessOID.OCSP][0]
73+
74+
# Encode request in DER format
75+
ocsp_request_bytes = ocsp_request.public_bytes(serialization.Encoding.DER)
76+
77+
headers = {
78+
"Content-Type": "application/ocsp-request",
79+
"Accept": "application/ocsp-response",
80+
}
81+
82+
response = requests.post(ocsp_url, data=ocsp_request_bytes, headers=headers)
83+
84+
if response.status_code != 200:
85+
print(f"OCSP request failed with status code {response.status_code}",
86+
file=sys.stderr)
87+
else:
88+
# === Parse OCSP Response ===
89+
ocsp_response = ocsp.load_der_ocsp_response(response.content)
90+
91+
cert_status_details['validation_status'] = str(ocsp_response.response_status)
92+
cert_status_details['cert_status'] = str(ocsp_response.certificate_status)
93+
cert_status_details['this_update'] = ocsp_response.this_update_utc.isoformat() if ocsp_response.this_update_utc else None
94+
cert_status_details['next_update'] = ocsp_response.next_update_utc.isoformat() if ocsp_response.next_update_utc else None
95+
cert_status_details['revocation_time'] = ocsp_response.revocation_time_utc.isoformat() if ocsp_response.revocation_time_utc else None
96+
cert_status_details['revocation_reason'] = ocsp_response.revocation_reason
97+
98+
return {
99+
"expired": expired,
100+
"cert_key_match": cert_key_match,
101+
"serial_number": cert.serial_number,
102+
"issuer": cert.issuer.rfc4514_string(),
103+
"subject": cert.subject.rfc4514_string(),
104+
"not_valid_before": cert.not_valid_before_utc.isoformat(),
105+
"not_valid_after": cert.not_valid_after_utc.isoformat(),
106+
"cert_ocsp_details": cert_status_details
107+
}, not expired and already_valid and cert_key_match is not False and \
108+
cert_status_details.get('cert_status') != 'OCSPCertStatus.REVOKED'
109+
110+
except Exception as e:
111+
raise e
112+
return {}, False
113+
114+
def make_parser():
115+
parser = argparse.ArgumentParser(description="Certificate Checker")
116+
parser.add_argument("--cert", required=True, help="Path to the certificate file (PEM format)")
117+
parser.add_argument("--key", help="Path to the private key file (PEM format)")
118+
parser.add_argument("--issuer", help="Path to the issuer certificate file (PEM format)")
119+
parser.add_argument("--ca", help="Path to the CA certificate file (PEM format)")
120+
return parser
121+
122+
123+
if __name__ == "__main__":
124+
parser = make_parser()
125+
args = parser.parse_args()
126+
cert_info_result, is_valid = cert_info(args.cert, cert_key_path=args.key, issuer_path=args.issuer)
127+
print(cert_info_result)
128+
if is_valid:
129+
print("Certification check succesfull", file=sys.stderr)
130+
else:
131+
print("Certification check failed", file=sys.stderr)
132+
sys.exit(1)
133+
134+

0 commit comments

Comments
 (0)