Skip to content

fix: async httpx TLS pinning via wrap_bio interception#60

Merged
jdrean merged 1 commit intomainfrom
jules/async
Feb 23, 2026
Merged

fix: async httpx TLS pinning via wrap_bio interception#60
jdrean merged 1 commit intomainfrom
jules/async

Conversation

@jdrean
Copy link
Member

@jdrean jdrean commented Feb 23, 2026

Summary by cubic

Fixes TLS pinning for httpx.AsyncClient by intercepting SSLContext.wrap_bio and verifying the peer cert’s public key fingerprint after handshake. Adds tests to ensure async pinning works and rejects mismatched hosts, matching the sync client behavior.

  • Bug Fixes

    • Pin async TLS by wrapping wrap_bio and checking the fingerprint after do_handshake.
    • Defer cert checks on SSLWantRead/Write until handshake completes.
    • Tests cover successful async connection and rejection of wrong hosts for both sync and async.
  • Refactors

    • Extracted _verify_peer_fingerprint helper used by both sync and async paths, with clear errors for missing or mismatched certs.

Written for commit a8a901e. Summary will update on new commits.

@jdrean jdrean merged commit 779a601 into main Feb 23, 2026
1 check passed
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/tinfoil/client.py">

<violation number="1" location="src/tinfoil/client.py:100">
P2: The `TLSBoundHTTPSHandler._get_connection` method still duplicates the fingerprint verification logic that `_verify_peer_fingerprint` was extracted to centralize. Consider refactoring `TLSBoundHTTPSHandler` to call `SecureClient._verify_peer_fingerprint(cert_binary, self.expected_pubkey)` (or promote the helper to a module-level function) so there's a single source of truth for cert pinning verification.</violation>

<violation number="2" location="src/tinfoil/client.py:145">
P2: Custom agent: **Check System Design and Architectural Patterns**

The async TLS pinning logic is inlined with deeply nested closures, breaking the separation-of-concerns pattern established by the sync path's `_create_socket_wrapper` helper. Extract a `_create_bio_wrapper(self, expected_fp)` method (parallel to `_create_socket_wrapper`) so the async pinning logic is independently testable, reusable, and consistent with the sync architecture. This also keeps `make_secure_async_http_client` at the same abstraction level as `make_secure_http_client`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

ctx = ssl.create_default_context()
original_wrap_bio = ctx.wrap_bio

def pinned_wrap_bio(*args, **kwargs):
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Check System Design and Architectural Patterns

The async TLS pinning logic is inlined with deeply nested closures, breaking the separation-of-concerns pattern established by the sync path's _create_socket_wrapper helper. Extract a _create_bio_wrapper(self, expected_fp) method (parallel to _create_socket_wrapper) so the async pinning logic is independently testable, reusable, and consistent with the sync architecture. This also keeps make_secure_async_http_client at the same abstraction level as make_secure_http_client.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tinfoil/client.py, line 145:

<comment>The async TLS pinning logic is inlined with deeply nested closures, breaking the separation-of-concerns pattern established by the sync path's `_create_socket_wrapper` helper. Extract a `_create_bio_wrapper(self, expected_fp)` method (parallel to `_create_socket_wrapper`) so the async pinning logic is independently testable, reusable, and consistent with the sync architecture. This also keeps `make_secure_async_http_client` at the same abstraction level as `make_secure_http_client`.</comment>

<file context>
@@ -114,28 +116,46 @@ def _create_socket_wrapper(self, expected_fp: str):
+        ctx = ssl.create_default_context()
+        original_wrap_bio = ctx.wrap_bio
+
+        def pinned_wrap_bio(*args, **kwargs):
+            ssl_object = original_wrap_bio(*args, **kwargs)
+            original_do_handshake = ssl_object.do_handshake
</file context>
Fix with Cubic

ctx.wrap_socket = wrap_socket
return httpx.Client(verify=ctx, follow_redirects=True)
@staticmethod
def _verify_peer_fingerprint(cert_binary: Optional[bytes], expected_fp: str) -> None:
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The TLSBoundHTTPSHandler._get_connection method still duplicates the fingerprint verification logic that _verify_peer_fingerprint was extracted to centralize. Consider refactoring TLSBoundHTTPSHandler to call SecureClient._verify_peer_fingerprint(cert_binary, self.expected_pubkey) (or promote the helper to a module-level function) so there's a single source of truth for cert pinning verification.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tinfoil/client.py, line 100:

<comment>The `TLSBoundHTTPSHandler._get_connection` method still duplicates the fingerprint verification logic that `_verify_peer_fingerprint` was extracted to centralize. Consider refactoring `TLSBoundHTTPSHandler` to call `SecureClient._verify_peer_fingerprint(cert_binary, self.expected_pubkey)` (or promote the helper to a module-level function) so there's a single source of truth for cert pinning verification.</comment>

<file context>
@@ -96,16 +96,18 @@ def ground_truth(self) -> Optional[GroundTruth]:
-        ctx.wrap_socket = wrap_socket
-        return httpx.Client(verify=ctx, follow_redirects=True)
+    @staticmethod
+    def _verify_peer_fingerprint(cert_binary: Optional[bytes], expected_fp: str) -> None:
+        """Verify that a certificate's public key fingerprint matches the expected value."""
+        if not cert_binary:
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant