首先有一些背景
调用SslStream.AuthenticateAsClient()发起TLS / SSL握手后,可以在下面的“ Windows安全性”对话框中向用户显示:
Windows安全性:此应用程序需要使用加密密钥
当同时满足以下两个原因时,就会发生这种情况:
客户端尝试连接到的SSL服务器请求客户端证书,这是TLS / SSL握手的一部分。
通过X509Certificate的第二个参数或通过SslStream.AuthenticateAsClient()回调(给定LocalCertificateSelectionCallback时)给出的constructing SslStream。 (对话框本身的外观会有所不同,具体取决于所谓的保护的“安全级别”。)
在Windows的更高版本(Win8和Win10)上,此对话框由名为svchost.exe调用的外部程序“ CredentialUIBroker.exe”显示。在早期版本中,它是由加载到正在运行的程序本身的dll呈现的:Win7中的comctl32.dll和WinXP中的cryptui.dll
问题
虽然此Windows安全性对话框看起来像是模式对话框,但其行为更像是没有设置所有者参数的模式对话框。
这将导致以下问题:
该对话框可以(并且经常)在正在运行的程序的窗口后面打开,从而使用户很难发现它自己。
用户可以通过单击正在运行的程序的其他窗口将对话框隐藏在后台,从而引起混乱。
对话框打开时,不会冻结正在运行的程序的其他窗口上的UI元素,用户可以自由执行其他操作。
因此,问题是:如何进行设置,以使Windows安全对话框显示为模式对话框?
在其他软件中看到的问题
Chrome遭受此问题的困扰,但尚未修复(Chrome 51)(错误轨道:strongly protected)
Internet Explorer不会遇到此问题。它将Windows安全性对话框显示为模式对话框。
Firefox是不适用的,因为它从未使用过Windows的证书存储,而是依靠其自己的存储。
复制代码
要显示该Windows安全性UI有点麻烦。
首先,它需要使用导入UI期间选中了“强保护”选项的证书。 (附带说明:使用的任何证书也应不可出口,因为仅适用于可出口证书的解决方案不适用于生产。)
下面的代码也需要服务器证书(任何没有强力保护的证书都可以),因为我们在伪造的TLS / SSL连接中使用SslStream.AuthenticateAsClientAsync()
。
此外,下面使用的FullDuplexPipeStream
是https://bugs.chromium.org/p/chromium/issues/detail?id=304152的基于FIFO queue的实现,由于很多样板代码,因此此处未包括在其中。
X509Certificate2 ServerCertificate = ...;
async Task Test(X509Certificate2 clientCertificate)
{
using (var serverStream = new FullDuplexPipeStream())
using (var clientStream = new FullDuplexPipeStream(serverStream))
using (var sslClientStream = new SslStream(clientStream, false,
(o, x509Certificate, chain, errors) => true,
(o, host, certificates, certificate, issuers) => clientCertificate))
using (var sslServerStream = new SslStream(serverStream, false,
(o, certificate, chain, errors) => true))
{
((Func<Task>)(async () =>
{
try
{
await sslServerStream.AuthenticateAsServerAsync(ServerCertificate,
true, SslProtocols.Tls, false);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}))();
await sslClientStream.AuthenticateAsClientAsync("foobar");
}
}
生成的代码在.Net 4.5+中工作,以重现“ Windows安全性”对话框。由于使用async / await,它在.Net 4.0中将无法工作。 (但是可以进行一些细微改动,将
AuthenticateAsServer()
挂接到其他线程上。)由于添加了Stream和RSACertificateExtensions.GetRSAPrivateKey(),在.Net 4.6中重现代码要容易得多:
clientCertificate.GetRSAPrivateKey()
.SignHash(new byte[20], HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1)
尽管此代码不再提及SSL,但我相当确定这与SslStream(或RSACng.SignHash())在后台进行的操作相同。
进一步的研究
我已经提到
RSACng.SignHash()
**会产生一个非常相似的对话框。似乎正在调用Win32函数Secure Channel。(**
RSACng
仅在.Net 4.6及更高版本中可用。.Net4.6之前可用的基于CAPI(CryptoAPI)的(?)RSA.SignHash()
显示了一个外观更传统的对话框。)看一下
NCryptSignHash()
的文档,有关于NCRYPT_SILENT_FLAG
标志的这个有趣的花絮:请求密钥服务提供者(KSP)不显示任何用户界面。如果提供者必须显示UI才能操作,则调用将失败,并且KSP应将
NTE_SILENT_CONTEXT
错误代码设置为最后一个错误。此外,NCryptSignHash()的
CRYPT_ACQUIRE_ WINDOWS_HANDLE_FLAG
标志的文档看起来很有希望:CSP或KSP所需的任何UI都是pvParameters参数中提供的
HWND
的子级。对于CSP密钥,使用此标志将导致对带有HCRYPTPROV的标志PP_CLIENT_HWND
的CryptSetProvParam函数使用该HWND
进行调用。对于KSP密钥,使用此标志将导致使用NULL
调用带有NCRYPT_WINDOW_HANDLE_PROPERTY
标志的NCryptSetProperty函数。不要将此标志与
HWND
一起使用。而且,我认为通过CryptAcquireCertificatePrivateKey设置的NCRYPT_WINDOW_HANDLE_PROPERTY属性是解决此问题的必要条件。
因此,对于可能的解决方案:任何以HWND或NCryptSetProperty()开头并且可能涉及P / Invoke的代码设置
CRYPT_ACQUIRE_SILENT_FLAG
都是可行的解决方案。理想情况下(至少对我而言),该代码也应在.Net 4.5中运行。
但是,我在.Net 4.5中找不到CNG的任何提及,因此我认为它很可能涉及P / Invoking。 NET 4.6中可能存在托管解决方案。
最佳答案
.NET 4.6中的修复(有点)实际上非常简单:
var rsa = (RSACng)clientCertificate.GetRSAPrivateKey();
rsa.Key.ParentWindowHandle = MyForm.Handle;
rsa.SignHash(new byte[20], HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
(从.Net 4.0开始,我实际上一直在研究此问题,谁知道.Net 4.6会如此简单!)
注意,这只是一个修复,因为它不能直接解决
SslStream.AuthenticateAsClient()
所示的UI的问题。但是,通过在SslStream.AuthenticateAsClient()
之前执行上述操作,CNG会缓存用户的授权,并且在TLS / SSL握手期间不会显示该对话框。(这就是为什么解决方案需要基于CNG而不是较早的CAPI的原因。)
此缓存的保留是通过组策略配置的,因此可能无法在所有环境中都起作用。
不幸的是,我仍然需要支持.Net 4.5,因此不胜感激。
关于c# - 在涉及SslStream.AuthenticateAsClient()的客户端证书期间设置“Windows安全性”对话框所有者,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37785820/