fix: capath instead of cafile for all LLM clients

OpenSSL 3.x ignores intermediate CA certs in -CAfile (verify code 20)
but correctly builds chains with -CApath (verify code 0).
All three clients now use capath:
  - service.py: ssl.create_default_context(capath=...)
  - _llm_http.py: verify='/etc/ssl/certs/'
  - preview_llm_client.py: verify='/etc/ssl/certs/'
check_llm_certs.py rewritten for clarity.
This commit is contained in:
2026-05-29 11:02:48 +03:00
parent 802727b58a
commit 8ecea09222
4 changed files with 165 additions and 339 deletions

View File

@@ -924,24 +924,27 @@ class LLMClient:
# region LLMClient._get_ssl_verify [TYPE Function]
# @PURPOSE Resolve SSL verification flag from environment.
# @POST Returns SSLContext with system CA bundle when enabled,
# @POST Returns SSLContext with system CA dir when enabled,
# False when LLM_SSL_VERIFY env var is "false"/"0"/"no"/"off".
# @RATIONALE Возвращаем ssl.SSLContext вместо True, потому что httpx по
# умолчанию использует certifi, в котором нет корпоративных CA.
# ssl.create_default_context(cafile=...) гарантирует, что используются
# сертификаты из системного хранилища (/etc/ssl/certs/ca-certificates.crt),
# куда update-ca-certificates и наш fallback добавляют корпоративные CA.
# @RATIONALE Используем capath=/etc/ssl/certs/ вместо cafile, потому что
# OpenSSL 3.x не использует intermediate CA сертификаты из cafile для
# построения цепочки (verify code 20). capath с хеш-симлинками работает
# корректно (verify code 0). Оба пути — cafile и capath — указывают на
# один и тот же набор сертификатов, но capath правильно обрабатывает
# цепочку Root → Policy → Issuing.
# @REJECTED verify=<string> отвергнут — httpx 0.28.x депрекейтит строковый
# путь в verify=, требует SSLContext.
# @REJECTED cafile отвергнут — OpenSSL 3.x не использует intermediate CA
# из единого bundle-файла. Только capath с хеш-симлинками даёт code 0.
@staticmethod
def _get_ssl_verify() -> ssl.SSLContext | bool:
raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower()
if raw in ("false", "0", "no", "off"):
return False
ca_path = "/etc/ssl/certs/ca-certificates.crt"
if os.path.exists(ca_path):
return ssl.create_default_context(cafile=ca_path)
# fallback: если системного CA нет (редко), используем дефолтный
ca_dir = "/etc/ssl/certs"
if os.path.isdir(ca_dir):
return ssl.create_default_context(capath=ca_dir)
# fallback: если директории нет (редко), используем дефолтный
return ssl.create_default_context()
# endregion LLMClient._get_ssl_verify