conftest.py: superset_container fixture supports {encrypted_key: True}
- Writes encrypted private key to container
- Decrypts with openssl rsa -passin env:SSL_KEY_PASSPHRASE
- Starts Flask/Werkzeug TLS server with decrypted key
- Falls back to unencrypted key for existing tests
test_superset_tls_custom_ca.py:
- NEW: TestEncryptedPrivateKeySuperset class
- test_superset_health_over_tls_with_encrypted_key
- test_openssl_capath_encrypted_key
- Uses parametrize({tls: True, encrypted_key: True})
Validates full production flow: encrypted key → SSL_KEY_PASSPHRASE
→ openssl decrypt → TLS handshake → client trust via capath.
603 lines
26 KiB
Python
603 lines
26 KiB
Python
# #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 superset-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 superset-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.\nstdout: {result.stdout[:2000]}\nstderr: {result.stderr[:2000]}"
|
|
code = int(match.group(1))
|
|
assert code == 0, f"Expected verify code 0, got {code}.\nFull 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.\nstdout: {result.stdout[:2000]}\nstderr: {result.stderr[:2000]}"
|
|
code = int(match.group(1))
|
|
assert code != 0, f"Expected non-zero verify code with certifi CA bundle, got 0.\nThis means certifi somehow trusts our generated CA, which should not happen.\nFull 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, httpx.ConnectError)):
|
|
await client.get(f"{superset_url}/health")
|
|
|
|
asyncio.run(_test())
|
|
|
|
# #endregion test_httpx_certifi_fails
|
|
|
|
# ── superset-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})
|
|
|
|
# get_dashboards returns (count, items) tuple
|
|
assert isinstance(result, (dict, tuple)), f"Expected dict or tuple, got {type(result)}"
|
|
if isinstance(result, tuple):
|
|
assert len(result) == 2, f"Expected (count, items), got {result}"
|
|
else:
|
|
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, superset_container, 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
|
|
|
|
|
|
# #region TestEncryptedPrivateKeySuperset [C:3] [TYPE Class] [SEMANTICS test,tls,ssl,encrypted-key,passphrase,superset]
|
|
# @BRIEF Integration test for Superset container with passphrase-protected
|
|
# private key, decrypted via SSL_KEY_PASSPHRASE env var.
|
|
# Validates the production flow: encrypted key → openssl decrypt → TLS handshake.
|
|
# @RELATION BINDS_TO -> [superset_container]
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
# @RELATION BINDS_TO -> [install_custom_ca]
|
|
# @RELATION BINDS_TO -> [ADR-0009]
|
|
# @RATIONALE Corporate PKI often distributes passphrase-protected keys.
|
|
# superset-tools entrypoint decrypts these with SSL_KEY_PASSPHRASE.
|
|
# This test validates the full pipeline: encrypted key in container →
|
|
# openssl decrypt via env var → Flask/Werkzeug TLS server → client trust.
|
|
@pytest.mark.parametrize("superset_container", [{"tls": True, "encrypted_key": True}], indirect=True)
|
|
class TestEncryptedPrivateKeySuperset:
|
|
"""Superset container with encrypted private key + SSL_KEY_PASSPHRASE."""
|
|
|
|
def test_superset_health_over_tls_with_encrypted_key(self, superset_url, install_custom_ca):
|
|
"""Health endpoint must respond 200 OK over HTTPS with encrypted key."""
|
|
import asyncio
|
|
import httpx
|
|
|
|
ca_verify_path = install_custom_ca["ca_verify_path"]
|
|
ctx = ssl.create_default_context(capath=ca_verify_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())
|
|
|
|
def test_openssl_capath_encrypted_key(self, superset_url, install_custom_ca):
|
|
"""openssl s_client -CApath must succeed with encrypted key container."""
|
|
ca_verify_path = install_custom_ca["ca_verify_path"]
|
|
parsed = urlparse(superset_url)
|
|
host = parsed.hostname
|
|
port = parsed.port or 443
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"openssl",
|
|
"s_client",
|
|
"-connect",
|
|
f"{host}:{port}",
|
|
"-servername",
|
|
host,
|
|
"-CApath",
|
|
ca_verify_path,
|
|
"-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, "No Verify return code in openssl output"
|
|
assert int(match.group(1)) == 0, f"Expected verify code 0 with encrypted key container, got {match.group(1)}"
|
|
|
|
|
|
# #endregion TestEncryptedPrivateKeySuperset
|
|
|
|
|
|
# #region TestEncryptedPrivateKey [C:3] [TYPE Class] [SEMANTICS test,tls,ssl,encrypted-key,passphrase]
|
|
# @BRIEF Integration tests for passphrase-protected (encrypted) private keys.
|
|
# Uses openssl CLI + ssl.SSLContext instead of Superset container — lighter, faster,
|
|
# and exercises the same certificate decryption code path.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
# @RELATION BINDS_TO -> [install_custom_ca]
|
|
# @RELATION BINDS_TO -> [ADR-0009]
|
|
# @RATIONALE Corporate PKI often distributes passphrase-encrypted private keys
|
|
# (PEM with DEK-Info headers). These tests verify that:
|
|
# - ssl.SSLContext.load_cert_chain(password=...) supports encrypted keys
|
|
# - openssl s_server -pass successfully decrypts and serves TLS
|
|
# - full TLS handshake succeeds with encrypted key + correct passphrase
|
|
# - wrong/missing passphrase is correctly rejected
|
|
# @REQUIRES openssl CLI available in PATH.
|
|
class TestEncryptedPrivateKey:
|
|
"""Integration tests for passphrase-protected (encrypted) private keys."""
|
|
|
|
# #region test_ssl_context_loads_encrypted_key [C:2] [TYPE Function]
|
|
# @BRIEF ssl.SSLContext.load_cert_chain() with encrypted key + correct password succeeds.
|
|
# @POST SSL context loaded; no exception raised.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
def test_ssl_context_loads_encrypted_key(self, ca_chain, tmp_path):
|
|
"""Load an encrypted private key into ssl.SSLContext with correct passphrase."""
|
|
cert_file = tmp_path / "server.crt"
|
|
key_file = tmp_path / "server.key"
|
|
cert_file.write_text(ca_chain["server_crt"])
|
|
key_file.write_text(ca_chain["server_key_encrypted"])
|
|
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
ctx.load_cert_chain(
|
|
certfile=str(cert_file),
|
|
keyfile=str(key_file),
|
|
password=ca_chain["server_key_passphrase"],
|
|
)
|
|
# No exception raised — key loaded successfully
|
|
|
|
# #endregion test_ssl_context_loads_encrypted_key
|
|
|
|
# #region test_encrypted_key_wrong_passphrase_fails [C:2] [TYPE Function]
|
|
# @BRIEF SSLContext.load_cert_chain() with wrong passphrase raises SSLError.
|
|
# @POST ssl.SSLError raised.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
def test_encrypted_key_wrong_passphrase_fails(self, ca_chain, tmp_path):
|
|
"""Wrong passphrase must raise ssl.SSLError."""
|
|
cert_file = tmp_path / "server.crt"
|
|
key_file = tmp_path / "server.key"
|
|
cert_file.write_text(ca_chain["server_crt"])
|
|
key_file.write_text(ca_chain["server_key_encrypted"])
|
|
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
with pytest.raises(ssl.SSLError):
|
|
ctx.load_cert_chain(
|
|
certfile=str(cert_file),
|
|
keyfile=str(key_file),
|
|
password="wrong-passphrase-99999",
|
|
)
|
|
|
|
# #endregion test_encrypted_key_wrong_passphrase_fails
|
|
|
|
# #region test_encrypted_key_no_passphrase_fails [C:2] [TYPE Function]
|
|
# @BRIEF SSLContext.load_cert_chain() without password on encrypted key raises SSLError.
|
|
# @POST ssl.SSLError raised.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
def test_encrypted_key_no_passphrase_fails(self, ca_chain, tmp_path):
|
|
"""Encrypted key without password must raise ssl.SSLError."""
|
|
cert_file = tmp_path / "server.crt"
|
|
key_file = tmp_path / "server.key"
|
|
cert_file.write_text(ca_chain["server_crt"])
|
|
key_file.write_text(ca_chain["server_key_encrypted"])
|
|
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
# Note: Python raises OSError (not SSLError) when an encrypted key is
|
|
# loaded without a password and no interactive terminal is available.
|
|
with pytest.raises((ssl.SSLError, OSError)):
|
|
ctx.load_cert_chain(
|
|
certfile=str(cert_file),
|
|
keyfile=str(key_file),
|
|
# no password argument — should fail
|
|
)
|
|
|
|
# #endregion test_encrypted_key_no_passphrase_fails
|
|
|
|
# #region test_asyncio_server_encrypted_key_ok [C:3] [TYPE Function]
|
|
# @BRIEF Python asyncio SSL server with encrypted key + passphrase → TLS handshake succeeds.
|
|
# Uses ssl.SSLContext.load_cert_chain() — the same production code path.
|
|
# @POST Client successfully connects and receives HTTP 200 over TLS with full chain trust.
|
|
# @SIDE_EFFECT Starts/stops an asyncio TCP server on a local port; writes temp files.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
# @RELATION BINDS_TO -> [install_custom_ca]
|
|
# @RATIONALE Using asyncio.start_server with ssl.SSLContext instead of openssl s_server
|
|
# because openssl s_server does not reliably send intermediate CA certs in the chain
|
|
# on OpenSSL 3.x. Python's ssl.SSLContext.load_cert_chain() properly sends the full
|
|
# chain from a concatenated cert file, and this is also the production code path.
|
|
def test_asyncio_server_encrypted_key_ok(self, ca_chain, install_custom_ca):
|
|
"""Full TLS handshake with encrypted key + correct passphrase using Python asyncio server."""
|
|
import asyncio
|
|
import socket
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
ca_verify_path = install_custom_ca["ca_verify_path"]
|
|
|
|
async def _test():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Write fullchain (server cert + intermediate) and encrypted key
|
|
cert_path = Path(tmpdir) / "fullchain.pem"
|
|
key_path = Path(tmpdir) / "server.key"
|
|
cert_path.write_text(ca_chain["fullchain"])
|
|
key_path.write_text(ca_chain["server_key_encrypted"])
|
|
|
|
# Find a free port
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
port = s.getsockname()[1]
|
|
|
|
# Create SSL context with encrypted key + passphrase
|
|
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
server_ctx.load_cert_chain(
|
|
certfile=str(cert_path),
|
|
keyfile=str(key_path),
|
|
password=ca_chain["server_key_passphrase"],
|
|
)
|
|
|
|
# Simple responder handler
|
|
async def handle(reader, writer):
|
|
try:
|
|
await reader.read(1024)
|
|
writer.write(b"HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nOK")
|
|
await writer.drain()
|
|
finally:
|
|
writer.close()
|
|
|
|
# Start the server with async context manager
|
|
server = await asyncio.start_server(
|
|
handle,
|
|
host="127.0.0.1",
|
|
port=port,
|
|
ssl=server_ctx,
|
|
)
|
|
|
|
async with server:
|
|
# Create client SSL context with system CA (install_custom_ca)
|
|
client_ctx = ssl.create_default_context(capath=ca_verify_path)
|
|
|
|
# Connect and verify TLS handshake
|
|
reader, writer = await asyncio.open_connection(
|
|
"127.0.0.1",
|
|
port,
|
|
ssl=client_ctx,
|
|
)
|
|
|
|
writer.write(b"GET / HTTP/1.0\r\n\r\n")
|
|
await writer.drain()
|
|
|
|
response = await reader.read(2048)
|
|
assert b"200 OK" in response, f"Expected '200 OK' in response, got: {response[:200]}"
|
|
|
|
writer.close()
|
|
|
|
asyncio.run(_test())
|
|
|
|
# #endregion test_asyncio_server_encrypted_key_ok
|
|
|
|
# #region test_openssl_key_wrong_passphrase_fails [C:2] [TYPE Function]
|
|
# @BRIEF openssl rsa with wrong passphrase exits non-zero.
|
|
# @POST openssl exits with non-zero code.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
def test_openssl_key_wrong_passphrase_fails(self, ca_chain):
|
|
"""openssl rsa -check with wrong passphrase must exit non-zero."""
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
key_path = Path(tmpdir) / "server.key"
|
|
key_path.write_text(ca_chain["server_key_encrypted"])
|
|
|
|
result = subprocess.run(
|
|
[
|
|
"openssl",
|
|
"rsa",
|
|
"-in",
|
|
str(key_path),
|
|
"-check",
|
|
"-passin",
|
|
"pass:wrong-passphrase-99999",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
|
|
assert result.returncode != 0, f"Expected openssl rsa to fail with wrong passphrase, but it exited with code 0.\nstderr: {result.stderr[:2000]}"
|
|
|
|
# #endregion test_openssl_key_wrong_passphrase_fails
|
|
|
|
# #region test_openssl_key_no_passphrase_fails [C:2] [TYPE Function]
|
|
# @BRIEF openssl rsa without -passin on encrypted key exits non-zero.
|
|
# @POST openssl exits with non-zero code.
|
|
# @RELATION BINDS_TO -> [ca_chain]
|
|
def test_openssl_key_no_passphrase_fails(self, ca_chain):
|
|
"""openssl rsa -check without -passin on encrypted key must exit non-zero."""
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
key_path = Path(tmpdir) / "server.key"
|
|
key_path.write_text(ca_chain["server_key_encrypted"])
|
|
|
|
# Without -passin, openssl prompts for password on stdin.
|
|
# Use stdin=DEVNULL to simulate EOF — openssl will fail.
|
|
result = subprocess.run(
|
|
[
|
|
"openssl",
|
|
"rsa",
|
|
"-in",
|
|
str(key_path),
|
|
"-check",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
stdin=subprocess.DEVNULL,
|
|
)
|
|
|
|
assert result.returncode != 0, f"Expected openssl rsa to fail without passphrase, but it exited with code 0.\nstderr: {result.stderr[:2000]}"
|
|
|
|
# #endregion test_openssl_key_no_passphrase_fails
|
|
|
|
|
|
# #endregion TestEncryptedPrivateKey
|
|
# #endregion TestSupersetTlsCustomCA
|