[API Proposal]: Make SslStream's NegotiatedApplicationProtocol and all overloads of AuthenticateAs* virtual to allow for pluggable TLS implementations
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background and motivation
Performing mTLS with HSMs, smart cards, or cloud key stores (e.g., Azure Key Vault) where private keys are non-extractable is currently difficult and platform-dependent in .NET:
- Windows (Schannel): Schannel does not coordinate TLS cipher suite negotiation with hardware key capabilities in many cases.
- Linux (OpenSSL): While an OpenSSL provider can technically be loaded via P/Invoke to use a custom provider, it is non-portable (OpenSSL is only used by the runtime on Linux), very complex, and managing OSSL_LIB_CTX teardowns safely in .NET is nearly impossible.
Consequently, using third-party TLS engines (e.g., BouncyCastle, BoringSSL, Rustls, or GnuTLS) is a necessity for these scenarios.
Since https://github.com/dotnet/runtime/issues/42455 was resolved, it has become possible to subclass SslStream to plug in custom TLS implementations to use in http requests using SocketsHttpHandler and HttpClient when using HTTP 1.1. It is only possible in HTTP 1.1 because NegotiatedApplicationProtocol is not virtual, and will throw if read, such as happens in HttpConnectionPool.Http2. Additionally, the overloads of AuthenticateAs* that take SslClientAuthenticationOptions are also not virtual, which is not a problem for the HttpClient use case, but prevent a full custom implementation of SslStream.
Other AuthenticateAs* methods are already virtual, and call this non-virtual, but public overload.
### API Proposal
```csharp
namespace System.Net.Security;
public partial class SslStream
{
public virtual SslApplicationProtocol NegotiatedApplicationProtocol { /* ... */ }
public virtual void AuthenticateAsClient(SslClientAuthenticationOptions sslClientAuthenticationOptions) { /* ... */ }
public virtual Task AuthenticateAsClientAsync(SslClientAuthenticationOptions sslClientAuthenticationOptions, CancellationToken cancellationToken = default) { /* ... */ }
public virtual void AuthenticateAsServer(SslServerAuthenticationOptions sslServerAuthenticationOptions) { /* ... */ }
public virtual Task AuthenticateAsServerAsync(SslServerAuthenticationOptions sslServerAuthenticationOptions, CancellationToken cancellationToken = default) { /* ... */ }
}
```
### API Usage
```csharp
var handler = new SocketsHttpHandler
{
ConnectCallback = async (context, cancellationToken) =>
{
var rawStream = await ConnectSocketAsync(context.DnsEndPoint, cancellationToken);
var customStream = new CustomHsmTlsStream(rawStream);
await customStream.AuthenticateAsClientAsync(/* ... */, cancellationToken);
return customStream;
}
};
```
### Alternative Designs
A perhaps better, but more difficult approach would be to extract the relevant portions of SslStream for SocketsHttpHandler use to an interface that SslStream would implement, and change the SslStream casts in HttpConnectionPool to use this new interface instead. This would help custom TLS implementations to not instantiate heavy SslStream internal state, which will be unused, and to not need to implement irrelevant methods, such as AuthenticateAsServer when used for a client.
### Risks
Binary/Source Compatibility: 100% compatible. Converting non-virtual members to virtual in C# is a non-breaking change.
Execution Risk: Minimal. Existing SslStream callers experience zero changes; custom streams gain the ability to participate in HTTP/2 negotiation and options-based handshakes.
Contributor guide
Research direction
Start by locating SslStream's existing virtual AuthenticateAs* methods and the members referenced by HttpConnectionPool, then review how HTTP/2 reads NegotiatedApplicationProtocol. Done means the proposed options-based overloads and property are virtual, allowing a custom SslStream to participate in SocketsHttpHandler HTTP/2 connections without the current exception.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend-api-design, networking, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100