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();
}