Skip to content

Commit 524598a

Browse files
authored
Port diagnostic for certs with ephemeral keys on Windows to S.N.Q (#97819)
* Add diagnostic for ephemeral certificate keys * Deduplicate some test code * Add Client-side test
1 parent 4fa9eb8 commit 524598a

12 files changed

+129
-90
lines changed

src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static partial class Certificates
4242
private static X509Certificate2Collection s_dynamicCaCertificates;
4343
private static object certLock = new object();
4444

45-
45+
4646
// These Get* methods make a copy of the certificates so that consumers own the lifetime of the
4747
// certificates handed back. Consumers are expected to dispose of their certs when done with them.
4848

@@ -99,7 +99,7 @@ public static void CleanupCertificates([CallerMemberName] string? testName = nul
9999
catch { };
100100
}
101101

102-
private static X509ExtensionCollection BuildTlsServerCertExtensions(string serverName)
102+
internal static X509ExtensionCollection BuildTlsServerCertExtensions(string serverName)
103103
{
104104
return BuildTlsCertExtensions(serverName, true);
105105
}
@@ -120,7 +120,7 @@ private static X509ExtensionCollection BuildTlsCertExtensions(string targetName,
120120
return extensions;
121121
}
122122

123-
public static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, [CallerMemberName] string? testName = null, bool longChain = false, bool serverCertificate = true)
123+
public static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, [CallerMemberName] string? testName = null, bool longChain = false, bool serverCertificate = true, bool ephemeralKey = false)
124124
{
125125
const int keySize = 2048;
126126
if (PlatformDetection.IsWindows && testName != null)
@@ -161,10 +161,10 @@ public static (X509Certificate2 certificate, X509Certificate2Collection) Generat
161161
responder.Dispose();
162162
root.Dispose();
163163

164-
if (PlatformDetection.IsWindows)
164+
if (!ephemeralKey && PlatformDetection.IsWindows)
165165
{
166166
X509Certificate2 ephemeral = endEntity;
167-
endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx));
167+
endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
168168
ephemeral.Dispose();
169169
}
170170

src/libraries/System.Net.Quic/src/Resources/Strings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -233,5 +233,8 @@
233233
<data name="net_quic_callback_error" xml:space="preserve">
234234
<value>User configured callback failed.</value>
235235
</data>
236+
<data name="net_auth_ephemeral" xml:space="preserve">
237+
<value>Authentication failed because the platform does not support ephemeral keys.</value>
238+
</data>
236239
</root>
237240

src/libraries/System.Net.Quic/src/System.Net.Quic.csproj

+12
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@
6565
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.certificates_types.cs" Link="Common\Interop\Windows\Crypt32\Interop.certificates_types.cs" />
6666
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertEnumCertificatesInStore.cs" Link="Common\Interop\Windows\Crypt32\Interop.CertEnumCertificatesInStore.cs" />
6767
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.MsgEncodingType.cs" Link="Common\Interop\Windows\Crypt32\Interop.Interop.MsgEncodingType.cs" />
68+
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertContextPropId.cs"
69+
Link="Common\Interop\Windows\Crypt32\Interop.CertContextPropId.cs" />
70+
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertDuplicateCertificateContext.cs"
71+
Link="Common\Interop\Windows\Crypt32\Interop.CertDuplicateCertificateContex.cs" />
72+
<Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertGetCertificateContextProperty_NO_NULLABLE.cs"
73+
Link="Common\Interop\Windows\Crypt32\Interop.CertGetCertificateContextProperty_NO_NULLABLE.cs" />
74+
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeCrypt32Handle.cs"
75+
Link="Common\Microsoft\Win32\SafeHandles\SafeCrypt32Handle.cs" />
76+
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeHandleCache.cs"
77+
Link="Common\Microsoft\Win32\SafeHandles\SafeHandleCache.cs" />
78+
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeCertContextHandle.cs"
79+
Link="Common\Microsoft\Win32\SafeHandles\SafeCertContextHandle.cs" />
6880
<Compile Include="$(CommonPath)Interop\Windows\SChannel\Interop.SECURITY_STATUS.cs" Link="Common\Interop\Windows\SChannel\Interop.SECURITY_STATUS.cs" />
6981
<Compile Include="$(CommonPath)System\Net\Security\CertificateValidation.Windows.cs" Link="Common\System\Net\Security\CertificateValidation.Windows.cs" />
7082
<Compile Include="$(CommonPath)System\Net\SocketAddressPal.Windows.cs" Link="Common\System\Net\SocketAddressPal.Windows.cs" />

