throw specific Authentication exception for ephemeral keys on Windows (#83436)
authorTomas Weinfurt <tweinfurt@yahoo.com>
Wed, 15 Mar 2023 19:31:38 +0000 (12:31 -0700)
committerGitHub <noreply@github.com>
Wed, 15 Mar 2023 19:31:38 +0000 (12:31 -0700)
* throw specific Authentication exception for ephemenral keys on Windows

* cleanup

* android

* Update src/libraries/System.Net.Security/src/Resources/Strings.resx

Co-authored-by: Kevin Jones <vcsjones@github.com>
---------

Co-authored-by: Kevin Jones <vcsjones@github.com>
src/libraries/System.Net.Security/src/Resources/Strings.resx
src/libraries/System.Net.Security/src/System.Net.Security.csproj
src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs
src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs
src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs

index 4bbcb9e..5d5d625 100644 (file)
   <data name="net_auth_alert" xml:space="preserve">
     <value>Authentication failed on the remote side (the stream might still be available for additional authentication attempts).</value>
   </data>
+  <data name="net_auth_ephemeral" xml:space="preserve">
+    <value>Authentication failed because the platform does not support ephemeral keys.</value>
+  </data>
   <data name="net_auth_message_not_encrypted" xml:space="preserve">
     <value>Protocol error: A received message contains a valid signature but it was not encrypted as required by the effective Protection Level.</value>
   </data>
index b594f3f..56bf536 100644 (file)
              Link="Common\Interop\Windows\Crypt32\Interop.CertEnumCertificatesInStore.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.MsgEncodingType.cs"
              Link="Common\Interop\Windows\Crypt32\Interop.Interop.MsgEncodingType.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertContextPropId.cs"
+             Link="Common\Interop\Windows\Crypt32\Interop.CertContextPropId.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertDuplicateCertificateContext.cs"
+             Link="Common\Interop\Windows\Crypt32\Interop.CertDuplicateCertificateContex.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Crypt32\Interop.CertGetCertificateContextProperty_NO_NULLABLE.cs"
+                 Link="Common\Interop\Windows\Crypt32\Interop.CertGetCertificateContextProperty_NO_NULLABLE.cs" />
+    <Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeCrypt32Handle.cs"
+             Link="Common\Microsoft\Win32\SafeHandles\SafeCrypt32Handle.cs" />
+    <Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeHandleCache.cs"
+                 Link="Common\Microsoft\Win32\SafeHandles\SafeHandleCache.cs" />
+    <Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeCertContextHandle.cs"
+             Link="Common\Microsoft\Win32\SafeHandles\SafeCertContextHandle.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.CloseHandle.cs"
              Link="Common\Interop\Windows\Kernel32\Interop.CloseHandle.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\SChannel\Interop.Alerts.cs"
index 6c4ab95..f1bd687 100644 (file)
@@ -181,6 +181,8 @@ namespace System.Net.Security
 
         public static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, bool newCredentialsRequested)
         {
+            SslStreamCertificateContext? certificateContext = sslAuthenticationOptions.CertificateContext;
+
             try
             {
                 EncryptionPolicy policy = sslAuthenticationOptions.EncryptionPolicy;
@@ -192,8 +194,6 @@ namespace System.Net.Security
                     AcquireCredentialsHandleSchCredentials(sslAuthenticationOptions);
 #pragma warning restore SYSLIB0040
 
-                SslStreamCertificateContext? certificateContext = sslAuthenticationOptions.CertificateContext;
-
                 if (certificateContext != null && certificateContext.Trust != null && certificateContext.Trust._sendTrustInHandshake)
                 {
                     AttachCertificateStore(cred, certificateContext.Trust._store!);
@@ -211,6 +211,13 @@ namespace System.Net.Security
 
                 return cred;
             }
+            catch (Win32Exception e) when (e.NativeErrorCode == (int)Interop.SECURITY_STATUS.NoCredentials && certificateContext != null)
+            {
+                Debug.Assert(certificateContext.Certificate.HasPrivateKey);
+                using SafeCertContextHandle safeCertContextHandle = Interop.Crypt32.CertDuplicateCertificateContext(certificateContext.Certificate.Handle);
+                // on Windows we do not support ephemeral keys.
+                throw new AuthenticationException(safeCertContextHandle.HasEphemeralPrivateKey ? SR.net_auth_ephemeral : SR.net_auth_SSPI, e);
+            }
             catch (Win32Exception e)
             {
                 throw new AuthenticationException(SR.net_auth_SSPI, e);
index 0c9a9ee..7e5ccca 100644 (file)
@@ -938,6 +938,43 @@ namespace System.Net.Security.Tests
             }
         }
 
+        [Fact]
+        [PlatformSpecific(TestPlatforms.Windows)]
+        public async Task SslStream_EphemeralKey_Throws()
+        {
+            (X509Certificate2 serverCertificate, X509Certificate2Collection chain) = TestHelper.GenerateCertificates(nameof(SslStream_EphemeralKey_Throws), ephemeralKey: true);
+            TestHelper.CleanupCertificates(nameof(SslStream_EphemeralKey_Throws));
+
+            var clientOptions = new SslClientAuthenticationOptions()
+            {
+                TargetHost = "localhost",
+                RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
+            };
+
+            var serverOptions = new SslServerAuthenticationOptions()
+            {
+                ServerCertificate = serverCertificate
+            };
+
+            (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams();
+
+            Task t1 = client.AuthenticateAsClientAsync(clientOptions, CancellationToken.None);
+            Task t2 = server.AuthenticateAsServerAsync(serverOptions, CancellationToken.None);
+
+            AuthenticationException e = await Assert.ThrowsAsync<AuthenticationException>(() => t2);
+            Assert.Contains("ephemeral", e.Message);
+            server.Dispose();
+            await Assert.ThrowsAsync<IOException>(() => t1);
+            client.Dispose();
+
+            TestHelper.CleanupCertificates(nameof(SslStream_EphemeralKey_Throws));
+            serverCertificate.Dispose();
+            foreach (X509Certificate c in chain)
+            {
+                c.Dispose();
+            }
+        }
+
         [Theory]
         [InlineData(16384 * 100, 4096, 1024, false)]
         [InlineData(16384 * 100, 4096, 1024, true)]
index fd0b91f..af73772 100644 (file)
@@ -162,7 +162,12 @@ namespace System.Net.Security.Tests
             return extensions;
         }
 
-        internal static (X509Certificate2 certificate, X509Certificate2Collection) GenerateCertificates(string targetName, [CallerMemberName] string? testName = null, bool longChain = false, bool serverCertificate = true)
+        internal 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)
@@ -203,7 +208,7 @@ namespace System.Net.Security.Tests
             responder.Dispose();
             root.Dispose();
 
-            if (PlatformDetection.IsWindows)
+            if (!ephemeralKey && PlatformDetection.IsWindows)
             {
                 X509Certificate2 ephemeral = endEntity;
                 endEntity = new X509Certificate2(endEntity.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);