From 468456aa0e8b57a9cb3ac17155ce46b06a4d299f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:37:03 +0000 Subject: [PATCH 1/4] Initial plan From 47eedb2f1b877612033d1ce9998ce163d814f2ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:05:07 +0000 Subject: [PATCH 2/4] Backport PR #132832: Recognize SEC_E_UNKNOWN_CREDENTIALS in MsQuic credential loading Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com> --- .../src/Resources/Strings.resx | 3 ++ .../src/System.Net.Quic.csproj | 6 +++ .../Net/Quic/Internal/MsQuicConfiguration.cs | 12 ++++++ .../tests/FunctionalTests/MsQuicTests.cs | 39 +++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index cf02872f9118a5..08e4e31decff52 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -189,6 +189,9 @@ Could not use a TLS version required by Quic. TLS 1.3 may have been disabled in the registry. + + Authentication failed because the platform does not support ephemeral keys. + CipherSuitePolicy must specify at least one cipher supported by QUIC. diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 1acd4b592b23bd..222b4b84d45d47 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -46,6 +46,12 @@ + + + + + + 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 337884c61a5d3b..0810e54747a722 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 @@ -4,9 +4,11 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Net.Security; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using Microsoft.Quic; +using Microsoft.Win32.SafeHandles; using static Microsoft.Quic.MsQuic; namespace System.Net.Quic; @@ -206,6 +208,16 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI { ThrowHelper.ThrowIfMsQuicError(status, SR.net_quic_tls_version_notsupported); } + + if ((status == QUIC_STATUS_CERT_NO_CERT || (Interop.SECURITY_STATUS)status == Interop.SECURITY_STATUS.UnknownCredentials) && + certificate is not null && certificate.HasPrivateKey()) + { + using SafeCertContextHandle safeCertContextHandle = Interop.Crypt32.CertDuplicateCertificateContext(certificate.Handle); + if (safeCertContextHandle.HasEphemeralPrivateKey) + { + throw new AuthenticationException(SR.net_auth_ephemeral); + } + } #endif ThrowHelper.ThrowIfMsQuicError(status, "ConfigurationLoadCredential failed"); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 5627009287a209..bf531266556f07 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -1364,5 +1364,44 @@ private async Task SniTestCore(string hostname, bool shouldSendSni) [InlineData("fe80::9c3a:b64d:6249:1de8%2")] [InlineData("fe80::9c3a:b64d:6249:1de8")] public Task DoesNotSendIPAsSni(string target) => SniTestCore(target, false); + + [ConditionalFact] + [PlatformSpecific(TestPlatforms.Windows)] + public async Task Server_CertificateWithEphemeralKey_Throws() + { + using X509Certificate2 cert = System.Net.Test.Common.Configuration.Certificates.GetServerCertificate(); + using ECDsa key = ECDsa.Create(); + using X509Certificate2 ephemeralCert = cert.CopyWithPrivateKey(key); + + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + listenerOptions.ConnectionOptionsCallback = (_, _, _) => + { + QuicServerConnectionOptions serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificate = ephemeralCert; + return ValueTask.FromResult(serverOptions); + }; + + await Assert.ThrowsAsync(async () => await CreateQuicListener(listenerOptions)); + } + + [ConditionalFact] + [PlatformSpecific(TestPlatforms.Windows)] + public async Task Client_CertificateWithEphemeralKey_Throws() + { + if (PlatformDetection.IsWindows10Version20348OrLower) + { + throw new SkipTestException("Client certificates are not supported on Windows Server 2022."); + } + + using X509Certificate2 cert = System.Net.Test.Common.Configuration.Certificates.GetClientCertificate(); + using ECDsa key = ECDsa.Create(); + using X509Certificate2 ephemeralCert = cert.CopyWithPrivateKey(key); + + await using QuicListener listener = await CreateQuicListener(); + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + clientOptions.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection() { ephemeralCert }; + + await Assert.ThrowsAsync(async () => await CreateQuicConnection(clientOptions)); + } } } From 31831017ea7def1688d342cbcf1a9bdc5056915a Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:12:58 +0200 Subject: [PATCH 3/4] Fix tests --- .../tests/FunctionalTests/MsQuicTests.cs | 92 ++++++++++++++----- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index bf531266556f07..1cbc962e76c9a4 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -1365,43 +1365,89 @@ private async Task SniTestCore(string hostname, bool shouldSendSni) [InlineData("fe80::9c3a:b64d:6249:1de8")] public Task DoesNotSendIPAsSni(string target) => SniTestCore(target, false); - [ConditionalFact] + [Fact] [PlatformSpecific(TestPlatforms.Windows)] public async Task Server_CertificateWithEphemeralKey_Throws() { - using X509Certificate2 cert = System.Net.Test.Common.Configuration.Certificates.GetServerCertificate(); - using ECDsa key = ECDsa.Create(); - using X509Certificate2 ephemeralCert = cert.CopyWithPrivateKey(key); + (X509Certificate2 serverCertificate, X509Certificate2Collection chain) = Configuration.Certificates.GenerateCertificates(nameof(Server_CertificateWithEphemeralKey_Throws), ephemeralKey: true); + Configuration.Certificates.CleanupCertificates(nameof(Server_CertificateWithEphemeralKey_Throws)); - QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); - listenerOptions.ConnectionOptionsCallback = (_, _, _) => + try { - QuicServerConnectionOptions serverOptions = CreateQuicServerOptions(); - serverOptions.ServerAuthenticationOptions.ServerCertificate = ephemeralCert; - return ValueTask.FromResult(serverOptions); - }; + QuicListenerOptions listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificate = null; + serverOptions.ServerAuthenticationOptions.ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, chain); + return ValueTask.FromResult(serverOptions); + } + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; }; + + // client connection attempt will fail + await Assert.ThrowsAsync(async () => await CreateQuicConnection(clientOptions)); - await Assert.ThrowsAsync(async () => await CreateQuicListener(listenerOptions)); + // server-side failure will be reported from AcceptConnectionAsync + AuthenticationException e = await Assert.ThrowsAsync(async () => await listener.AcceptConnectionAsync()); + Assert.Contains("ephemeral", e.Message); + } + finally + { + Configuration.Certificates.CleanupCertificates(nameof(Server_CertificateWithEphemeralKey_Throws)); + serverCertificate.Dispose(); + foreach (X509Certificate c in chain) + { + c.Dispose(); + } + } } - [ConditionalFact] + [Fact] [PlatformSpecific(TestPlatforms.Windows)] public async Task Client_CertificateWithEphemeralKey_Throws() { - if (PlatformDetection.IsWindows10Version20348OrLower) - { - throw new SkipTestException("Client certificates are not supported on Windows Server 2022."); - } + (X509Certificate2 clientCertificate, X509Certificate2Collection chain) = Configuration.Certificates.GenerateCertificates(nameof(Client_CertificateWithEphemeralKey_Throws), ephemeralKey: true); + Configuration.Certificates.CleanupCertificates(nameof(Client_CertificateWithEphemeralKey_Throws)); - using X509Certificate2 cert = System.Net.Test.Common.Configuration.Certificates.GetClientCertificate(); - using ECDsa key = ECDsa.Create(); - using X509Certificate2 ephemeralCert = cert.CopyWithPrivateKey(key); + try + { + QuicListenerOptions listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ClientCertificateRequired = true; + serverOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; }; + return ValueTask.FromResult(serverOptions); + } + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); - await using QuicListener listener = await CreateQuicListener(); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); - clientOptions.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection() { ephemeralCert }; + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + clientOptions.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection() { clientCertificate }; + clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; }; - await Assert.ThrowsAsync(async () => await CreateQuicConnection(clientOptions)); + AuthenticationException e = await Assert.ThrowsAsync(async () => await CreateQuicConnection(clientOptions)); + Assert.Contains("ephemeral", e.Message); + } + finally + { + Configuration.Certificates.CleanupCertificates(nameof(Client_CertificateWithEphemeralKey_Throws)); + clientCertificate.Dispose(); + foreach (X509Certificate c in chain) + { + c.Dispose(); + } + } } } } From a44c06ab379382a5b78ea1b3c5371063b2e7369a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:50:22 +0000 Subject: [PATCH 4/4] Add ephemeral certificate helper overload Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com> --- .../tests/System/Net/Configuration.Certificates.Dynamic.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs b/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs index 6becbe26b1acf5..19a3afbd7b82ab 100644 --- a/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs +++ b/src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs @@ -120,7 +120,7 @@ private static X509ExtensionCollection BuildTlsCertExtensions(string targetName, return extensions; } - public static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, [CallerMemberName] string? testName = null, bool longChain = false, bool serverCertificate = true) + public static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, [CallerMemberName] string? testName = null, bool longChain = false, bool serverCertificate = true, bool ephemeralKey = false) { const int keySize = 2048; if (PlatformDetection.IsWindows && testName != null) @@ -161,10 +161,10 @@ public static (X509Certificate2 certificate, X509Certificate2Collection) Generat responder.Dispose(); root.Dispose(); - if (PlatformDetection.IsWindows) + if (!ephemeralKey && PlatformDetection.IsWindows) { X509Certificate2 ephemeral = endEntity; - endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx)); + endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable); ephemeral.Dispose(); }