Skip to content

Scope the NIOSSL mTLS identity to its origin - #21

Merged
o-nnerb merged 5 commits into
releasefrom
claude/nio-identity-per-origin-release
Oct 7, 2026
Merged

o-nnerb merged 5 commits into
releasefrom
claude/nio-identity-per-origin-release

Conversation

@o-nnerb

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

Copy link
Copy Markdown
Member

Same change as #20 (draft against main), applied to release: a per-origin tlsLocalIdentityProviderNIOSSL so an mTLS identity is not presented to every server the client connects to, including redirect targets. See #20 for the problem statement and design.

Summary

  • New tlsLocalIdentityProviderNIOSSL: (@Sendable (_ host: String, _ port: Int) -> NIOSSLClientIdentity?)?. When set it is the only source of the client identity: its answer replaces certificateChain/privateKey of the TLS configuration used for the connection (client-level or request-level), at both NIOSSL sites (direct and proxy tunnel). Without a provider nothing changes, so existing users of the 1.38.x/1.39.x tags are unaffected; adopting it is a caller-side opt-in.
  • Shares ConnectionPool.Key.origin with the Network.framework provider from Scope the Network.framework mTLS identity to its origin #19; that helper now takes the origin.

Dependencies

Stacked on #19 (the Network.framework counterpart): this branch is that one plus the single cherry-picked commit. Land #19 first. Only conflict on the cherry-pick was the factory call site, where release also passes customVerification; both arguments kept.

Tests

LocalIdentityNIOSSLTests (11) + LocalIdentityNetworkFrameworkTests (7) + TrustCustomVerificationTests (6): 24 tests pass. Redirect to another host does not receive the identity; a nil answer beats an identity in tlsConfiguration and in a request's own TLS configuration; behaviour without a provider is unchanged. Full suite on this branch: the only failures are the two testConnectTimeout, which also fail on a clean main (see #18).

Proxy tunnel: three tests go through a simulated CONNECT proxy that terminates TLS and demands a client certificate: the provider is asked for the destination origin (not the proxy), and the identity is presented or withheld accordingly. Removing the tunnel call site makes two of them fail.

🤖 Generated with Claude Code

o-nnerb and others added 5 commits October 7, 2026 14:38
`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>
Setting `certificateChain`/`privateKey` on `tlsConfiguration` (or on a request's
own TLS configuration, which redirects preserve) presents that identity to every
server the client connects to, including the target of a redirect to a different
host. URLSession and browsers choose the certificate per challenge/origin.

Add `tlsLocalIdentityProviderNIOSSL`, 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). When it is set, it is the only source of the client identity: its
answer replaces the certificate chain and private key of the TLS configuration
used for the connection, for both direct and proxy-tunnelled connections. Without
a provider behaviour is unchanged.

Share the origin computation with the Network.framework provider through
`ConnectionPool.Key.origin`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The proxy-tunnel TLS setup is a separate call site from the direct-connection
one. Cover that the provider is asked for the destination origin (not the proxy)
and that the identity it chooses is, or is not, presented through the tunnel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y-per-origin-release

# Conflicts:
#	Sources/AsyncHTTPClient/ConnectionPool.swift
#	Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift
#	Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift
#	Tests/AsyncHTTPClientTests/LocalIdentityNetworkFrameworkTests.swift
…OSSL's doc comment

TLSConfiguration is NIOSSL's type, so DocC can't resolve it as a symbol link of this module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@o-nnerb o-nnerb added the 🆕 semver/minor Additive, non-breaking API change label Oct 7, 2026
@o-nnerb
o-nnerb merged commit 0fa2490 into release Oct 7, 2026
39 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Additive, non-breaking API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant