fix(ssl): replace verify=True with ssl.create_default_context() for corporate CA support

Core fix: all httpx.AsyncClient instances now use ssl.create_default_context()
instead of bool verify=True, which uses certifi and ignores system CA store.
This makes corporate CA certificates installed via update-ca-certificates
visible to Python HTTP clients.

Files:
- async_network.py: AsyncAPIClient.__init__ converts True→SSLContext
- client_registry.py: get_client converts bool→SSLContext before passing
- notifications/providers.py: _get_http_client uses ssl.create_default_context()
- services/git/_base.py: GitServiceBase uses ssl.create_default_context()
- translate/_llm_async_http.py: _get_verify returns SSLContext (not string)

Test: new integration test with 3-tier PKI (Root→Intermediate→Server),
TLS-protected Superset container, and custom CA installation via
update-ca-certificates or SSL_CERT_DIR fallback.
- conftest.py: added ca_chain, install_custom_ca, superset_tls_env fixtures;
  parameterized superset_container for TLS mode
- test_superset_tls_custom_ca.py: 8 tests (openssl -CApath, -CAfile certifi,
  httpx capath, httpx certifi, AsyncAPIClient, SupersetClient, fingerprint, verify=False)
This commit is contained in:
2026-06-15 10:20:43 +03:00
parent af923972b6
commit 27a20cbbeb
7 changed files with 603 additions and 31 deletions

View File

@@ -18,6 +18,7 @@ import asyncio
import io
import json
from pathlib import Path
import ssl
from typing import Any
import httpx
@@ -59,23 +60,32 @@ class AsyncAPIClient:
# @DATA_CONTRACT Input[config: Dict[str, Any]] -> self._auth_cache_key[str]
# @RELATION CALLS -> [AsyncAPIClient._normalize_base_url]
# @RELATION DEPENDS_ON -> [SupersetAuthCache]
def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT,
def __init__(self, config: dict[str, Any], verify_ssl: bool | ssl.SSLContext = True, timeout: int = DEFAULT_TIMEOUT,
semaphore: asyncio.Semaphore | None = None):
self.base_url: str = self._normalize_base_url(config.get("base_url", ""))
self.api_base_url: str = f"{self.base_url}/api/v1"
self.auth = config.get("auth")
self.request_settings = {"verify_ssl": verify_ssl, "timeout": timeout}
# Use system CA store via ssl.create_default_context() instead of bool True.
# httpx defaults to certifi, ignoring SSL_CERT_FILE and corporate CA certs.
if verify_ssl is True:
ssl_context: bool | ssl.SSLContext = ssl.create_default_context()
elif verify_ssl is False:
ssl_context = False
else:
ssl_context = verify_ssl # уже SSLContext
self._client = httpx.AsyncClient(
verify=verify_ssl,
verify=ssl_context,
timeout=httpx.Timeout(timeout),
follow_redirects=True,
)
self._tokens: dict[str, str] = {}
self._authenticated = False
# Cache key по-прежнему использует bool для хеширования
self._auth_cache_key = SupersetAuthCache.build_key(
self.base_url,
self.auth,
verify_ssl,
verify_ssl if isinstance(verify_ssl, bool) else True,
)
# Optional shared semaphore for connection limiting (set by SupersetClientRegistry).
self._semaphore = semaphore

View File