src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Generic;
55
using System.Collections.ObjectModel;
6+
using System.Security.Authentication;
67
using System.Net.Security;
78
using System.Security.Cryptography.X509Certificates;
89
using System.Threading;
@@ -253,6 +254,15 @@ private static unsafe MsQuicSafeHandle Create(QuicConnectionOptions options, QUI
253254
{
254255
ThrowHelper.ThrowIfMsQuicError(status, SR.net_quic_tls_version_notsupported);
255256
}
257+
258+
if (status == MsQuic.QUIC_STATUS_CERT_NO_CERT && certificate != null && certificate.HasPrivateKey())
259+
{
260+
using Microsoft.Win32.SafeHandles.SafeCertContextHandle safeCertContextHandle = Interop.Crypt32.CertDuplicateCertificateContext(certificate.Handle);
261+
if (safeCertContextHandle.HasEphemeralPrivateKey)
262+
{
263+
throw new AuthenticationException(SR.net_auth_ephemeral);
264+
}
265+
}
256266
#endif
257267

258268
ThrowHelper.ThrowIfMsQuicError(status, "ConfigurationLoadCredential failed");

src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs

+85
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,91 @@ public async Task ConnectWithClientCertificate(bool sendCertificate, ClientCertS
729729
await serverConnection.DisposeAsync();
730730
}
731731

732+
[Fact]
733+
[PlatformSpecific(TestPlatforms.Windows)]
734+
public async Task Server_CertificateWithEphemeralKey_Throws()
735+
{
736+
(X509Certificate2 serverCertificate, X509Certificate2Collection chain) = Configuration.Certificates.GenerateCertificates(nameof(Server_CertificateWithEphemeralKey_Throws), ephemeralKey: true);
737+
Configuration.Certificates.CleanupCertificates(nameof(Server_CertificateWithEphemeralKey_Throws));
738+
739+
try
740+
{
741+
QuicListenerOptions listenerOptions = new QuicListenerOptions()
742+
{
743+
ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0),
744+
ApplicationProtocols = new List<SslApplicationProtocol>() { ApplicationProtocol },
745+
ConnectionOptionsCallback = (_, _, _) =>
746+
{
747+
var serverOptions = CreateQuicServerOptions();
748+
serverOptions.ServerAuthenticationOptions.ServerCertificate = null;
749+
serverOptions.ServerAuthenticationOptions.ServerCertificateContext = SslStreamCertificateContext.Create(serverCertificate, chain);
750+
return ValueTask.FromResult(serverOptions);
751+
}
752+
};
753+
await using QuicListener listener = await CreateQuicListener(listenerOptions);
754+
755+
QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint);
756+
clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; };
757+
758+
// client connection attempt will fail
759+
await Assert.ThrowsAsync<AuthenticationException>(async () => await CreateQuicConnection(clientOptions));
760+
761+
// server-side failure will be reported from AcceptConnectionAsync
762+
AuthenticationException e = await Assert.ThrowsAsync<AuthenticationException>(async () => await listener.AcceptConnectionAsync());
763+
Assert.Contains("ephemeral", e.Message);
764+
}
765+
finally
766+
{
767+
Configuration.Certificates.CleanupCertificates(nameof(Server_CertificateWithEphemeralKey_Throws));
768+
serverCertificate.Dispose();
769+
foreach (X509Certificate c in chain)
770+
{
771+
c.Dispose();
772+
}
773+
}
774+
}
775+
776+
[Fact]
777+
[PlatformSpecific(TestPlatforms.Windows)]
778+
public async Task Client_CertificateWithEphemeralKey_Throws()
779+
{
780+
(X509Certificate2 clientCertificate, X509Certificate2Collection chain) = Configuration.Certificates.GenerateCertificates(nameof(Client_CertificateWithEphemeralKey_Throws), ephemeralKey: true);
781+
Configuration.Certificates.CleanupCertificates(nameof(Client_CertificateWithEphemeralKey_Throws));
782+
783+
try
784+
{
785+
QuicListenerOptions listenerOptions = new QuicListenerOptions()
786+
{
787+
ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0),
788+
ApplicationProtocols = new List<SslApplicationProtocol>() { ApplicationProtocol },
789+
ConnectionOptionsCallback = (_, _, _) =>
790+
{
791+
var serverOptions = CreateQuicServerOptions();
792+
serverOptions.ServerAuthenticationOptions.ClientCertificateRequired = true;
793+
serverOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; };
794+
return ValueTask.FromResult(serverOptions);
795+
}
796+
};
797+
await using QuicListener listener = await CreateQuicListener(listenerOptions);
798+
799+
QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint);
800+
clientOptions.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection() { clientCertificate };
801+
clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = delegate { return true; };
802+
803+
AuthenticationException e = await Assert.ThrowsAsync<AuthenticationException>(async () => await CreateQuicConnection(clientOptions));
804+
Assert.Contains("ephemeral", e.Message);
805+
}
806+
finally
807+
{
808+
Configuration.Certificates.CleanupCertificates(nameof(Client_CertificateWithEphemeralKey_Throws));
809+
clientCertificate.Dispose();
810+
foreach (X509Certificate c in chain)
811+
{
812+
c.Dispose();
813+
}
814+
}
815+
}
816+
732817
[Theory]
733818
[InlineData(false)]
734819
[InlineData(true)]

