Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ada2c85
Add low-level TLS state machine API (TlsContext / TlsSession)
wfurt Jul 8, 2026
51c03bc
TlsSession: address PR #130366 review feedback
wfurt Jul 8, 2026
a6674a7
TlsSession: additional PR #130366 review fixes
wfurt Jul 8, 2026
125f64e
Merge remote-tracking branch 'upstream/main' into TlsSession-clean
wfurt Jul 8, 2026
880ad1c
TlsSession: reconcile entrypoints.c/opensslshim.h with upstream EC OS…
wfurt Jul 9, 2026
f404a23
TlsSession: address remaining PR #130366 review comments
wfurt Jul 9, 2026
e10a826
TlsSession: replace manual buffer triads with ArrayBuffer
wfurt Jul 9, 2026
b72843b
TlsSession tests: fix two GetClientHelloBytes throw-tests optimized t…
wfurt Jul 9, 2026
257fe7f
TlsSession: give server-side SNI target host session-local backing
wfurt Jul 9, 2026
f1decc3
TlsSession: give CertificateContext session-local backing
wfurt Jul 9, 2026
b0e2212
TlsSession: small PR #130366 review fixes + resume-test threshold
wfurt Jul 9, 2026
a477056
TlsSession: fix per-session dispatch_semaphore leak in Apple CreateSe…
wfurt Jul 9, 2026
1bf3094
TlsSession: throw InvalidOperationException when Handshake is called …
wfurt Jul 9, 2026
c680c16
TlsSession: fix macOS SecureTransport external cert-validation resume
wfurt Jul 9, 2026
1081b4f
Merge remote-tracking branch 'upstream/main' into TlsSession-clean
wfurt Jul 11, 2026
6d8de10
TlsSession: broaden OpenSSL platform coverage; rename NotUnix stub
wfurt Jul 11, 2026
3163e66
TlsSession tests: fix Windows CI expectations
wfurt Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ The PR that reveals the implementation of the `<IncludeInternalObsoleteAttribute
| __`SYSLIB0062`__ | XSLT Script blocks are not supported. |
| __`SYSLIB0063`__ | This constructor has been deprecated and argument bool isConnected does not have any effect. Use NamedPipeClientStream(PipeDirection direction, bool isAsync, SafePipeHandle safePipeHandle) instead. |
| __`SYSLIB0064`__ | RSACryptoServiceProvider.Encrypt and RSACryptoServiceProvider.Decrypt methods that take a Boolean are obsolete. Use the overload that accepts RSAEncryptionPadding instead. |
| __`SYSLIB0065`__ | Setting AsnEncodedData.RawData is obsolete. Use CopyFrom instead. |

Comment thread
wfurt marked this conversation as resolved.
## Analyzer Warnings

Expand Down Expand Up @@ -320,3 +319,4 @@ Diagnostic id values for experimental APIs must not be recycled, as that could s
| __`SYSLIB5004`__ | .NET 9 | TBD | `X86Base.DivRem` is experimental since performance is not as optimized as `T.DivRem` |
| __`SYSLIB5005`__ | .NET 9 | .NET 10 | `System.Formats.Nrbf` is experimental |
| __`SYSLIB5006`__ | .NET 10 | TBD | Types for Post-Quantum Cryptography (PQC) are experimental. |
| __`SYSLIB5007`__ | .NET 10 | TBD | Low-level TLS engine types (`TlsContext`, `TlsSession`) in `System.Net.Security` are experimental. |
Comment thread
wfurt marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal static unsafe partial bool Init(

// Create a new connection context
[LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionCreate", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial SafeNwHandle NwConnectionCreate([MarshalAs(UnmanagedType.I4)] bool isServer, IntPtr context, string targetName, byte* alpnBuffer, int alpnLength, SslProtocols minTlsProtocol, SslProtocols maxTlsProtocol, uint* cipherSuites, int cipherSuitesLength);
internal static unsafe partial SafeNwHandle NwConnectionCreate([MarshalAs(UnmanagedType.I4)] bool isServer, IntPtr context, string targetName, byte* alpnBuffer, int alpnLength, SslProtocols minTlsProtocol, SslProtocols maxTlsProtocol, uint* cipherSuites, int cipherSuitesLength, IntPtr serverIdentity);

// Start the TLS handshake, notifications are received via the status callback (potentially from a different thread).
[LibraryImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_NwConnectionStart")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,13 @@ private static SslProtocols CalculateEffectiveProtocols(SslAuthenticationOptions
return protocols;
}

internal static SafeSslContextHandle GetOrCreateSslContextHandle(SslAuthenticationOptions sslAuthenticationOptions, bool allowCached)
internal static SafeSslContextHandle GetOrCreateSslContextHandle(SslAuthenticationOptions sslAuthenticationOptions, bool allowCached, bool enableResume)
{
SslProtocols protocols = CalculateEffectiveProtocols(sslAuthenticationOptions);

if (!allowCached)
{
return AllocateSslContext(sslAuthenticationOptions, protocols, allowCached);
return AllocateSslContext(sslAuthenticationOptions, protocols, enableResume);
}

bool hasAlpn = sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0;
Expand All @@ -208,9 +208,9 @@ internal static SafeSslContextHandle GetOrCreateSslContextHandle(SslAuthenticati
sslAuthenticationOptions.CertificateContext);
return s_sslContexts.GetOrCreate(key, static (args) =>
{
var (sslAuthOptions, protocols, allowCached) = args;
return AllocateSslContext(sslAuthOptions, protocols, allowCached);
}, (sslAuthenticationOptions, protocols, allowCached));
var (sslAuthOptions, protocols, enableResume) = args;
return AllocateSslContext(sslAuthOptions, protocols, enableResume);
}, (sslAuthenticationOptions, protocols, enableResume));
}

// This essentially wraps SSL_CTX* aka SSL_CTX_new + setting
Expand Down Expand Up @@ -361,7 +361,16 @@ internal static void UpdateClientCertificate(SafeSslHandle ssl, SslAuthenticatio
internal static unsafe SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuthenticationOptions)
{
SafeSslHandle? sslHandle = null;
bool cacheSslContext = sslAuthenticationOptions.AllowTlsResume && !LocalAppContextSwitches.DisableTlsResume && sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption && sslAuthenticationOptions.CipherSuitesPolicy == null;
// When a TlsContext owns a long-lived SSL_CTX (set via PreallocatedSslContext)
// we bypass the global SslContextCacheKey lookup and the TLS-resume cache hung
// off it: the TlsContext is the resume scope. The handle is borrowed here, not
// owned, so the conditional Dispose() in the finally below skips it.
SafeSslContextHandle? preallocatedSslCtx = sslAuthenticationOptions.PreallocatedSslContext;
bool cacheSslContext = preallocatedSslCtx is null
&& sslAuthenticationOptions.AllowTlsResume
&& !LocalAppContextSwitches.DisableTlsResume
&& sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption
&& sslAuthenticationOptions.CipherSuitesPolicy == null;

if (cacheSslContext)
{
Expand Down Expand Up @@ -398,9 +407,13 @@ internal static unsafe SafeSslHandle AllocateSslHandle(SslAuthenticationOptions
// For uncached SafeSslContextHandles, the handle will be disposed and closed.
// Cached SafeSslContextHandles are returned with increaset rent count so that
// Dispose() here will not close the handle.
using SafeSslContextHandle sslCtxHandle = GetOrCreateSslContextHandle(sslAuthenticationOptions, cacheSslContext);

sslHandle = SafeSslHandle.Create(sslCtxHandle, sslAuthenticationOptions);
// When a preallocated SSL_CTX is provided (TlsContext-owned), we borrow it
// for the duration of this method without disposing — the TlsContext keeps
// it alive across every TlsSession it produces.
SafeSslContextHandle sslCtxHandle = preallocatedSslCtx ?? GetOrCreateSslContextHandle(sslAuthenticationOptions, cacheSslContext, cacheSslContext);
try
Comment thread
wfurt marked this conversation as resolved.
{
sslHandle = SafeSslHandle.Create(sslCtxHandle, sslAuthenticationOptions);
Debug.Assert(sslHandle != null, "Expected non-null return value from SafeSslHandle.Create");
if (sslHandle.IsInvalid)
{
Expand Down Expand Up @@ -524,6 +537,14 @@ internal static unsafe SafeSslHandle AllocateSslHandle(SslAuthenticationOptions
}
}
}
}
finally
{
if (preallocatedSslCtx is null)
{
sslCtxHandle.Dispose();
}
}

return sslHandle;
}
Expand Down Expand Up @@ -735,6 +756,15 @@ internal static unsafe SecurityStatusPalErrorCode DoSslHandshake(SafeSslHandle c
return SecurityStatusPalErrorCode.CredentialsNeeded;
}

if (errorCode == Ssl.SslErrorCode.SSL_ERROR_WANT_RETRY_VERIFY)
{
// OpenSSL 3.0+ retry-verify: the certificate verification
// callback paused the handshake. The application owns
// certificate validation and must resume the handshake
// (by calling DoSslHandshake again) once it has a verdict.
return SecurityStatusPalErrorCode.CertValidationNeeded;
}

if (errorCode == Ssl.SslErrorCode.SSL_ERROR_SSL && context.CertificateValidationException is Exception ex)
{
// Clear the OpenSSL error queue since we are using our own
Expand Down Expand Up @@ -1007,7 +1037,29 @@ internal static int CertVerifyCallback(IntPtr storeCtx, IntPtr arg)
.TryGetTarget(out SslAuthenticationOptions? options);
Debug.Assert(options != null, "Expected to get SslAuthenticationOptions from GCHandle");

sslHandle = (SafeSslHandle)options!.SslStream!._securityContext!;
sslHandle = options!.SafeSslHandle as SafeSslHandle;
Debug.Assert(sslHandle is not null, "Expected SslAuthenticationOptions.SafeSslHandle to be set by SafeSslHandle.Create");

// No in-callback validator (TlsSession path): accept the certificate here so
// the TLS handshake completes, then surface the peer cert to the caller via
// NeedsCertificateValidation on the next ProcessHandshake. Any subsequent
// Encrypt/Decrypt blocks until the caller posts a verdict.
//
// Ideally we would pause the handshake via SSL_set_retry_verify on OpenSSL 3.0+
// so a caller's reject can emit a fatal TLS alert to the peer mid-handshake.
// In practice SSL_set_retry_verify is not honored for peer-cert verification
// on either client or server SSLs in current upstream OpenSSL (the callback is
// not re-entered after SSL_do_handshake resumes). The native shim
// CryptoNative_SslSetRetryVerify, the SafeSslHandle.RetryVerifyAttempted /
// ExternalValidationAccepted fields, and TlsSession's PushExternalValidation-
// VerdictToPalIfRetryVerify are kept in place as dormant infrastructure; once
// upstream OpenSSL honors retry-verify, gate the SSL_set_retry_verify path in
// this branch behind a version check (or feature probe) to opt into it.
if (options.RemoteCertificateValidator is null)
{
Ssl.X509StoreCtxSetError(storeCtx, (int)Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_OK);
return 1;
}

// We need to note the number of certs in ExtraStore that were
// provided (by the user), we will add more from the received peer
Expand All @@ -1021,7 +1073,9 @@ internal static int CertVerifyCallback(IntPtr storeCtx, IntPtr arg)
try
{
ProtocolToken alertToken = default;
if (options.SslStream!.VerifyRemoteCertificate(certificate, chain, options.CertificateContext?.Trust, ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus))
SslAuthenticationOptions.VerifyRemoteCertificateCallback? validator = options.RemoteCertificateValidator;
Debug.Assert(validator is not null, "Expected SslAuthenticationOptions.RemoteCertificateValidator to be set by SslStream or TlsSession");
if (validator!(certificate, chain, options.CertificateContext?.Trust, ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus))
{
Ssl.X509StoreCtxSetError(storeCtx, (int)Interop.Crypto.X509VerifyStatusCodeUniversal.X509_V_OK);
return 1;
Expand Down
Loading
Loading