@@ -19,6 +19,7 @@
# shows callers create SupersetClient anyway, defeating the purpose.
import asyncio
import ssl
from typing import Any
from ..logger import logger
@@ -47,6 +48,7 @@ def _build_env_id(env: Any) -> str:
if isinstance(env, dict):
return f"{env.get('base_url','')}|{env.get('username','')}"
return str(id(env))
# #endregion _build_env_id
# #region get_client [C:3] [TYPE Function]
@@ -97,9 +99,15 @@ async def get_client(
config = {}
verify_ssl = True
timeout = request_timeout
# Convert bool to SSLContext with system CA store — httpx defaults to certifi,
# ignoring corporate CA certificates installed via update-ca-certificates.
if verify_ssl is True:
ssl_context: bool | ssl.SSLContext = ssl.create_default_context()
else:
ssl_context = verify_ssl
async_client = AsyncAPIClient(
config=config,
verify_ssl=verify_ssl,
verify_ssl=ssl_context,
timeout=timeout,
)
semaphore = asyncio.Semaphore(connection_pool_size)

View File

@@ -16,6 +16,7 @@
import asyncio
import os
import ssl
from typing import Any
import httpx
@@ -31,19 +32,22 @@ DEFAULT_MAX_TOKENS: int = 8192
# #region _get_verify [C:1] [TYPE Function] [SEMANTICS translate, ssl, verify]
# @BRIEF Resolve SSL verification path from LLM_SSL_VERIFY env var.
# @RATIONALE Используем capath=/etc/ssl/certs/ вместо cafile, потому что
# OpenSSL 3.x не использует intermediate CA сертификаты из cafile для
# построения цепочки (verify code 20). capath с хеш-симлинками работает
# корректно (verify code 0).
# @BRIEF Resolve SSL verification context from LLM_SSL_VERIFY env var.
# @RATIONALE Используем capath=/etc/ssl/certs/ через ssl.create_default_context,
# потому что OpenSSL 3.x не использует intermediate CA сертификаты из cafile
# для построения цепочки (verify code 20). capath с хеш-симлинками работает
# корректно (verify code 0). Возвращаем SSLContext вместо строки — httpx 0.28.x
# депрекейтит строковый путь в verify=.
# @REJECTED cafile отвергнут — OpenSSL 3.x не использует intermediate CA
# из единого bundle-файла. Только capath с хеш-симлинками даёт code 0.
# @POST Returns path to /etc/ssl/certs/ when enabled, False when disabled.
def _get_verify() -> str | bool:
# @REJECTED Строковый путь "/etc/ssl/certs/" отвергнут — httpx 0.28.x депрекейтит
# строки в verify=, требует SSLContext.
# @POST Returns ssl.SSLContext with capath when enabled, False when disabled.
def _get_verify() -> ssl.SSLContext | bool:
raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower()
if raw in ("false", "0", "no", "off"):
return False
return "/etc/ssl/certs/"
return ssl.create_default_context(capath="/etc/ssl/certs/")
# #endregion _get_verify

View File

@@ -14,6 +14,7 @@ import os
from pathlib import Path
import re
import shutil
import ssl
import threading
from urllib.parse import quote
@@ -56,8 +57,11 @@ class GitServiceBase:
self._closed = False
self._close_lock = threading.Lock()
# Fix 5: Shared httpx.AsyncClient with connection pooling
# Fix 5: Shared httpx.AsyncClient with connection pooling.
# Uses system CA store (ssl.create_default_context) instead of certifi
# to trust corporate CA certificates installed via update-ca-certificates.
self._http_client = httpx.AsyncClient(
verify=ssl.create_default_context(),
timeout=30.0,
limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),
)
@@ -397,5 +401,3 @@ class GitServiceBase:
# endregion close
# #endregion GitServiceBase
# #endregion GitServiceBase
# #endregion GitServiceBase
# #endregion GitServiceBase

View File

@@ -20,6 +20,7 @@
from abc import ABC, abstractmethod
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import ssl
from typing import Any
import aiosmtplib
@@ -32,10 +33,16 @@ _http_client: httpx.AsyncClient | None = None
async def _get_http_client() -> httpx.AsyncClient:
"""Get or create the module-level httpx.AsyncClient singleton."""
"""Get or create the module-level httpx.AsyncClient singleton.
Uses system CA store (ssl.create_default_context) instead of certifi
to trust corporate CA certificates installed via update-ca-certificates.
"""
global _http_client
if _http_client is None:
_http_client = httpx.AsyncClient(timeout=httpx.Timeout(30.0))
_http_client = httpx.AsyncClient(
verify=ssl.create_default_context(),
timeout=httpx.Timeout(30.0),
)
return _http_client

