Skip to content

Commit 4963dea

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

File tree

2 files changed

+504
-0
lines changed

2 files changed

+504
-0
lines changed

utils/check-cert.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
16+
def load_cert(path):
17+
with open(path, "rb") as f:
18+
return x509.load_pem_x509_certificate(f.read(), default_backend())
19+
20+
21+
def cert_info(cert_path, cert_key_path=None, issuer_path=None):
22+
try:
23+
# 1. Load and Validate Certificate
24+
cert = load_cert(cert_path)
25+
# 2. Load and Validate Private Key
26+
27+
cert_key_match = None
28+
cert_status = None
29+
cert_status_details = {}
30+
expired = cert.not_valid_after_utc < datetime.datetime.now(tz=datetime.timezone.utc)
31+
already_valid = cert.not_valid_before_utc > datetime.datetime.now(tz=datetime.timezone.utc)
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(
68+
x509.AuthorityInformationAccess
69+
).value
70+
except x509.extensions.ExtensionNotFound:
71+
ocsp_urls = []
72+
if ocsp_urls:
73+
ocsp_url = [
74+
access.access_location.value
75+
for access in ocsp_urls
76+
if access.access_method == x509.AuthorityInformationAccessOID.OCSP
77+
][0]
78+
79+
# Encode request in DER format
80+
ocsp_request_bytes = ocsp_request.public_bytes(serialization.Encoding.DER)
81+
82+
headers = {
83+
"Content-Type": "application/ocsp-request",
84+
"Accept": "application/ocsp-response",
85+
}
86+
87+
response = requests.post(ocsp_url, data=ocsp_request_bytes, headers=headers)
88+
89+
if response.status_code != 200:
90+
print(
91+
f"OCSP request failed with status code {response.status_code}",
92+
file=sys.stderr,
93+
)
94+
else:
95+
# === Parse OCSP Response ===
96+
ocsp_response = ocsp.load_der_ocsp_response(response.content)
97+
98+
cert_status_details["validation_status"] = str(
99+
ocsp_response.response_status
100+
)
101+
cert_status_details["cert_status"] = str(ocsp_response.certificate_status)
102+
cert_status_details["this_update"] = (
103+
ocsp_response.this_update_utc.isoformat()
104+
if ocsp_response.this_update_utc
105+
else None
106+
)
107+
cert_status_details["next_update"] = (
108+
ocsp_response.next_update_utc.isoformat()
109+
if ocsp_response.next_update_utc
110+
else None
111+
)
112+
cert_status_details["revocation_time"] = (
113+
ocsp_response.revocation_time_utc.isoformat()
114+
if ocsp_response.revocation_time_utc
115+
else None
116+
)
117+
cert_status_details["revocation_reason"] = ocsp_response.revocation_reason
118+
119+
return (
120+
{
121+
"expired": expired,
122+
"cert_key_match": cert_key_match,
123+
"serial_number": cert.serial_number,
124+
"issuer": cert.issuer.rfc4514_string(),
125+
"subject": cert.subject.rfc4514_string(),
126+
"not_valid_before": cert.not_valid_before_utc.isoformat(),
127+
"not_valid_after": cert.not_valid_after_utc.isoformat(),
128+
"cert_ocsp_details": cert_status_details,
129+
},
130+
not expired
131+
and already_valid
132+
and cert_key_match is not False
133+
and cert_status_details.get("cert_status") != "OCSPCertStatus.REVOKED",
134+
)
135+
136+
except Exception as e:
137+
raise e
138+
return {}, False
139+
140+
141+
def make_parser():
142+
parser = argparse.ArgumentParser(description="Certificate Checker")
143+
parser.add_argument(
144+
"--cert", required=True, help="Path to the certificate file (PEM format)"
145+
)
146+
parser.add_argument("--key", help="Path to the private key file (PEM format)")
147+
parser.add_argument("--issuer", help="Path to the issuer certificate file (PEM format)")
148+
parser.add_argument("--ca", help="Path to the CA certificate file (PEM format)")
149+
return parser
150+
151+
152+
if __name__ == "__main__":
153+
parser = make_parser()
154+
args = parser.parse_args()
155+
cert_info_result, is_valid = cert_info(
156+
args.cert, cert_key_path=args.key, issuer_path=args.issuer
157+
)
158+
print(cert_info_result)
159+
if is_valid:
160+
print("Certification check succesfull", file=sys.stderr)
161+
else:
162+
print("Certification check failed", file=sys.stderr)
163+
sys.exit(1)

0 commit comments

Comments
 (0)