fix: async httpx TLS pinning via wrap_bio interception#60
Conversation
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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>
| 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: |
There was a problem hiding this comment.
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>
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
Refactors
Written for commit a8a901e. Summary will update on new commits.