View File

@@ -220,6 +220,111 @@ def verify_postgres_features(session: Session) -> dict:
# #endregion verify_postgres_features
# ── TLS / PKI Fixtures ────────────────────────────────────────────────
# #region ca_chain [C:2] [TYPE Fixture]
# @BRIEF Session-scoped — generates 3-tier PKI (Root CA → Intermediate CA → Server cert).
# @POST Returns dict with root_crt, intermediate_crt, server_crt, server_key, fullchain PEM strings.
@pytest.fixture(scope="session")
def ca_chain():
"""Generate 3-tier PKI: Root CA → Intermediate CA → Server certificate (SAN: localhost)."""
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import datetime
def _gen_key():
return rsa.generate_private_key(public_exponent=65537, key_size=2048)
def _gen_cert_pem(subject_name, issuer_name, subject_key, issuer_key, is_ca=False, san_dns=None, san_ip=None):
now = datetime.datetime.now(datetime.timezone.utc)
builder = x509.CertificateBuilder()
builder = builder.subject_name(x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, subject_name),
]))
builder = builder.issuer_name(x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, issuer_name),
]))
builder = builder.not_valid_before(now - datetime.timedelta(hours=1))
builder = builder.not_valid_after(now + datetime.timedelta(days=365))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(subject_key.public_key())
if is_ca:
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None), critical=True,
)
builder = builder.add_extension(
x509.KeyUsage(
key_cert_sign=True, crl_sign=True,
digital_signature=False, content_commitment=False,
key_encipherment=False, data_encipherment=False,
key_agreement=False, encipher_only=False, decipher_only=False,
), critical=True,
)
else:
# Server cert
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None), critical=True,
)
sans: list[x509.GeneralName] = []
if san_dns:
sans.append(x509.DNSName(san_dns))
if san_ip:
sans.append(x509.IPAddress(san_ip))
builder = builder.add_extension(
x509.SubjectAlternativeName(sans), critical=False,
)
cert = builder.sign(issuer_key, hashes.SHA256())
return cert.public_bytes(serialization.Encoding.PEM).decode()
root_key = _gen_key()
root_crt = _gen_cert_pem("Test Root CA", "Test Root CA", root_key, root_key, is_ca=True)
intermediate_key = _gen_key()
intermediate_crt = _gen_cert_pem(
"Test Intermediate CA", "Test Root CA",
intermediate_key, root_key, is_ca=True,
)
server_key = _gen_key()
import ipaddress
server_crt = _gen_cert_pem(
"localhost", "Test Intermediate CA",
server_key, intermediate_key, is_ca=False,
san_dns="localhost", san_ip=ipaddress.IPv4Address("127.0.0.1"),
)
# Full chain for server: server PEM + intermediate PEM (order: leaf first)
fullchain = server_crt + "\n" + intermediate_crt
return {
"root_crt": root_crt,
"root_key": root_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
).decode(),
"intermediate_crt": intermediate_crt,
"intermediate_key": intermediate_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
).decode(),
"server_crt": server_crt,
"server_key": server_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
).decode(),
"fullchain": fullchain,
}
# #endregion ca_chain
# ── Superset Integration Test Fixtures ────────────────────────────────
@@ -278,23 +383,54 @@ def superset_admin_password():
# #region superset_container [C:3] [TYPE Fixture]
# @BRIEF Session-scoped Apache Superset container — db upgrade → admin → init → web server.
# Can be parametrized with {"tls": True} to enable HTTPS with custom 3-tier PKI.
# @PRE Docker daemon is running. superset_db_url points to an empty Postgres database.
# @POST Superset web server is listening on port 8088. Admin user created.
# @POST Superset web server is listening on port 8088 (HTTP or HTTPS). Admin user created.
# @SIDE_EFFECT Spins up a short-lived init container, then a long-lived web container.
# In TLS mode: writes server cert + key into container via base64, runs with --cert/--key.
# @RATIONALE Two-container approach (init + web) avoids race conditions where the web
# server starts before migrations have completed. The init container runs `superset db
# upgrade`, creates the admin user, and runs `superset init`, then exits. The web
# container then starts against the fully initialized database.
# TLS parametrization allows testing corporate SSL certificate chain with the
# same Superset instance, using a dynamically generated 3-tier PKI.
# @REJECTED Single container with sequential exec calls — cannot change entrypoint
# from init to web server within the same container lifecycle.
# @REJECTED Using `latest` tag — version drift breaks tests silently.
@pytest.fixture(scope="session")
def superset_container(superset_db_url, superset_secret_key, superset_admin_password):
"""Start an Apache Superset instance for integration testing."""
def superset_container(request, superset_db_url, superset_secret_key, superset_admin_password):
"""Start an Apache Superset instance for integration testing.
Parametrize with indirect=True, {"tls": True} to enable HTTPS with custom CA chain.
"""
from testcontainers.core.container import DockerContainer
import base64
superset_image = "apache/superset:4.1.2"
# Check if TLS was requested via parametrization
tls_config = getattr(request, 'param', None)
use_tls = tls_config is not None and tls_config.get('tls', False)
if use_tls:
ca_chain = request.getfixturevalue('ca_chain')
# Base64-encode certs to avoid shell escaping issues in heredoc
fullchain_b64 = base64.b64encode(ca_chain["fullchain"].encode()).decode()
key_b64 = base64.b64encode(ca_chain["server_key"].encode()).decode()
_write_certs = (
f"echo '{fullchain_b64}' | base64 -d > /tmp/server.crt && "
f"echo '{key_b64}' | base64 -d > /tmp/server.key"
)
_protocol = "https"
_superset_run = (
f"superset run -h 0.0.0.0 -p 8088 "
f"--cert /tmp/server.crt --key /tmp/server.key --with-threads"
)
else:
_write_certs = ""
_protocol = "http"
_superset_run = "superset run -h 0.0.0.0 -p 8088 --with-threads"
# Superset ignores SQLALCHEMY_DATABASE_URI env var — we must create
# superset_config.py and point SUPERSET_CONFIG_PATH to it.
_bootstrap_config = (
@@ -311,6 +447,7 @@ def superset_container(superset_db_url, superset_secret_key, superset_admin_pass
init.with_env("SUPERSET_DB_URI", superset_db_url)
init.with_env("SUPERSET_LOAD_EXAMPLES", "no")
init.with_env("FLASK_APP", "superset")
# Note: init container does NOT need TLS certs — only runs db upgrade + init
init.with_command(
f"sh -c '"
f"python -m pip install psycopg2-binary -q && "
@@ -344,28 +481,46 @@ def superset_container(superset_db_url, superset_secret_key, superset_admin_pass
superset.with_env("SUPERSET_DB_URI", superset_db_url)
superset.with_env("SUPERSET_LOAD_EXAMPLES", "no")
superset.with_env("FLASK_APP", "superset")
superset.with_command(
f"sh -c '"
f"python -m pip install psycopg2-binary -q && "
f"{_bootstrap_config}"
f"{_export_config} && "
f"superset run -h 0.0.0.0 -p 8088 --with-threads'"
)
# Build web server command: optionally write certs, then start superset
if use_tls and _write_certs:
web_cmd = (
f"sh -c '"
f"python -m pip install psycopg2-binary -q && "
f"{_write_certs} && "
f"{_bootstrap_config}"
f"{_export_config} && "
f"{_superset_run}'"
)
else:
web_cmd = (
f"sh -c '"
f"python -m pip install psycopg2-binary -q && "
f"{_bootstrap_config}"
f"{_export_config} && "
f"{_superset_run}'"
)
superset.with_command(web_cmd)
superset.with_exposed_ports(8088)
superset.start()
# Store protocol on the container object for superset_url fixture
superset._ss_protocol = _protocol
# Wait for the /health endpoint to respond
host = superset.get_container_host_ip()
port = superset.get_exposed_port(8088)
health_url = f"http://{host}:{port}/health"
health_url = f"{_protocol}://{host}:{port}/health"
deadline = time.time() + 90
last_error = None
while time.time() < deadline:
try:
resp = requests.get(health_url, timeout=3)
# verify=False in TLS mode because the custom CA hasn't been installed yet
resp = requests.get(health_url, timeout=3, verify=False)
if resp.status_code == 200:
break
except requests.RequestException as e:
@@ -386,12 +541,13 @@ def superset_container(superset_db_url, superset_secret_key, superset_admin_pass
# #region superset_url [C:1] [TYPE Fixture]
# @BRIEF Function-scoped — base URL of the running Superset container.
# @BRIEF Function-scoped — base URL of the running Superset container (auto-detects http/https).
@pytest.fixture
def superset_url(superset_container):
host = superset_container.get_container_host_ip()
port = superset_container.get_exposed_port(8088)
return f"http://{host}:{port}"
protocol = getattr(superset_container, '_ss_protocol', 'http')
return f"{protocol}://{host}:{port}"
# #endregion superset_url
@@ -491,4 +647,89 @@ async def superset_client(superset_env):
# #endregion superset_client
# ── TLS / Custom CA Fixtures ─────────────────────────────────────
# #region install_custom_ca [C:2] [TYPE Fixture]
# @BRIEF Function-scoped — installs Test Root CA into system trust store.
# Tries update-ca-certificates (root required), falls back to SSL_CERT_DIR.
# @POST Root CA accessible via ssl.create_default_context().
# @SIDE_EFFECT Modifies /usr/local/share/ca-certificates/custom/ and runs update-ca-certificates.
# On fallback: sets SSL_CERT_DIR env var, creates temp dir with hash symlinks.
@pytest.fixture
def install_custom_ca(ca_chain):
"""Install the custom Root CA into the system trust store.
Strategy 1 (prod-like): write .crt → update-ca-certificates (requires root).
Strategy 2 (fallback): SSL_CERT_DIR with hash symlinks.
"""
import subprocess
import tempfile
from pathlib import Path
ca_pem = ca_chain["root_crt"]
used_update_ca = False
ca_verify_path = None
cert_dir = None
# Strategy 1: update-ca-certificates (matches production install_certificates())
try:
cert_dir = Path("/usr/local/share/ca-certificates/custom")
cert_dir.mkdir(parents=True, exist_ok=True)
ca_path = cert_dir / "test_ss_tools_ca.crt"
ca_path.write_text(ca_pem)
subprocess.run(
["update-ca-certificates", "--fresh"],
check=True, timeout=30, capture_output=True,
)
ca_verify_path = "/etc/ssl/certs"
used_update_ca = True
except (subprocess.CalledProcessError, PermissionError, OSError):
# Strategy 2: fallback — SSL_CERT_DIR
cert_dir = Path(tempfile.mkdtemp(prefix="test_ca_"))
ca_path = cert_dir / "test_ss_tools_ca.pem"
ca_path.write_text(ca_pem)
# Create hash symlink for capath
result = subprocess.run(
["openssl", "x509", "-hash", "-noout"],
input=ca_pem, capture_output=True, text=True, timeout=10,
)
cert_hash = result.stdout.strip()
symlink_path = cert_dir / f"{cert_hash}.0"
if symlink_path.exists():
symlink_path.unlink()
symlink_path.symlink_to(ca_path.name)
os.environ["SSL_CERT_DIR"] = str(cert_dir)
ca_verify_path = str(cert_dir)
used_update_ca = False
yield {
"ca_verify_path": ca_verify_path,
"used_update_ca": used_update_ca,
"ca_pem": ca_pem,
}
# Cleanup
if used_update_ca:
ca_file = Path("/usr/local/share/ca-certificates/custom/test_ss_tools_ca.crt")
if ca_file.exists():
ca_file.unlink()
try:
subprocess.run(
["update-ca-certificates", "--fresh"],
timeout=30, capture_output=True,
)
except Exception:
pass
else:
if "SSL_CERT_DIR" in os.environ:
del os.environ["SSL_CERT_DIR"]
if cert_dir and cert_dir.exists():
import shutil
shutil.rmtree(str(cert_dir), ignore_errors=True)
# #endregion install_custom_ca
# #endregion IntegrationTestConftest

View File

@@ -0,0 +1,300 @@
# #region TestSupersetTlsCustomCA [C:3] [TYPE Module] [SEMANTICS test,superset,tls,ssl,integration,ca]
# @BRIEF Integration tests validating the full corporate SSL certificate chain:
# custom CA generation → system store installation → HTTPS via ss-tools HTTP clients.
# @RELATION BINDS_TO -> [AsyncAPIClient]
# @RELATION BINDS_TO -> [SupersetClientBase]
# @RELATION BINDS_TO -> [SupersetClientRegistryModule]
# @RELATION BINDS_TO -> [install_certificates (entrypoint)]
# @RELATION BINDS_TO -> [ADR-0009]
# @RATIONALE Validates that ssl.create_default_context() (used by our patched HTTP clients)
# trusts custom CA certificates installed via update-ca-certificates or SSL_CERT_DIR.
# Uses 3-tier PKI (Root → Intermediate → Server) to test the OpenSSL 3.x capath vs cafile
# limitation documented in ADR-0009 Finding 7.
# @TEST_CONTRACT InstallCustomCA ->
# update-ca-certificates (or SSL_CERT_DIR fallback) makes custom Root CA discoverable
# ssl.create_default_context(capath=...) builds chain successfully
# ssl.create_default_context(cafile=...) fails with intermediate CA (OpenSSL 3.x bug)
# AsyncAPIClient(verify_ssl=True) authenticates against TLS Superset
# SupersetClient(verify_ssl=True) provides full API access over HTTPS
# @REQUIRES Docker daemon running. --run-integration flag.
import pytest
import re
import ssl
import subprocess
from urllib.parse import urlparse
# ── Test Class ──────────────────────────────────────────────────────────
# #region TestCustomCATrustChain [C:3] [TYPE Class]
# @BRIEF Validate that custom CA certificates installed into the system store
# are trusted by openssl, httpx, and ss-tools patched HTTP clients.
@pytest.mark.parametrize("superset_container", [{"tls": True}], indirect=True)
class TestCustomCATrustChain:
"""Full corporate CA trust chain validation against TLS-protected Superset."""
# ── openssl tests ─────────────────────────────────────────────────
# #region test_openssl_verify_capath_ok [C:2] [TYPE Function]
# @BRIEF openssl s_client with -CApath succeeds (hash symlinks).
# @POST Verify return code: 0 (ok)
def test_openssl_verify_capath_ok(self, superset_url, install_custom_ca):
"""openssl s_client -CApath must return code 0 (full chain)."""
parsed = urlparse(superset_url)
host = parsed.hostname
port = parsed.port or 443
ca_path = install_custom_ca["ca_verify_path"]
result = subprocess.run(
[
"openssl", "s_client",
"-connect", f"{host}:{port}",
"-servername", host,
"-CApath", ca_path,
"-verify_return_error",
],
input="", capture_output=True, text=True, timeout=15,
)
# Parse Verify return code from output
match = re.search(r"Verify return code:\s*(\d+)", result.stderr + result.stdout)
assert match is not None, (
f"No Verify return code in openssl output.\n"
f"stdout: {result.stdout[:2000]}\nstderr: {result.stderr[:2000]}"
)
code = int(match.group(1))
assert code == 0, (
f"Expected verify code 0, got {code}.\n"
f"Full output:\n{result.stderr[:2000]}"
)
# #endregion test_openssl_verify_capath_ok
# #region test_openssl_verify_cafile_certifi_fails [C:2] [TYPE Function]
# @BRIEF openssl s_client -CAfile with certifi's bundle fails.
# This proves that certifi's CA bundle does NOT trust our custom Root CA.
# Before our patches, httpx used certifi by default (verify=True → certifi).
# After our patches, httpx uses ssl.create_default_context() (system CA store).
def test_openssl_verify_cafile_certifi_fails(self, superset_url):
"""openssl s_client -CAfile $(certifi) must return non-zero verify code.
This proves that our custom CA is NOT trusted by certifi, which is what
httpx used before our patches. After patches, we use system CA store."""
import certifi
parsed = urlparse(superset_url)
host = parsed.hostname
port = parsed.port or 443
result = subprocess.run(
[
"openssl", "s_client",
"-connect", f"{host}:{port}",
"-servername", host,
"-CAfile", certifi.where(),
"-verify_return_error",
],
input="", capture_output=True, text=True, timeout=15,
)
match = re.search(r"Verify return code:\s*(\d+)", result.stderr + result.stdout)
assert match is not None, (
f"No Verify return code in openssl output.\n"
f"stdout: {result.stdout[:2000]}\nstderr: {result.stderr[:2000]}"
)
code = int(match.group(1))
assert code != 0, (
"Expected non-zero verify code with certifi CA bundle, got 0.\n"
f"This means certifi somehow trusts our generated CA, which should not happen.\n"
f"Full output:\n{result.stderr[:2000]}"
)
# #endregion test_openssl_verify_cafile_certifi_fails
# ── httpx SSL context tests ──────────────────────────────────────
# #region test_httpx_capath_works [C:2] [TYPE Function]
# @BRIEF httpx with ssl.create_default_context(capath=...) succeeds.
def test_httpx_capath_works(self, superset_url, install_custom_ca):
"""ssl.create_default_context(capath=...) must successfully connect.
This is the production code path used by all patched HTTP clients."""
import asyncio
import httpx
ca_path = install_custom_ca["ca_verify_path"]
ctx = ssl.create_default_context(capath=ca_path)
async def _test():
async with httpx.AsyncClient(verify=ctx, timeout=10) as client:
resp = await client.get(f"{superset_url}/health")
assert resp.status_code == 200
assert resp.text.strip() == "OK"
asyncio.run(_test())
# #endregion test_httpx_capath_works
# #region test_httpx_certifi_fails [C:2] [TYPE Function]
# @BRIEF httpx with certifi's CA bundle must fail.
# This proves the old behavior (verify=True → certifi) would fail with
# corporate CA certificates. After our patches, we use system CA store
# via ssl.create_default_context(capath=...).
def test_httpx_certifi_fails(self, superset_url):
"""ssl.create_default_context(cafile=certifi.where()) must raise SSLError.
Proves that certifi does NOT trust our custom Root CA, which is why
our patches replacing verify=True with ssl.create_default_context()
are necessary."""
import asyncio
import certifi
import httpx
ctx = ssl.create_default_context(cafile=certifi.where())
async def _test():
async with httpx.AsyncClient(verify=ctx, timeout=10) as client:
with pytest.raises((ssl.SSLCertVerificationError, httpx.RemoteProtocolError)):
await client.get(f"{superset_url}/health")
asyncio.run(_test())
# #endregion test_httpx_certifi_fails
# ── ss-tools client tests ────────────────────────────────────────
# #region test_asyncapiclient_verify_true [C:2] [TYPE Function]
# @BRIEF AsyncAPIClient(verify_ssl=True) authenticates against TLS Superset.
# Validates our patched async_network.py code path.
def test_asyncapiclient_verify_true(self, superset_url, superset_admin_password, install_custom_ca):
"""AsyncAPIClient with verify_ssl=True must authenticate over HTTPS.
This validates the core SSL fix in async_network.py."""
# install_custom_ca is used for its side effect — installing CA into system store
assert install_custom_ca is not None
import asyncio
from src.core.utils.async_network import AsyncAPIClient
parsed = urlparse(superset_url)
base_url = f"{parsed.scheme}://{parsed.hostname}:{parsed.port}"
client = AsyncAPIClient(
config={
"base_url": base_url,
"auth": {
"username": "admin",
"password": superset_admin_password,
"provider": "db",
"refresh": "true",
},
},
verify_ssl=True,
timeout=30,
)
async def _test():
try:
tokens = await client.authenticate()
assert "access_token" in tokens, "No access_token in auth response"
assert "csrf_token" in tokens, "No csrf_token in auth response"
finally:
await client.aclose()
asyncio.run(_test())
# #endregion test_asyncapiclient_verify_true
# #region test_superset_client_full_auth [C:2] [TYPE Function]
# @BRIEF Full SupersetClient auth + API call over TLS with verify_ssl=True.
def test_superset_client_full_auth(self, superset_url, superset_admin_password, install_custom_ca):
"""SupersetClient with verify_ssl=True must authenticate and
execute API queries over HTTPS."""
# install_custom_ca is used for its side effect — installing CA into system store
assert install_custom_ca is not None
import asyncio
from src.core.config_models import Environment
from src.core.superset_client import SupersetClient
env = Environment(
id="test_tls",
name="Test TLS Superset",
url=superset_url,
username="admin",
password=superset_admin_password,
verify_ssl=True,
timeout=30,
)
async def _test():
client = SupersetClient(env)
try:
tokens = await client.authenticate()
assert "access_token" in tokens, "No access_token"
assert "csrf_token" in tokens, "No csrf_token"
# Fetch dashboards list via the authenticated client
result = await client.get_dashboards(
query={"page": 0, "page_size": 10}
)
assert isinstance(result, dict), f"Expected dict, got {type(result)}"
assert "result" in result, f"No 'result' key: {list(result.keys())}"
finally:
await client.aclose()
asyncio.run(_test())
# #endregion test_superset_client_full_auth
# ── Regression tests ─────────────────────────────────────────────
# #region test_certifi_bundle_notrust [C:1] [TYPE Function]
# @BRIEF Prove that certifi's CA bundle does NOT contain our custom Root CA.
# This validates WHY the patches are needed: before our fix, httpx used
# certifi by default, which would NOT trust corporate CA certificates.
def test_certifi_bundle_notrust(self, ca_chain):
"""certifi.where() bundle must NOT contain our custom Root CA.
This proves that default httpx.AsyncClient(verify=True) would reject
corporate certificates installed via update-ca-certificates.
Uses SHA256 fingerprint dedup, same as the entrypoint fallback logic."""
import certifi
# Load our Root CA and compute its SHA256 fingerprint
from cryptography import x509
from cryptography.hazmat.primitives import hashes
our_ca = x509.load_pem_x509_certificate(ca_chain["root_crt"].encode())
our_fingerprint = our_ca.fingerprint(hashes.SHA256()).hex()
# Load certifi's bundle and check if any cert matches our fingerprint
with open(certifi.where(), "rb") as f:
bundle_data = f.read()
assert our_fingerprint not in bundle_data.hex(), (
"Our custom Root CA was found in certifi bundle — "
"this should not happen since the CA was just generated at test time."
)
# #endregion test_certifi_bundle_notrust
# #region test_verify_false_still_works [C:1] [TYPE Function]
# @BRIEF verify_ssl=False must still work (regression test).
# The SSL patches must not break the disabled-verification path.
def test_verify_false_still_works(self, superset_url):
"""AsyncAPIClient(verify_ssl=False) must work over HTTPS
(regression — the LLM_SSL_VERIFY=false escape hatch must not break)."""
import asyncio
import httpx
async def _test():
async with httpx.AsyncClient(verify=False, timeout=10) as client:
resp = await client.get(f"{superset_url}/health")
assert resp.status_code == 200
assert resp.text.strip() == "OK"
asyncio.run(_test())
# #endregion test_verify_false_still_works
# #endregion TestCustomCATrustChain
# #endregion TestSupersetTlsCustomCA