From dc7dcb7fc49a95c8721428afbe2074f7b51bbc6b Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 24 Sep 2026 13:48:52 +0200 Subject: [PATCH 1/4] Disable implicit AIA fetching when creating Schannel Credentials. --- .../Interop/Windows/SspiCli/Interop.SSPI.cs | 4 ++- .../Net/Quic/Internal/MsQuicConfiguration.cs | 28 +++++++++++++++++-- .../Net/Security/SslStreamPal.Windows.cs | 8 +++--- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs index 1051e13fc4a201..33cdcc66ed4992 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -205,6 +205,7 @@ public enum Flags SCH_CRED_REVOCATION_CHECK_END_CERT = 0x100, SCH_CRED_IGNORE_NO_REVOCATION_CHECK = 0x800, SCH_CRED_IGNORE_REVOCATION_OFFLINE = 0x1000, + SCH_CRED_CACHE_ONLY_URL_RETRIEVAL = 0x8000, SCH_SEND_AUX_RECORD = 0x00200000, SCH_USE_STRONG_CRYPTO = 0x00400000, } @@ -246,7 +247,8 @@ public enum Flags SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT = 0x400, SCH_CRED_IGNORE_NO_REVOCATION_CHECK = 0x800, SCH_CRED_IGNORE_REVOCATION_OFFLINE = 0x1000, - SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE = 0x2000, + SCH_CRED_CACHE_ONLY_URL_RETRIEVAL = 0x8000, + SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE = 0x20000, SCH_SEND_ROOT_CERT = 0x40000, SCH_SEND_AUX_RECORD = 0x00200000, SCH_USE_STRONG_CRYPTO = 0x00400000, diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs index cd9ab5d74703aa..5640d78f979285 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs @@ -188,7 +188,15 @@ private static MsQuicConfigurationSafeHandle Create(QuicConnectionOptions option allowedCipherSuites = CipherSuitePolicyToFlags(cipherSuitesPolicy); } - if (!MsQuicApi.UsesSChannelBackend) + if (MsQuicApi.UsesSChannelBackend) + { + // Either user code passed SslStreamCertificateContext and + // intermediates have already been retrieved, or they will be + // retrieved by the cert context we build internally, there is no + // need for MsQuic to do another AIA fetch + flags |= QUIC_CREDENTIAL_FLAGS.CACHE_ONLY_URL_RETRIEVAL; + } + else { flags |= QUIC_CREDENTIAL_FLAGS.USE_PORTABLE_CERTIFICATES; } @@ -203,12 +211,13 @@ private static MsQuicConfigurationSafeHandle Create(QuicConnectionOptions option private static unsafe MsQuicConfigurationSafeHandle CreateInternal(QUIC_SETTINGS settings, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, ReadOnlyCollection? intermediates, List alpnProtocols, QUIC_ALLOWED_CIPHER_SUITE_FLAGS allowedCipherSuites) { - if (!MsQuicApi.UsesSChannelBackend && certificate is X509Certificate2 cert && intermediates is null) + SslStreamCertificateContext? context = null; + if (certificate is X509Certificate2 cert && intermediates is null) { // MsQuic will not lookup intermediates in local CA store if not explicitly provided, // so we build the cert context to get on feature parity with SslStream. Note that this code // path runs after the MsQuicConfigurationCache check. - SslStreamCertificateContext context = SslStreamCertificateContext.Create(cert, additionalCertificates: null, offline: true, trust: null); + context = SslStreamCertificateContext.Create(cert, additionalCertificates: null, offline: false); intermediates = context.IntermediateCertificates; } @@ -309,6 +318,19 @@ private static unsafe MsQuicConfigurationSafeHandle CreateInternal(QUIC_SETTINGS configurationHandle.Dispose(); throw; } + finally + { + if (context is not null) + { + // For Schannel, we pass reference only to the leaf, for + // OpenSSL, we serialize all certs, in either case we don't need + // to hold onto the intermediate certs references + foreach (X509Certificate2 cert in context.IntermediateCertificates) + { + cert.Dispose(); + } + } + } return configurationHandle; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 2d4d75b709fd0e..2920974ba50dc9 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -337,7 +337,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss X509Certificate2? certificate = authOptions.CertificateContext?.TargetCertificate; bool isServer = authOptions.IsServer; int protocolFlags = GetProtocolFlagsFromSslProtocols(authOptions.EnabledSslProtocols, isServer); - Interop.SspiCli.SCHANNEL_CRED.Flags flags; + Interop.SspiCli.SCHANNEL_CRED.Flags flags = Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_CACHE_ONLY_URL_RETRIEVAL; Interop.SspiCli.CredentialUse direction; bool allowTlsResume = authOptions.AllowTlsResume && !LocalAppContextSwitches.DisableTlsResume; @@ -384,7 +384,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss } #pragma warning restore SYSLIB0040 - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info($"flags=({flags}), ProtocolFlags=({protocolFlags}), EncryptionPolicy={policy}"); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"flags=({flags}), ProtocolFlags=({protocolFlags}), EncryptionPolicy={policy}"); Interop.SspiCli.SCHANNEL_CRED secureCredential = CreateSecureCredential( flags, protocolFlags, @@ -412,7 +412,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( X509Certificate2? certificate = authOptions.CertificateContext?.TargetCertificate; bool isServer = authOptions.IsServer; int protocolFlags = GetProtocolFlagsFromSslProtocols(authOptions.EnabledSslProtocols, isServer); - Interop.SspiCli.SCH_CREDENTIALS.Flags flags; + Interop.SspiCli.SCH_CREDENTIALS.Flags flags = Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_CACHE_ONLY_URL_RETRIEVAL; Interop.SspiCli.CredentialUse direction; bool allowTlsResume = authOptions.AllowTlsResume && !LocalAppContextSwitches.DisableTlsResume; @@ -485,7 +485,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( credential.paCred = &certificateHandle; } - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info($"flags=({flags}), ProtocolFlags=({protocolFlags}), EncryptionPolicy={policy}"); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"flags=({flags}), ProtocolFlags=({protocolFlags}), EncryptionPolicy={policy}"); Interop.SspiCli.TLS_PARAMETERS tlsParameters = default; credential.cTlsParameters = 1; From 7a54ebf05a3fb16d95d55050d0f023cd5988b521 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 24 Sep 2026 13:56:17 +0200 Subject: [PATCH 2/4] use the _ON_CREATE variant on SslStream --- .../Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs | 1 + .../src/System/Net/Security/SslStreamPal.Windows.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs index 33cdcc66ed4992..80538e31b243b2 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -206,6 +206,7 @@ public enum Flags SCH_CRED_IGNORE_NO_REVOCATION_CHECK = 0x800, SCH_CRED_IGNORE_REVOCATION_OFFLINE = 0x1000, SCH_CRED_CACHE_ONLY_URL_RETRIEVAL = 0x8000, + SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE = 0x20000, SCH_SEND_AUX_RECORD = 0x00200000, SCH_USE_STRONG_CRYPTO = 0x00400000, } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 2920974ba50dc9..0773522d4d7571 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -337,7 +337,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss X509Certificate2? certificate = authOptions.CertificateContext?.TargetCertificate; bool isServer = authOptions.IsServer; int protocolFlags = GetProtocolFlagsFromSslProtocols(authOptions.EnabledSslProtocols, isServer); - Interop.SspiCli.SCHANNEL_CRED.Flags flags = Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_CACHE_ONLY_URL_RETRIEVAL; + Interop.SspiCli.SCHANNEL_CRED.Flags flags = Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE; Interop.SspiCli.CredentialUse direction; bool allowTlsResume = authOptions.AllowTlsResume && !LocalAppContextSwitches.DisableTlsResume; @@ -412,7 +412,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( X509Certificate2? certificate = authOptions.CertificateContext?.TargetCertificate; bool isServer = authOptions.IsServer; int protocolFlags = GetProtocolFlagsFromSslProtocols(authOptions.EnabledSslProtocols, isServer); - Interop.SspiCli.SCH_CREDENTIALS.Flags flags = Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_CACHE_ONLY_URL_RETRIEVAL; + Interop.SspiCli.SCH_CREDENTIALS.Flags flags = Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE; Interop.SspiCli.CredentialUse direction; bool allowTlsResume = authOptions.AllowTlsResume && !LocalAppContextSwitches.DisableTlsResume; From 8f674a52cbdd80af90f44412ce7ce7ec1aaa2db2 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 24 Sep 2026 14:26:28 +0200 Subject: [PATCH 3/4] Fix applying flags --- .../src/System/Net/Security/SslStreamPal.Windows.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 0773522d4d7571..bcddbb8199c52b 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -345,7 +345,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss if (!isServer) { direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND; - flags = + flags |= Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_MANUAL_CRED_VALIDATION | Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_NO_DEFAULT_CREDS | Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_SEND_AUX_RECORD; @@ -362,7 +362,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss else { direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND; - flags = + flags |= Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_SEND_AUX_RECORD | Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_NO_SYSTEM_MAPPER; if (!allowTlsResume) @@ -420,7 +420,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( if (isServer) { direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND; - flags = + flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_SEND_AUX_RECORD | Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_NO_SYSTEM_MAPPER; if (!allowTlsResume) @@ -432,7 +432,7 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( else { direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND; - flags = + flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_MANUAL_CRED_VALIDATION | Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_NO_DEFAULT_CREDS | Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_SEND_AUX_RECORD; From 9368e42796fd4e1a0ed1dce201aa50e26a21eaf2 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 29 Sep 2026 10:31:58 +0200 Subject: [PATCH 4/4] Adress code review comments --- .../Net/Quic/Internal/MsQuicConfiguration.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs index 5640d78f979285..24215cf530f3bb 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; using System.Security.Authentication; using System.Net.Security; using System.Security.Cryptography.X509Certificates; @@ -211,16 +212,6 @@ private static MsQuicConfigurationSafeHandle Create(QuicConnectionOptions option private static unsafe MsQuicConfigurationSafeHandle CreateInternal(QUIC_SETTINGS settings, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, ReadOnlyCollection? intermediates, List alpnProtocols, QUIC_ALLOWED_CIPHER_SUITE_FLAGS allowedCipherSuites) { - SslStreamCertificateContext? context = null; - if (certificate is X509Certificate2 cert && intermediates is null) - { - // MsQuic will not lookup intermediates in local CA store if not explicitly provided, - // so we build the cert context to get on feature parity with SslStream. Note that this code - // path runs after the MsQuicConfigurationCache check. - context = SslStreamCertificateContext.Create(cert, additionalCertificates: null, offline: false); - intermediates = context.IntermediateCertificates; - } - QUIC_HANDLE* handle; using MsQuicBuffers msquicBuffers = new MsQuicBuffers(); @@ -236,8 +227,18 @@ private static unsafe MsQuicConfigurationSafeHandle CreateInternal(QUIC_SETTINGS "ConfigurationOpen failed"); MsQuicConfigurationSafeHandle configurationHandle = new MsQuicConfigurationSafeHandle(handle); + SslStreamCertificateContext? context = null; + try { + if (certificate is X509Certificate2 cert && intermediates is null) + { + // Build the local chain after the configuration cache check. Avoid OCSP fetches + // because we have no way to pass the staple to the MsQuic API anyway. + context = CreateCertificateContext(null, cert, additionalCertificates: null, offline: false, trust: null, noOcspFetch: true); + intermediates = context.IntermediateCertificates; + } + QUIC_CREDENTIAL_CONFIG config = new QUIC_CREDENTIAL_CONFIG { Flags = flags, @@ -322,19 +323,19 @@ private static unsafe MsQuicConfigurationSafeHandle CreateInternal(QUIC_SETTINGS { if (context is not null) { - // For Schannel, we pass reference only to the leaf, for - // OpenSSL, we serialize all certs, in either case we don't need - // to hold onto the intermediate certs references - foreach (X509Certificate2 cert in context.IntermediateCertificates) - { - cert.Dispose(); - } + ReleaseResources(context); } } return configurationHandle; } + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = nameof(SslStreamCertificateContext.Create))] + private static extern SslStreamCertificateContext CreateCertificateContext(SslStreamCertificateContext? context, X509Certificate2 target, X509Certificate2Collection? additionalCertificates, bool offline, SslCertificateTrust? trust, bool noOcspFetch); + + [UnsafeAccessor(UnsafeAccessorKind.Method)] + private static extern void ReleaseResources(SslStreamCertificateContext context); + private static QUIC_ALLOWED_CIPHER_SUITE_FLAGS CipherSuitePolicyToFlags(CipherSuitesPolicy cipherSuitesPolicy) { QUIC_ALLOWED_CIPHER_SUITE_FLAGS flags = QUIC_ALLOWED_CIPHER_SUITE_FLAGS.NONE;