78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
"""Preventive SSL CA certificate checks — catch broken CA bundle paths before
|
|
OpenAI/httpx turns them into an opaque ``FileNotFoundError``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import ssl
|
|
from pathlib import Path
|
|
|
|
from agent.errors import SSLConfigurationError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_CA_BUNDLE_ENV_VARS = ("HERMES_CA_BUNDLE", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE")
|
|
_SKIP_VALUES = {"1", "true", "yes", "on"}
|
|
_REPAIR_HINT = (
|
|
"Repair: run `hermes doctor --fix` (auto-reinstalls certifi), or "
|
|
"manually: python -m pip install --force-reinstall certifi openai httpx\n"
|
|
"If you configured a custom corporate CA bundle, fix or unset the "
|
|
"broken CA bundle environment variable."
|
|
)
|
|
|
|
|
|
def _skip_ssl_guard_enabled() -> bool:
|
|
return os.getenv("HERMES_SKIP_SSL_GUARD", "").strip().lower() in _SKIP_VALUES
|
|
|
|
|
|
def _ssl_err(message: str) -> SSLConfigurationError:
|
|
"""Create a consistent, user-actionable SSL configuration error."""
|
|
return SSLConfigurationError(f"{message}\n{_REPAIR_HINT}")
|
|
|
|
|
|
def _validate_bundle_path(label: str, value: str, *, require_substantial: bool = False) -> None:
|
|
path = Path(value).expanduser()
|
|
if not path.exists():
|
|
raise _ssl_err(f"{label} points to a missing CA bundle: {value}")
|
|
if not path.is_file():
|
|
raise _ssl_err(f"{label} does not point to a CA bundle file: {value}")
|
|
if require_substantial and path.stat().st_size < 1024:
|
|
raise _ssl_err(f"{label} at {value} appears corrupted (too small)")
|
|
try:
|
|
ctx = ssl.create_default_context(cafile=str(path))
|
|
except Exception as exc:
|
|
raise _ssl_err(f"{label} CA bundle at {value} cannot be loaded: {exc}") from exc
|
|
try:
|
|
loaded_certs = ctx.get_ca_certs()
|
|
except NotImplementedError:
|
|
# truststore-backed SSLContext (Windows OS trust store) lacks get_ca_certs(); loading above already validated it.
|
|
return
|
|
if not loaded_certs:
|
|
raise _ssl_err(f"{label} CA bundle at {value} did not load any certificates")
|
|
|
|
|
|
def verify_ca_bundle() -> None:
|
|
"""Verify configured and bundled CA certificates are present and loadable.
|
|
|
|
Raises SSLConfigurationError when an explicit CA-bundle env var points at a
|
|
bad path or certifi's bundled ``cacert.pem`` is missing/corrupt.
|
|
"""
|
|
if _skip_ssl_guard_enabled():
|
|
logger.debug("SSL CA bundle guard skipped via HERMES_SKIP_SSL_GUARD")
|
|
return
|
|
for env_var in _CA_BUNDLE_ENV_VARS:
|
|
value = os.getenv(env_var)
|
|
if value:
|
|
_validate_bundle_path(env_var, value)
|
|
try:
|
|
import certifi
|
|
except Exception as exc:
|
|
raise _ssl_err(f"certifi is not importable: {exc}") from exc
|
|
_validate_bundle_path("certifi", str(certifi.where()), require_substantial=True)
|
|
|
|
|
|
def verify_ca_bundle_with_fallback() -> None:
|
|
"""Backward-compatible name for older call sites; a broken certifi bundle fails later anyway, so enforce the same check."""
|
|
verify_ca_bundle()
|