Skip to content

Scope the Network.framework mTLS identity to its origin - #19

Merged
o-nnerb merged 1 commit into
releasefrom
claude/mtls-identity-per-request-release
Oct 7, 2026
Merged

o-nnerb merged 1 commit into
releasefrom
claude/mtls-identity-per-request-release

Conversation

@o-nnerb

@o-nnerb o-nnerb commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Same change as #18 (draft against main), applied to release, which already contains the mTLS identity hook (#12) and the trust-verification hooks; this is the single fix commit cherry-picked on top of release.

Problem

HTTPClient.Configuration.tlsLocalIdentityNetworkFramework is one client-wide identity. On the Network.framework (NIOTS) backend every connection presented it to any server that requested a client certificate — including the target of a redirect to a different host. URLSession and browsers choose the certificate per challenge/origin; only the NIOTS path leaked.

Change

  • New tlsLocalIdentityProviderNetworkFramework: (@Sendable (_ host: String, _ port: Int) -> SecIdentity?)?, asked for the origin each connection is opened to (nil = present none). Connections are per origin, so a redirect to another host asks again with that host.
  • Host is the one named in the URL (SNI override under a DNS override); IPv6 brackets stripped; unix sockets never consult the provider.
  • The provider takes precedence over tlsLocalIdentityNetworkFramework, which is kept for source compatibility (it ships in the 1.38.x/1.39.x tags request-dl-nio pins) and now documents that it is not origin-scoped. Adopting the provider is a caller-side change; nothing breaks for existing users.
  • Only conflict on the cherry-pick: the factory call site, which on release also passes customVerification. Both arguments are kept.

Tests

LocalIdentityNetworkFrameworkTests + TrustCustomVerificationTests: 13 tests, 0 failures, 0 skipped. The redirect-to-another-host test fails if the scoping is removed. The identity tests now build the SecIdentity from an in-memory PKCS#12 import (macOS 15 / iOS 18+, otherwise XCTSkip) instead of a Keychain round-trip that never worked here, so they actually run.

Not covered: .nio (NIOSSL) still has one client-wide certificateChain/privateKey; separate change. I ran the targeted suites on this branch, not the full suite. Note that testConnectTimeout (HTTPClientTests and AsyncAwaitEndToEndTests) fails locally with "connection reset by peer", also on a clean main, so it is pre-existing and unrelated.

🤖 Generated with Claude Code

`tlsLocalIdentityNetworkFramework` is a single client-wide identity, so every
connection opened by a client that uses Network.framework presented it to any
server that asked for a client certificate, including the target of a redirect
to a different host. Apple's own URLSession (and browsers) choose the
certificate per challenge/origin instead.

Add `tlsLocalIdentityProviderNetworkFramework`, a closure that receives the host
and port of the origin a connection is opened to and returns the identity to
present (or nil for none). Connections are per origin, so a redirect to another
host asks the provider again with that host and an identity meant for the
original host is never sent to it. The provider takes precedence over the
unscoped property, which is kept for source compatibility and documented as
not origin-scoped.

The tests build the SecIdentity from an in-memory PKCS#12 bundle
(kSecImportToMemoryOnly) rather than a Keychain round-trip, which failed to find
the key it had just added and made every identity test skip, and configure the
client not to verify the self-signed test server so the handshake can complete.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@o-nnerb
o-nnerb merged commit f8b2639 into release Oct 7, 2026
38 of 39 checks passed
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