Repository navigation
[API Proposal]: low level TLS machine #128871
Description
Activity
- addedapi-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementation
on Jun 1, 2026 - addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jun 1, 2026 dotnet-policy-service commented
on Jun 1, 2026 ContributorMore actionsTagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.- removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jun 2, 2026 - addedblockingMarks issues that we want to fast track in order to unblock other important workMarks issues that we want to fast track in order to unblock other important workapi-ready-for-reviewAPI is ready for review, it is NOT ready for implementationAPI is ready for review, it is NOT ready for implementationand removedapi-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementation
on Jun 30, 2026 - Changed TlsContext.Create to CreateClient and CreateServer
- Changed TlsSession to use constructors (default and SocketHandle)
- TlsContext no longer takes nullable server options
- Removed TlsContext.IsServer
- TlsOperationStatus.NeedsServerOptions => NeedsTlsContext
- TlsSession.SetServerContext => SetContext
- Split TlsSession into a socket-attached and a manual mode
- Should HasPendingOutput move to TlsBufferSession?
- Removed the span-returning GetClientHelloBytes, replaced it with a copying pattern.
- Renamed WantCredentials to CertificateRequested ("Requested" to account for the state where a server will accept null)
- Made SetClientCertificateContext nullable to account for the client saying no.
- Renamed WantRead/WantWrite to NeedMoreData and DestinationTooSmall (copying from OperationStatus)
namespace System.Net.Security; [Experimental("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public enum TlsOperationStatus { Complete = 0, DestinationTooSmall = 1, NeedMoreData = 2, Closed = 3, CertificateRequested = 4, NeedsCertificateValidation = 5, NeedsTlsContext = 6, } [Experimental("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public sealed class TlsContext : IDisposable { public static TlsContext CreateServer(SslServerAuthenticationOptions options); public static TlsContext CreateClient(SslClientAuthenticationOptions options); public void Dispose(); } [Experimental("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public sealed class TlsBufferSession : TlsSession { public TlsBufferSession(); public TlsOperationStatus Handshake( ReadOnlySpan<byte> source, Span<byte> destination, out int bytesConsumed, out int bytesWritten); public TlsOperationStatus Write( ReadOnlySpan<byte> source, Span<byte> destination, out int bytesConsumed, out int bytesWritten); public TlsOperationStatus Read( ReadOnlySpan<byte> source, Span<byte> destination, out int bytesConsumed, out int bytesWritten); public TlsOperationStatus Shutdown(Span<byte> destination, out int bytesWritten); public TlsOperationStatus DrainPendingOutput(Span<byte> destination, out int bytesWritten); public TlsOperationStatus RequestClientCertificate(Span<byte> destination, out int bytesWritten); } [Experimental("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public sealed class TlsSocketSession : TlsSession { public TlsSocketSession(SafeSocketHandle socket); public SafeSocketHandle Socket { get; } public TlsOperationStatus Handshake(); public TlsOperationStatus Read(Span<byte> buffer, out int bytesRead); public TlsOperationStatus Write(ReadOnlySpan<byte> buffer, out int bytesWritten); public TlsOperationStatus Shutdown(); public TlsOperationStatus RequestClientCertificate(); } [Experimental("SYSLIB5007", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] public abstract class TlsSession : IDisposable { private protected TlsSession(); public void SetContext(TlsContext context); public bool IsHandshakeComplete { get; } public bool HasPendingOutput { get; } public string? TargetHostName { get; set; } public SslClientHelloInfo? ClientHelloInfo { get; } public int GetClientHelloLength(); public bool TryGetClientHelloBytes(Span<byte> destination, out int bytesWritten); public SslProtocols NegotiatedProtocol { get; } [CLSCompliant(false)] public TlsCipherSuite NegotiatedCipherSuite { get; } public SslApplicationProtocol NegotiatedApplicationProtocol { get; } public X509Certificate2? LocalCertificate { get; } public SslPolicyErrors AcceptWithDefaultValidation(); public X509Certificate2? GetRemoteCertificate(); public X509Certificate2Collection? GetRemoteCertificates(); public void SetRemoteCertificateValidationResult(SslPolicyErrors errors); public void SetClientCertificateContext(SslStreamCertificateContext? context); public IReadOnlyList<string>? GetAcceptableIssuers(); public ChannelBinding? GetChannelBinding(ChannelBindingKind kind); public void Dispose(); }
Reacted by Korolev Dmitry- addedapi-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedand removedapi-ready-for-reviewAPI is ready for review, it is NOT ready for implementationAPI is ready for review, it is NOT ready for implementationblockingMarks issues that we want to fast track in order to unblock other important workMarks issues that we want to fast track in order to unblock other important work
on Jul 7, 2026 @bartonjs can you edit the approved API shape comment and rename the
ciphertextparameters todestination? Copilot agent review is tripping it and posting a comment about it each review cycle@rzikm Not sure what you're talking about, they all say "destination" that I see 😄 (aka "done!")
Reacted by Radek Zikmund- added a commit that references this issue
on Jul 23, 2026 Use-case - right now, I'm looking at a io_uring / IOCP / RIO driver with full TLS, including kTLS on io_uring for NIC/kernel offload.
So: pretty pretty please, make the handshake (user-space) separable from the bits I would then want to push down to kTLS via set-socket-option.
@DeagleGross knows what I'm working on here.
Reacted by Nikolay ZdravkovIn the newly designed
TlsSessionand correspondingTlsContext, is there a designed way to retrieve the client /server application traffic secrets?
I would like to use them to derive the QUIC keys. Would it make sense to create a separate API proposal for this?In the newly designed
TlsSessionand correspondingTlsContext, is there a designed way to retrieve the client /server application traffic secrets? I would like to use them to derive the QUIC keys. Would it make sense to create a separate API proposal for this?Retrieving keys is practically possible only on Linux. On Windows, it is possible only in one specific configuration that is only ever useful for QUIC (i.e. TLS 1.3 only and only unencrypted records are exchanged with Schannel, basically what MsQuic does internally). Extending TlsSession to allow implementing QUIC on top of it requires more than just exposing the keys (e.g. it requires sending custom TLS extension to negotiate QUIC transport parameters.), and I don't see us making this kind of investment since we already support QUIC via MsQuic.
@rzikm , it is less about QUIC this time, but more about exposing these APIs in a managed way on TlsSession and TlsContext. But I can understand if there are no such plans for now.
- locked and limited conversation to collaborators
on Sep 14, 2026
Background and motivation
Right now, SslStream is the only way how one can do TLS in .NET. It offers
Streaminterface and it is real legacy since .NET Framework, It works fine for majority cases but there are also cases when it does not fit that well. Examples may be inside of asp.net server where the consumer does not useStreambut it is forced to consume that interface. Another example may be various databases or custom protocol where all you want is to pump data from socket (sometimes synchronously) and decrypt and consume your own framing.The
SslStreamalso inherited various craft over the years and we need to drag some forward for compatibility reasons.This is mostly minimalists API driven by performance in some niche scenarios. While they may not be common, but the benefits still may be interesting. It is very similar to #127928 but there are some significant differences.
SslStreamand it would provide functional equivalent - something we discussed with @davidfowl while back.SslStreamhere the validation is externalized e.g. the state machine needs to be told if certificate should be trusted or now. One can still use standardX509Chainto do that but it is not forced into it. And AIA or certificate fetching should happen outside of the core loop.API Proposal
API Usage
TLS server over an
IDuplexPipeThe motivating sans-I/O case: a server that already owns its transport
(here a duplex pipe, but the shape is the same for
Memory<byte>queues,custom message framing, etc.).
SNI-aware server choosing options
The server creates a "bootstrap" context with
nulloptions. The firstProcessHandshakecall returnsNeedsServerOptions; the caller inspectsClientHelloInfoand resolves the right credential set, without anyasync callback or re-entrancy. This is the suspend-the-state-machine
equivalent of
ServerOptionsSelectionCallback, with full control of wherethe lookup happens (cache, KV store, certificate service, ...).
The full raw ClientHello record is also available via
s.GetClientHelloBytes()onevery server session — useful for JA3 fingerprinting, tenant routing, or audit logging.
The bytes are captured by default; the peek + parse + BIO-handoff overhead measured on
loopback is ~10-30 us per handshake and ~500 B. Anyone who wants the last ~1% can
disable capture via the
System.Net.Security.CaptureClientHelloAppContext switch(also readable from
DOTNET_SYSTEM_NET_SECURITY_CAPTURECLIENTHELLO=0); on serversessions with options supplied up front, that takes the SSL_set_fd fast path and
GetClientHelloBytes()throws.Peer-certificate inspection before accept
NeedsCertificateValidationlets the caller look at the peer certificatetogether with the negotiated parameters (
NegotiatedProtocol,NegotiatedCipherSuite,NegotiatedApplicationProtocol) before deciding,which is awkward with the current async callback because those properties
are not populated when the callback runs.
TLS 1.3 post-handshake client-cert request (server side)
Servers occasionally need to ask for a client certificate after the
handshake completes (e.g., a privileged operation behind an otherwise
anonymous endpoint). TLS 1.3 supports this via post-handshake
authentication and SChannel exposes
SEC_I_RENEGOTIATEfor the TLS 1.2equivalent.
RequestClientCertificateproduces the request record; thecaller writes it to the transport and the engine reports
NeedsCertificateValidationwhen the response arrives.Socket-bound non-blocking server
For callers who already own a non-blocking socket and would otherwise have
to build the byte-shuttling loop themselves, the socket-bound mode handles
it. The session owns the
SafeSocketHandle, soDisposecleans up the OShandle too.
Alternative Designs
#127928 is similar proposal but it is focusing primarily on specific use case. We could try to extend
SslStreambut theStreamsemantic is one of the things this proposal is trying to avoid.Alternatives considered for the deferred / SNI-callback server flow:
Null
TlsContextonTlsSession.Create— flatten the API by letting callers writeTlsSession.Create((TlsContext?)null, socket)instead of first creating a bootstrapTlsContext.Create((SslServerAuthenticationOptions?)null).Considered but not adopted. Loses reusable identity — the bootstrap
TlsContextis a first-class value that survives the process (or the listener), and once real per-tenantTlsContexts are added, sessions are steered onto them viaSetServerContext. Also introduces role ambiguity —TlsSession.Create(null, socket)doesn't say server vs client, requiring another argument or overload split. And it fragments the general pattern (all other constructions areTlsContext.Create -> TlsSession.Create(ctx)).Static shared bootstrap — a
TlsContext.DeferredServersingleton so callers avoid even the one bootstrap allocation.Open for API review. Safe now that per-session credential handles are properly session-local (both
SetServerContextandSetClientCertificateContextacquire session-local credentials, so multiple sessions on a shared bootstrap resolving to different tenants don't race on the bootstrap'sCredentialsHandle). Trade-off is the awkwardness of a staticIDisposable— the singleton can never be disposed. Not currently in the proposal.Named factory —
TlsContext.ForDeferredServer()(or similar) as sugar overTlsContext.Create((SslServerAuthenticationOptions?)null)to avoid the null-cast.Open for API review. Cleanest ergonomics for the SNI-dispatch use case without changing lifecycle semantics. One-line addition alongside the current overload.
Risks
The risk of the API itself is small - mostly around meeting the perf expectations. For maintainability I really want to change
SslStreamto work on top of this e.g. this becomes the internal PAL. With that, there is great potential for regressions if your test coverage is not good enough.To decrease risk, we are proposing to mark it as experimental for now. That would likely be removed in next LTS e.g. .NET 12