src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationRemoteServer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ private async Task ConnectWithRevocation_WithCallback_Core(
204204
intermediateAuthorityCount: noIntermediates ? 0 : 1,
205205
subjectName: serverName,
206206
keySize: 2048,
207-
extensions: TestHelper.BuildTlsServerCertExtensions(serverName));
207+
extensions: Configuration.Certificates.BuildTlsServerCertExtensions(serverName));
208208

209209
CertificateAuthority issuingAuthority = noIntermediates ? rootAuthority : intermediateAuthorities[0];
210210
X509Certificate2 issuerCert = issuingAuthority.CloneIssuerCert();

src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCertificateContextTests.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace System.Net.Security.Tests
1111
{
12+
using Configuration = System.Net.Test.Common.Configuration;
13+
1214
public static class SslStreamCertificateContextTests
1315
{
1416
[Fact]
@@ -27,7 +29,7 @@ public static async Task Create_OcspDoesNotReturnOrCacheInvalidStapleData()
2729
intermediateAuthorityCount: 1,
2830
subjectName: serverName,
2931
keySize: 2048,
30-
extensions: TestHelper.BuildTlsServerCertExtensions(serverName));
32+
extensions: Configuration.Certificates.BuildTlsServerCertExtensions(serverName));
3133

3234
using (responder)
3335
using (rootAuthority)

