Compare commits

...
2 Commits
Author SHA1 Message Date
Ross 134bbf85b6 Add proxy support for SSL certificate fetching in ssl_check utility 2025-11-24 16:44:37 +00:00
Ross Kruger b7242d19de . 2025-11-24 16:35:05 +00:00
4 changed files with 192 additions and 21 deletions
+58 -2
View File
@@ -401,6 +401,8 @@ def ssl_check_cmd(url: str) -> str:
Falls back to a minimal Python-based peer certificate fetch if openssl is not available. Falls back to a minimal Python-based peer certificate fetch if openssl is not available.
""" """
from urllib.parse import urlparse
try: try:
parsed = urlparse(url) parsed = urlparse(url)
host = parsed.hostname or url host = parsed.hostname or url
@@ -450,11 +452,64 @@ def ssl_check_cmd(url: str) -> str:
logger.exception("OpenSSL check failed") logger.exception("OpenSSL check failed")
return f"OpenSSL check failed: {e}\n" return f"OpenSSL check failed: {e}\n"
else: else:
# Fallback: use Python ssl to fetch the peer certificate (leaf only) # Fallback: use Python ssl to fetch the peer certificate (leaf only).
# If a proxy is configured, perform an HTTP CONNECT to the proxy first
# and then do TLS over the established tunnel so the result matches the
# path used by the running Python process.
import ssl import ssl
from urllib.parse import urlparse
import base64
logger.debug("OpenSSL not found; using Python ssl fallback") def fetch_via_proxy(proxy_url: str) -> str:
parsed = urlparse(proxy_url)
proxy_host = parsed.hostname
proxy_port = parsed.port or (443 if parsed.scheme == "https" else 80)
auth_header = None
if parsed.username:
user = parsed.username
pwd = parsed.password or ""
auth_header = base64.b64encode(f"{user}:{pwd}".encode("utf-8")).decode("ascii")
s = socket.create_connection((proxy_host, proxy_port), timeout=15)
try: try:
req = f"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n"
if auth_header:
req += f"Proxy-Authorization: Basic {auth_header}\r\n"
req += "\r\n"
s.sendall(req.encode("ascii"))
# read headers
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if len(resp) > 65536:
break
first_line = resp.split(b"\r\n", 1)[0].decode("ascii", errors="ignore")
if not first_line.startswith("HTTP/") or ("200" not in first_line):
raise RuntimeError(f"Proxy CONNECT failed: {first_line}")
ctx = ssl.create_default_context()
with ctx.wrap_socket(s, server_hostname=host) as ss:
ss.settimeout(15)
der = ss.getpeercert(True)
return ssl.DER_cert_to_PEM_cert(der)
except Exception:
s.close()
raise
logger.debug("OpenSSL not found; using Python ssl fallback (proxy-aware)")
# Prefer HTTPS_PROXY then HTTP_PROXY
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") or os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
try:
if proxy:
logger.debug(f"Using proxy for ssl check: {proxy}")
pem = fetch_via_proxy(proxy)
else:
ctx = ssl.create_default_context() ctx = ssl.create_default_context()
s = socket.socket(socket.AF_INET) s = socket.socket(socket.AF_INET)
with ctx.wrap_socket(s, server_hostname=host) as ss: with ctx.wrap_socket(s, server_hostname=host) as ss:
@@ -462,6 +517,7 @@ def ssl_check_cmd(url: str) -> str:
ss.connect((host, port)) ss.connect((host, port))
der = ss.getpeercert(True) der = ss.getpeercert(True)
pem = ssl.DER_cert_to_PEM_cert(der) pem = ssl.DER_cert_to_PEM_cert(der)
logger.debug("Python ssl fallback succeeded") logger.debug("Python ssl fallback succeeded")
return pem return pem
except Exception as e: except Exception as e:
+81
View File
@@ -20,6 +20,7 @@ import argparse
import base64 import base64
import re import re
import shutil import shutil
import os
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@@ -43,6 +44,59 @@ def run_openssl_s_client(host: str, port: int = 443, tls_flag: str | None = None
return proc.stdout + "\n" + proc.stderr return proc.stdout + "\n" + proc.stderr
def fetch_cert_via_proxy(host: str, port: int, proxy: str, timeout: int = 20) -> List[str]:
"""Connect to proxy, issue CONNECT, perform TLS handshake and return PEM of the leaf cert."""
from urllib.parse import urlparse
import socket
import ssl
import base64
parsed = urlparse(proxy)
proxy_host = parsed.hostname
proxy_port = parsed.port or (443 if parsed.scheme == 'https' else 80)
auth_header = None
if parsed.username:
user = parsed.username
pwd = parsed.password or ""
cred = f"{user}:{pwd}".encode("utf-8")
auth_header = base64.b64encode(cred).decode("ascii")
s = socket.create_connection((proxy_host, proxy_port), timeout=timeout)
try:
connect_req = f"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n"
if auth_header:
connect_req += f"Proxy-Authorization: Basic {auth_header}\r\n"
connect_req += "\r\n"
s.sendall(connect_req.encode("ascii"))
# read response headers
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if len(resp) > 65536:
break
header = resp.split(b"\r\n\r\n", 1)[0].decode("ascii", errors="ignore")
# Simple status check
first_line = header.splitlines()[0] if header.splitlines() else ""
if not first_line.startswith("HTTP/") or ("200" not in first_line):
raise RuntimeError(f"Proxy CONNECT failed: {first_line}")
# Wrap the socket with SSL and fetch peer cert (leaf)
ctx = ssl.create_default_context()
with ctx.wrap_socket(s, server_hostname=host) as ss:
der = ss.getpeercert(True)
pem = ssl.DER_cert_to_PEM_cert(der)
return [pem]
except Exception:
s.close()
raise
def extract_pem_blocks(s: str) -> List[str]: def extract_pem_blocks(s: str) -> List[str]:
pattern = re.compile(r"-----BEGIN CERTIFICATE-----(?:.|\n)*?-----END CERTIFICATE-----", re.M) pattern = re.compile(r"-----BEGIN CERTIFICATE-----(?:.|\n)*?-----END CERTIFICATE-----", re.M)
return pattern.findall(s) return pattern.findall(s)
@@ -127,6 +181,33 @@ def main(argv: List[str]) -> int:
except Exception as e: except Exception as e:
print(f"OpenSSL check failed: {e}") print(f"OpenSSL check failed: {e}")
# If openssl couldn't fetch a chain (or wasn't available), attempt a proxy-aware
# fetch if HTTPS_PROXY or HTTP_PROXY is set. This helps in corporate networks
# where direct TLS is blocked and a proxy must be used.
if not extract_pem_blocks(out):
proxy = None
for key in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"):
proxy = os.environ.get(key)
if proxy:
break
if proxy:
try:
print(f"Attempting proxy CONNECT via {proxy}")
pems = fetch_cert_via_proxy(host, port, proxy)
if pems:
print(f"Fetched {len(pems)} PEM(s) via proxy")
server_paths = write_pems_to_temp(pems)
server_info = [inspect_cert(p) for p in server_paths]
print("Server chain (proxy fetched):")
for i, (s, iss, fp) in enumerate(server_info):
print(f"[{i}] SUBJECT: {s}")
print(f" ISSUER: {iss}")
print(f" FP: {fp}")
else:
print("Proxy CONNECT succeeded but no certs found")
except Exception as e:
print(f"Proxy CONNECT fetch failed: {e}")
# extract certs from openssl output if any # extract certs from openssl output if any
pems = extract_pem_blocks(out) pems = extract_pem_blocks(out)
if pems: if pems:
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['ssl_check.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='ssl_check',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+6 -10
View File
@@ -1,10 +1,6 @@
2025-11-24 16:19:37.964 | DEBUG | __main__:ssl_check_ui:750 - Starting SSL check via UI PS T:\rad-tools\uploader\Uploader_test> .\ssl_check.exe --url http://www.penracourses.org.uk --cert-dir ./cert
2025-11-24 16:19:37.966 | DEBUG | __main__:ssl_check_cmd:412 - ssl_check_cmd: host=www.penracourses.org.uk port=443 Using openssl: C:\Sybase\OCS-15_0\bin\openssl.EXE
2025-11-24 16:19:37.967 | DEBUG | __main__:ssl_check_cmd:416 - Running openssl: C:\Sybase\OCS-15_0\bin\openssl.EXE s_client -showcerts -connect www.penracourses.org.uk:443 -servername www.penracourses.org.uk Server rejected legacy TLS; retrying with -tls1_2
2025-11-24 16:19:39.200 | DEBUG | __main__:ssl_check_cmd:422 - OpenSSL finished, 170 bytes returned, rc=0 No certificate chain found from openssl (server may have rejected the handshake or openssl is incompatible)
Found 2 bundled cert files in cert
No server certs to compare; check network/openssl compatibility
CONNECTED(000002CC)
Loading 'screen' into random state - done
5096:error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version:.\ssl\s23_clnt.c:596: