mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(auth): use ssl.SSLContext for CA bundle instead of deprecated string path (#12706)
This commit is contained in:
@@ -20,6 +20,7 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
import shlex
|
||||
import ssl
|
||||
import stat
|
||||
import base64
|
||||
import hashlib
|
||||
@@ -1663,7 +1664,7 @@ def _resolve_verify(
|
||||
insecure: Optional[bool] = None,
|
||||
ca_bundle: Optional[str] = None,
|
||||
auth_state: Optional[Dict[str, Any]] = None,
|
||||
) -> bool | str:
|
||||
) -> bool | ssl.SSLContext:
|
||||
tls_state = auth_state.get("tls") if isinstance(auth_state, dict) else {}
|
||||
tls_state = tls_state if isinstance(tls_state, dict) else {}
|
||||
|
||||
@@ -1683,13 +1684,12 @@ def _resolve_verify(
|
||||
if effective_ca:
|
||||
ca_path = str(effective_ca)
|
||||
if not os.path.isfile(ca_path):
|
||||
import logging
|
||||
logging.getLogger("hermes.auth").warning(
|
||||
logger.warning(
|
||||
"CA bundle path does not exist: %s — falling back to default certificates",
|
||||
ca_path,
|
||||
)
|
||||
return True
|
||||
return ca_path
|
||||
return ssl.create_default_context(cafile=ca_path)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -27,15 +27,23 @@ class TestResolveVerifyFallback:
|
||||
})
|
||||
assert result is True
|
||||
|
||||
def test_valid_ca_bundle_in_auth_state_is_returned(self, tmp_path):
|
||||
def test_valid_ca_bundle_in_auth_state_is_returned(self, tmp_path, monkeypatch):
|
||||
import ssl
|
||||
from hermes_cli.auth import _resolve_verify
|
||||
|
||||
ca_file = tmp_path / "ca-bundle.pem"
|
||||
ca_file.write_text("fake cert")
|
||||
|
||||
# Avoid loading actual PEM — just verify the return type
|
||||
mock_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
monkeypatch.setattr(ssl, "create_default_context", lambda **kw: mock_ctx)
|
||||
|
||||
result = _resolve_verify(auth_state={
|
||||
"tls": {"insecure": False, "ca_bundle": str(ca_file)},
|
||||
})
|
||||
assert result == str(ca_file)
|
||||
assert isinstance(result, ssl.SSLContext), (
|
||||
f"Expected ssl.SSLContext but got {type(result).__name__}: {result!r}"
|
||||
)
|
||||
|
||||
def test_missing_ssl_cert_file_env_falls_back(self, monkeypatch):
|
||||
from hermes_cli.auth import _resolve_verify
|
||||
@@ -76,13 +84,21 @@ class TestResolveVerifyFallback:
|
||||
result = _resolve_verify(ca_bundle="/nonexistent/explicit-ca.pem")
|
||||
assert result is True
|
||||
|
||||
def test_explicit_ca_bundle_param_valid_is_returned(self, tmp_path):
|
||||
def test_explicit_ca_bundle_param_valid_is_returned(self, tmp_path, monkeypatch):
|
||||
import ssl
|
||||
from hermes_cli.auth import _resolve_verify
|
||||
|
||||
ca_file = tmp_path / "explicit-ca.pem"
|
||||
ca_file.write_text("fake cert")
|
||||
|
||||
# Avoid loading actual PEM — just verify the return type
|
||||
mock_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
monkeypatch.setattr(ssl, "create_default_context", lambda **kw: mock_ctx)
|
||||
|
||||
result = _resolve_verify(ca_bundle=str(ca_file))
|
||||
assert result == str(ca_file)
|
||||
assert isinstance(result, ssl.SSLContext), (
|
||||
f"Expected ssl.SSLContext but got {type(result).__name__}: {result!r}"
|
||||
)
|
||||
|
||||
|
||||
def _setup_nous_auth(
|
||||
|
||||
84
tests/test_resolve_verify_ssl_context.py
Normal file
84
tests/test_resolve_verify_ssl_context.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Tests for _resolve_verify returning ssl.SSLContext instead of str for CA bundles.
|
||||
|
||||
This test verifies the fix for bug #12706: httpx deprecates verify=<str> and
|
||||
expects ssl.SSLContext when a custom CA bundle is configured.
|
||||
|
||||
The test should:
|
||||
- FAIL before the fix (returns str path)
|
||||
- PASS after the fix (returns ssl.SSLContext)
|
||||
"""
|
||||
|
||||
import os
|
||||
import ssl
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.auth import _resolve_verify
|
||||
|
||||
|
||||
# Use the system's default CA bundle for testing
|
||||
DEFAULT_CA_FILE = ssl.get_default_verify_paths().cafile
|
||||
|
||||
|
||||
class TestResolveVerifySslContext:
|
||||
"""Test that _resolve_verify returns ssl.SSLContext for CA bundles."""
|
||||
|
||||
def test_resolve_verify_returns_ssl_context_for_ca_bundle(self, monkeypatch):
|
||||
"""When a CA bundle path is provided, _resolve_verify returns an ssl.SSLContext."""
|
||||
# Clear any env vars that might interfere
|
||||
monkeypatch.delenv("HERMES_CA_BUNDLE", raising=False)
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
|
||||
# Use the system's actual CA bundle which is a valid PEM file
|
||||
result = _resolve_verify(ca_bundle=DEFAULT_CA_FILE)
|
||||
|
||||
# The result should be an ssl.SSLContext, NOT a string
|
||||
assert isinstance(result, ssl.SSLContext), (
|
||||
f"Expected ssl.SSLContext but got {type(result).__name__}: {result!r}. "
|
||||
"httpx deprecates verify=<str> and requires ssl.SSLContext."
|
||||
)
|
||||
|
||||
def test_resolve_verify_returns_true_when_no_ca_bundle(self, monkeypatch):
|
||||
"""When no CA bundle is configured, _resolve_verify returns True (not a path)."""
|
||||
# Clear any env vars that might interfere
|
||||
monkeypatch.delenv("HERMES_CA_BUNDLE", raising=False)
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
|
||||
result = _resolve_verify()
|
||||
assert result is True, f"Expected True but got {result!r}"
|
||||
|
||||
def test_resolve_verify_returns_true_for_missing_ca_bundle_path(self, monkeypatch):
|
||||
"""When a CA bundle path is configured but doesn't exist, returns True."""
|
||||
monkeypatch.delenv("HERMES_CA_BUNDLE", raising=False)
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
|
||||
result = _resolve_verify(ca_bundle="/nonexistent/path/to/ca-bundle.crt")
|
||||
assert result is True, f"Expected True for missing CA bundle but got {result!r}"
|
||||
|
||||
def test_resolve_verify_returns_false_when_insecure_is_true(self, monkeypatch):
|
||||
"""When insecure=True, _resolve_verify returns False (skip SSL verification)."""
|
||||
monkeypatch.delenv("HERMES_CA_BUNDLE", raising=False)
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
|
||||
result = _resolve_verify(insecure=True)
|
||||
assert result is False, f"Expected False for insecure=True but got {result!r}"
|
||||
|
||||
def test_resolve_verify_returns_ssl_context_from_hermes_ca_bundle_env(self, monkeypatch):
|
||||
"""SSLContext is returned when HERMES_CA_BUNDLE env var is set."""
|
||||
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
||||
monkeypatch.setenv("HERMES_CA_BUNDLE", DEFAULT_CA_FILE)
|
||||
|
||||
result = _resolve_verify()
|
||||
assert isinstance(result, ssl.SSLContext), (
|
||||
f"Expected ssl.SSLContext from HERMES_CA_BUNDLE env var, got {type(result).__name__}"
|
||||
)
|
||||
|
||||
def test_resolve_verify_returns_ssl_context_from_ssl_cert_file_env(self, monkeypatch):
|
||||
"""SSLContext is returned when SSL_CERT_FILE env var is set."""
|
||||
monkeypatch.delenv("HERMES_CA_BUNDLE", raising=False)
|
||||
monkeypatch.setenv("SSL_CERT_FILE", DEFAULT_CA_FILE)
|
||||
|
||||
result = _resolve_verify()
|
||||
assert isinstance(result, ssl.SSLContext), (
|
||||
f"Expected ssl.SSLContext from SSL_CERT_FILE env var, got {type(result).__name__}"
|
||||
)
|
||||
Reference in New Issue
Block a user