Skip to content

Commit 4f611dc

Browse files
committed
feat(RELEASE-2031): 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 4f611dc

File tree

2 files changed

+528
-0
lines changed

2 files changed

+528
-0
lines changed

utils/check-cert.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import datetime
5+
import requests
6+
import sys
7+
import traceback
8+
9+
from cryptography import x509
10+
from cryptography.hazmat.backends import default_backend
11+
from cryptography.hazmat.primitives import serialization, hashes
12+
from cryptography.x509 import ocsp
13+
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_details = {}
28+
expired = cert.not_valid_after_utc < datetime.datetime.now(tz=datetime.timezone.utc)
29+
already_valid = cert.not_valid_before_utc > datetime.datetime.now(
30+
tz=datetime.timezone.utc
31+
)
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+
(
131+
not expired
132+
and already_valid
133+
and cert_key_match is not False
134+
and cert_status_details.get("cert_status") != "OCSPCertStatus.REVOKED"
135+
),
136+
)
137+
138+
except Exception:
139+
traceback.print_exc()
140+
return {}, False
141+
142+
143+
def make_parser():
144+
parser = argparse.ArgumentParser(description="Certificate Checker")
145+
parser.add_argument(
146+
"--cert", required=True, help="Path to the certificate file (PEM format)"
147+
)
148+
parser.add_argument("--key", help="Path to the private key file (PEM format)")
149+
parser.add_argument("--issuer", help="Path to the issuer certificate file (PEM format)")
150+
parser.add_argument("--ca", help="Path to the CA certificate file (PEM format)")
151+
return parser
152+
153+
154+
if __name__ == "__main__":
155+
parser = make_parser()
156+
args = parser.parse_args()
157+
cert_info_result, is_valid = cert_info(
158+
args.cert, cert_key_path=args.key, issuer_path=args.issuer
159+
)
160+
print(cert_info_result)
161+
if is_valid:
162+
print("Certification check succesfull", file=sys.stderr)
163+
else:
164+
print("Certification check failed", file=sys.stderr)
165+
sys.exit(1)

0 commit comments

Comments
 (0)