Skip to content

Commit 5a34a32

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 c681042 commit 5a34a32

2 files changed

Lines changed: 568 additions & 0 deletions

File tree

utils/check-cert.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import datetime
5+
import json
6+
import requests
7+
import sys
8+
import traceback
9+
10+
from cryptography import x509
11+
from cryptography.hazmat.backends import default_backend
12+
from cryptography.hazmat.primitives import serialization, hashes
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, ocsp_timeout=10):
22+
try:
23+
# 1. Load and Validate Certificate
24+
cert = load_cert(cert_path)
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+
# 2. Load and Validate Private Key
34+
if cert_key_path:
35+
with open(cert_key_path, "rb") as f:
36+
key_data = f.read()
37+
# If your key has a password, provide it in 'password='
38+
private_key = serialization.load_pem_private_key(key_data, password=None)
39+
# 3. Check if they match
40+
# We compare the public key derived from the cert vs the one from the private key
41+
cert_pub_key = cert.public_key().public_bytes(
42+
encoding=serialization.Encoding.PEM,
43+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
44+
)
45+
key_pub_key = private_key.public_key().public_bytes(
46+
encoding=serialization.Encoding.PEM,
47+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
48+
)
49+
if cert_pub_key == key_pub_key:
50+
cert_key_match = True
51+
else:
52+
cert_key_match = False
53+
54+
if issuer_path:
55+
# if issuer provided, we can also check OCSP status (revocation)
56+
issuer_cert = load_cert(issuer_path)
57+
58+
builder = ocsp.OCSPRequestBuilder()
59+
builder = builder.add_certificate(cert, issuer_cert, hashes.SHA1())
60+
ocsp_request = builder.build()
61+
62+
# Extract OCSP URL from the certificate
63+
try:
64+
ocsp_urls = cert.extensions.get_extension_for_class(
65+
x509.AuthorityInformationAccess
66+
).value
67+
except x509.extensions.ExtensionNotFound:
68+
ocsp_urls = []
69+
if ocsp_urls:
70+
ocsp_url = [
71+
access.access_location.value
72+
for access in ocsp_urls
73+
if access.access_method == x509.AuthorityInformationAccessOID.OCSP
74+
][0]
75+
76+
# Encode request in DER format
77+
ocsp_request_bytes = ocsp_request.public_bytes(serialization.Encoding.DER)
78+
79+
headers = {
80+
"Content-Type": "application/ocsp-request",
81+
"Accept": "application/ocsp-response",
82+
}
83+
84+
response = requests.post(
85+
ocsp_url, data=ocsp_request_bytes, headers=headers, timeout=ocsp_timeout
86+
)
87+
88+
if response.status_code != 200:
89+
print(
90+
f"OCSP request failed with status code {response.status_code}",
91+
file=sys.stderr,
92+
)
93+
else:
94+
# === Parse OCSP Response ===
95+
ocsp_response = ocsp.load_der_ocsp_response(response.content)
96+
97+
cert_status_details["validation_status"] = str(
98+
ocsp_response.response_status
99+
)
100+
cert_status_details["cert_status"] = str(ocsp_response.certificate_status)
101+
cert_status_details["this_update"] = (
102+
ocsp_response.this_update_utc.isoformat()
103+
if ocsp_response.this_update_utc
104+
else None
105+
)
106+
cert_status_details["next_update"] = (
107+
ocsp_response.next_update_utc.isoformat()
108+
if ocsp_response.next_update_utc
109+
else None
110+
)
111+
cert_status_details["revocation_time"] = (
112+
ocsp_response.revocation_time_utc.isoformat()
113+
if ocsp_response.revocation_time_utc
114+
else None
115+
)
116+
cert_status_details["revocation_reason"] = ocsp_response.revocation_reason
117+
else:
118+
print(
119+
"No OCSP URL found in certificate. Cannot check revocation",
120+
file=sys.stderr,
121+
)
122+
123+
return (
124+
{
125+
"expired": expired,
126+
"cert_key_match": cert_key_match,
127+
"serial_number": cert.serial_number,
128+
"issuer": cert.issuer.rfc4514_string(),
129+
"subject": cert.subject.rfc4514_string(),
130+
"not_valid_before": cert.not_valid_before_utc.isoformat(),
131+
"not_valid_after": cert.not_valid_after_utc.isoformat(),
132+
"cert_ocsp_details": cert_status_details,
133+
},
134+
(
135+
not expired
136+
and already_valid
137+
and cert_key_match is not False
138+
and cert_status_details.get("cert_status") != "OCSPCertStatus.REVOKED"
139+
),
140+
)
141+
142+
except Exception:
143+
traceback.print_exc()
144+
return {}, False
145+
146+
147+
def make_parser():
148+
parser = argparse.ArgumentParser(
149+
description="""Certificate Checker
150+
This script checks the validity of a certificate by performing the following checks:
151+
1. Expiration Check: Verifies if the certificate is currently valid based on its
152+
'not valid before' and 'not valid after' dates.
153+
This check is performed every time the script is run.
154+
2. Certificate-Key Match: If a private key is provided, the script checks
155+
if it matches the public key in the certificate.
156+
3. OCSP Revocation Check: If an issuer certificate is provided, the script performs
157+
an OCSP check to determine if the certificate has been revoked.
158+
159+
Script produce following json to stdout and return 0 if all checks are successful,
160+
otherwise return 1
161+
162+
{'expired': <boolean>,
163+
'cert_key_match': <boolean>,
164+
'serial_number': <serial_number>,
165+
'issuer': <issuer>,
166+
'subject': <subject>,
167+
'not_valid_before': <YYYY-MM-DDThh:mm:ss+tz:tz>,
168+
'not_valid_after': <YYYY-MM-DDThh:mm:ss+tz:tz>,
169+
'cert_ocsp_details': {'validation_status': 'OCSPResponseStatus.<STATUS>',
170+
'cert_status': 'OCSPCertStatus.<STATUS>',
171+
'this_update': '<YYYY-MM-DDThh:mm:ss+tz:tz>',
172+
'next_update': <YYYY-MM-DDThh:mm:ss+tz:tz> or null,
173+
'revocation_time': <YYYY-MM-DDThh:mm:ss+tz:tz> or null,
174+
'revocation_reason': <reason>}}
175+
"""
176+
)
177+
parser.add_argument(
178+
"--cert", required=True, help="Path to the certificate file (PEM format)"
179+
)
180+
parser.add_argument("--key", help="Path to the private key file (PEM format)")
181+
parser.add_argument("--issuer", help="Path to the issuer certificate file (PEM format)")
182+
parser.add_argument(
183+
"--ocsp-timeout",
184+
type=int,
185+
default=10,
186+
help="Timeout for OCSP request in seconds (default: 10)",
187+
)
188+
return parser
189+
190+
191+
if __name__ == "__main__":
192+
parser = make_parser()
193+
args = parser.parse_args()
194+
cert_info_result, is_valid = cert_info(
195+
args.cert,
196+
cert_key_path=args.key,
197+
issuer_path=args.issuer,
198+
ocsp_timeout=args.ocsp_timeout,
199+
)
200+
print(json.dumps(cert_info_result))
201+
if is_valid:
202+
print("Certification check successful", file=sys.stderr)
203+
else:
204+
print("Certification check failed", file=sys.stderr)
205+
sys.exit(1)

0 commit comments

Comments
 (0)