src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCertificateTrustTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class SslStreamCertificateTrustTest
2222
[SkipOnPlatform(TestPlatforms.Windows, "CertificateCollection-based SslCertificateTrust is not Supported on Windows")]
2323
public async Task SslStream_SendCertificateTrust_CertificateCollection()
2424
{
25-
(X509Certificate2 certificate, X509Certificate2Collection caCerts) = TestHelper.GenerateCertificates(nameof(SslStream_SendCertificateTrust_CertificateCollection));
25+
(X509Certificate2 certificate, X509Certificate2Collection caCerts) = Configuration.Certificates.GenerateCertificates(nameof(SslStream_SendCertificateTrust_CertificateCollection));
2626

2727
SslCertificateTrust trust = SslCertificateTrust.CreateForX509Collection(caCerts, sendTrustInHandshake: true);
2828
string[] acceptableIssuers = await ConnectAndGatherAcceptableIssuers(trust);
@@ -94,7 +94,7 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
9494
[PlatformSpecific(TestPlatforms.Windows)]
9595
public void SslStream_SendCertificateTrust_CertificateCollection_ThrowsOnWindows()
9696
{
97-
(X509Certificate2 certificate, X509Certificate2Collection caCerts) = TestHelper.GenerateCertificates(nameof(SslStream_SendCertificateTrust_CertificateCollection));
97+
(X509Certificate2 certificate, X509Certificate2Collection caCerts) = Configuration.Certificates.GenerateCertificates(nameof(SslStream_SendCertificateTrust_CertificateCollection));
9898

9999
Assert.Throws<PlatformNotSupportedException>(() => SslCertificateTrust.CreateForX509Collection(caCerts, sendTrustInHandshake: true));
100100
}
@@ -103,7 +103,7 @@ public void SslStream_SendCertificateTrust_CertificateCollection_ThrowsOnWindows
103103
[SkipOnPlatform(TestPlatforms.Windows, "Windows tested separately")]
104104
public void SslStream_SendCertificateTrust_ThrowsOnUnsupportedPlatform()
105105
{
106-
(X509Certificate2 certificate, X509Certificate2Collection caCerts) = TestHelper.GenerateCertificates(nameof(SslStream_SendCertificateTrust_CertificateCollection));
106+
(X509Certificate2 certificate, X509Certificate2Collection caCerts) = Configuration.Certificates.GenerateCertificates(nameof(SslStream_SendCertificateTrust_CertificateCollection));
107107

108108
using X509Store store = new X509Store("Root", StoreLocation.LocalMachine);
109109

src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class CertificateSetup : IDisposable
2828
public CertificateSetup()
2929
{
3030
TestHelper.CleanupCertificates(nameof(SslStreamNetworkStreamTest));
31-
(serverCert, serverChain) = TestHelper.GenerateCertificates("localhost", nameof(SslStreamNetworkStreamTest), longChain: true);
31+
(serverCert, serverChain) = Configuration.Certificates.GenerateCertificates("localhost", nameof(SslStreamNetworkStreamTest), longChain: true);
3232
}
3333

3434
public void Dispose()
@@ -863,7 +863,7 @@ public async Task SslStream_ClientCertificate_SendsChain()
863863
StoreName storeName = OperatingSystem.IsMacOS() ? StoreName.My : StoreName.CertificateAuthority;
864864
List<SslStream> streams = new List<SslStream>();
865865
TestHelper.CleanupCertificates(nameof(SslStream_ClientCertificate_SendsChain), storeName);
866-
(X509Certificate2 clientCertificate, X509Certificate2Collection clientChain) = TestHelper.GenerateCertificates(nameof(SslStream_ClientCertificate_SendsChain), serverCertificate: false);
866+
(X509Certificate2 clientCertificate, X509Certificate2Collection clientChain) = Configuration.Certificates.GenerateCertificates(nameof(SslStream_ClientCertificate_SendsChain), serverCertificate: false);
867867

868868
using (X509Store store = new X509Store(storeName, StoreLocation.CurrentUser))
869869
{
@@ -918,7 +918,7 @@ public async Task SslStream_ClientCertificate_SendsChain()
918918
[ActiveIssue("https://github.com/dotnet/runtime/issues/68206", TestPlatforms.Android)]
919919
public async Task SslStream_ClientCertificateContext_SendsChain()
920920
{
921-
(X509Certificate2 clientCertificate, X509Certificate2Collection clientChain) = TestHelper.GenerateCertificates(nameof(SslStream_ClientCertificateContext_SendsChain), serverCertificate: false);
921+
(X509Certificate2 clientCertificate, X509Certificate2Collection clientChain) = Configuration.Certificates.GenerateCertificates(nameof(SslStream_ClientCertificateContext_SendsChain), serverCertificate: false);
922922
TestHelper.CleanupCertificates(nameof(SslStream_ClientCertificateContext_SendsChain));
923923

924924
var clientOptions = new SslClientAuthenticationOptions()
@@ -942,7 +942,7 @@ public async Task SslStream_ClientCertificateContext_SendsChain()
942942
[PlatformSpecific(TestPlatforms.Windows)]
943943
public async Task SslStream_EphemeralKey_Throws()
944944
{
945-
(X509Certificate2 serverCertificate, X509Certificate2Collection chain) = TestHelper.GenerateCertificates(nameof(SslStream_EphemeralKey_Throws), ephemeralKey: true);
945+
(X509Certificate2 serverCertificate, X509Certificate2Collection chain) = Configuration.Certificates.GenerateCertificates(nameof(SslStream_EphemeralKey_Throws), ephemeralKey: true);
946946
TestHelper.CleanupCertificates(nameof(SslStream_EphemeralKey_Throws));
947947

948948
var clientOptions = new SslClientAuthenticationOptions()

src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamSniTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public async Task UnencodedHostName_ValidatesCertificate()
243243
string rawHostname = "räksmörgås.josefsson.org";
244244
string punycodeHostname = "xn--rksmrgs-5wao1o.josefsson.org";
245245

246-
var (serverCert, serverChain) = TestHelper.GenerateCertificates(punycodeHostname);
246+
var (serverCert, serverChain) = Configuration.Certificates.GenerateCertificates(punycodeHostname);
247247
try
248248
{
249249
SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions()

src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
Link="Common\System\Net\Configuration.Security.cs" />
6868
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.cs"
6969
Link="Common\System\Net\Configuration.Certificates.cs" />
70+
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.Dynamic.cs"
71+
Link="TestCommon\System\Net\Configuration.Certificates.Dynamic.cs" />
7072
<Compile Include="$(CommonTestPath)System\Net\Configuration.Http.cs"
7173
Link="Common\System\Net\Configuration.Http.cs" />
7274
<Compile Include="$(CommonTestPath)System\Net\HttpsTestClient.cs"

0 commit comments

Comments
 (0)