Repository navigation
Conversation
|
@sethmlarson, could you look into this, please? |
|
Hi! Thanks for working on this. We've been chasing openai/openai-python#3777, and I wanted to bring back some concrete results: we tested this exact PR at We ran the PR source unchanged on Ubuntu 24.04, Python 3.12.3, and OpenSSL 3.0.13, with OpenAI 3.6.0, HTTPX2/httpcore2 2.12.0, anyio 4.14.2, and aiohttp 3.14.3. No monkeypatch or SSL-context workaround in these runs. Our local server sends a leaf and intermediate certificate, and the client trusts the root. One reused client makes 500 requests, all returning HTTP 200 with certificate and hostname verification enabled. The server closes each connection so we can exercise fresh handshakes quickly. Retries are off.
The arrows compare requests 101-200 with 401-500. Runs were sequential, and CPU includes the local server on the same event loop. Task counts stayed flat. The directory scans tell the same story. With 0.10.4, 50 successful requests caused 1,275 opens of We also checked that verification still does its job:
One regression case I'd suggest keeping is the valid chain with an intermediate supplied by the server. Our simpler leaf-to-root fixture missed the bug entirely. This matters for ordinary long-lived services: one of the SDK reports involves little more than a health check every 15 seconds. This is Linux validation of the submitted head, not a full cross-platform review. We haven't tested certificate-store changes during process lifetime or rebased the PR onto newer main. Anything else we can help test to get this over the line? We'd really like to give affected users a fixed dependency they can upgrade to. Appreciate the work here! Full reproducer and the HTTPX2 tracking issue: pydantic/httpx2#1252. |
|
I had Daybreak Blue do quick review and it thinks it's found two medium regressions. Listed with MREs in expanded detail sections: A reused context no longer sees a CA added to its default bundle. Main accepts the next request; this PR keeps rejecting it until CA-bundle refresh reproducer# /// script
# requires-python = ">=3.10"
# dependencies = [
# "trustme==1.2.1",
# "truststore @ git+https://github.com/sethmlarson/truststore.git@bd68d581c011d406daf5ddec7e21e38794772c47",
# ]
# ///
import http.client
import http.server
import os
import ssl
import tempfile
import threading
from pathlib import Path
import trustme
import truststore
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
def log_message(self, *_args):
pass
def request():
connection = http.client.HTTPSConnection(
"localhost",
server.server_port,
context=client,
timeout=5,
)
try:
connection.request("GET", "/")
return f"HTTP {connection.getresponse().status}"
except ssl.SSLCertVerificationError:
return "certificate rejected"
finally:
connection.close()
with tempfile.TemporaryDirectory() as directory:
bundle = Path(directory) / "roots.pem"
bundle.write_bytes(trustme.CA().cert_pem.bytes())
os.environ.update(SSL_CERT_FILE=str(bundle), SSL_CERT_DIR=directory)
ca = trustme.CA()
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ca.issue_cert("localhost").configure_cert(server_context)
client = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
with http.server.HTTPServer(("127.0.0.1", 0), Handler) as server:
server.socket = server_context.wrap_socket(server.socket, server_side=True)
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
print("Before update:", request())
with bundle.open("ab") as stream:
stream.write(ca.cert_pem.bytes())
print("After update:", request())
client.set_default_verify_paths()
print("After explicit reload:", request())
finally:
server.shutdown()$ uv run mre_bundle_refresh.py # PR revision pinned above
Before update: certificate rejected
After update: certificate rejected
After explicit reload: HTTP 200
$ uv run --with 'truststore @ git+https://github.com/sethmlarson/truststore.git@98195f0d27c3594899feb8d04a47f081959c6ba5' mre_bundle_refresh.py
Before update: certificate rejected
After update: HTTP 200
After explicit reload: HTTP 200The new module-level Reentrant setup reproducer# /// script
# requires-python = ">=3.10"
# dependencies = [
# "truststore @ git+https://github.com/sethmlarson/truststore.git@bd68d581c011d406daf5ddec7e21e38794772c47",
# ]
# ///
import os
import signal
import ssl
import sys
import tempfile
from pathlib import Path
import truststore
with tempfile.TemporaryDirectory() as ca_directory:
os.environ["SSL_CERT_FILE"] = str(Path(ca_directory) / "missing.pem")
os.environ["SSL_CERT_DIR"] = ca_directory
handled = []
def audit(event, args):
if event == "os.listdir" and args[0] == ca_directory and not handled:
handled.append(True)
print("Audit callback entered", flush=True)
nested = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
nested.wrap_bio(
ssl.MemoryBIO(), ssl.MemoryBIO(), server_hostname="localhost"
)
sys.addaudithook(audit)
signal.alarm(3)
context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO(), server_hostname="localhost")
signal.alarm(0)
print("TLS setup completed after the audit callback")The PR prints |
|
For those interested, I created an alternate implementation using a different mechanism to detect when to reload certificates: #244 |
Summary
SSLContextset_default_verify_paths()user-controlled and unchangedWhy
On OpenSSL 3.0-3.3, repeated automatic calls to
set_default_verify_paths()on the sameSSLContextcan append duplicate CApath lookups. Long-lived clients that reuse one context can then hit quadraticopendir(capath)growth during certificate verification.This change only deduplicates truststore's automatic pre-wrap configuration. If a caller explicitly invokes
SSLContext.set_default_verify_paths(), truststore still passes that through directly.Fixes #212.
Validation
PRE_COMMIT_HOME=/private/tmp/truststore-pre-commit-cache /private/tmp/truststore-pr-venv/bin/pre-commit run --all-filesCI=1 /private/tmp/truststore-pr-venv/bin/nox -s test -- tests/test_openssl.py tests/test_threading.pytests/test_threading.py::test_threadingskipped locally becausemkcertis not installedCI=1 /private/tmp/truststore-pr-venv/bin/nox -s test -- tests/test_api.py::test_trustme_cert tests/test_api.py::test_trustme_cert_loaded_via_capathpython3 -m compileall -qq -f src/truststore/ tests/test_openssl.py