1

我开发了一个使用 TLS/SSL 进行通信的 C#、.NET4.5.2 客户端/服务器系统。证书是从文件中加载的。我使用“MakeCert”实用程序创建了证书文件以创建 .pvk 和 .cer 文件,然后使用“pvk2pfx”实用程序将它们组合成 .pfx。

要使用证书,我使用带有重载的 X509Certificate2 构造函数来加载它们,以将文件路径和密码作为字符串传递:

X509Certificate2(string filePath, string password)

我注意到随着时间的推移,证书的加载变得非常缓慢。我不确定是否存在使它们变慢的“事件”,或者是否是渐进的,但现在加载 PFX 文件大约需要 6 秒。加载 CER 文件没有问题,大约需要 0.1 秒。

我正在运行 Windows 8.1,问题仅在于我的用户在笔记本电脑上登录。

我编写了以下测试应用程序来验证问题:

    private const string filePath = @"c:\testcert.pfx";
    private const string password = "testpassword";
    static void Main(string[] args)
    {
        var stopwatch = new Stopwatch();
        try
        {
            Console.WriteLine("About to create certificate. Press Enter.");
            Console.ReadLine();

            stopwatch.Start();
            var cert = new X509Certificate(filePath, password);
            stopwatch.Stop();

            Console.WriteLine(stopwatch.Elapsed);
            Console.WriteLine("Certificate created. About to reset.  Press Enter.");
            Console.ReadLine();

            cert.Reset();

            Console.WriteLine("Certificate reset.  Press Enter.");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        Console.ReadLine();
    }

我请一些同事在他们的 PC 上运行该程序。我还尝试在虚拟机中运行,然后在我自己的笔记本电脑上设置一个新用户。在所有情况下,它在 ~0.1 秒内运行,但对于我的普通用户登录,它在 >6 秒内运行。

起初我没有在证书上调用“Reset()”,所以我认为某处的一些临时文件可能存在问题,所以我使用 procmon 来找出发生了什么。我发现在以下目录中创建了一些临时文件(尽管即使在没有调用 Reset() 的情况下,当应用程序退出时它们也会被整理):

C:\Users\<username>\AppData\Roaming\Microsoft\Crypto\RSA\<SID>

只是为了确保我已尝试删除此目录中的文件,但没有任何区别。

使用 procmon 我可以看到在加载证书期间文件/注册表活动中有 2 个间隙,这在负载快速的系统中不会发生。首先是在它尝试使用“dpapi.dll”之后。第二个是在读取以下 'C:\Extend\$UsnJrnl:$J:$DATA' 之后。DPAPI.dll 是 Windows 数据保护的接口。后一个文件是用于记录文件更改的 NTFS 的 USN 日志。我不是这方面的专家,我不确定两者是否相关!

然后我尝试使用 API Monitor http://www.rohitab.com/apimonitor来观察系统调用。同样,我不是专家,但我通过拖网查看暂停之前发生的事情。那里有很多我不明白可能相关或不相关的内容,我欢迎对其中任何内容发表评论以帮助关注问题。

2 秒间隔之前的最后一次调用是一个 memcpy,其调用堆栈如下:

#   Module  Address Offset  Location
1   RPCRT4.dll  0x74f6378b  0x2378b I_RpcSendReceive + 0x1bb
2   RPCRT4.dll  0x74f6367b  0x2367b I_RpcSendReceive + 0xab
3   RPCRT4.dll  0x74f594df  0x194df NdrServerInitializeNew + 0x83f
4   RPCRT4.dll  0x74f63619  0x23619 I_RpcSendReceive + 0x49
5   RPCRT4.dll  0x74f6398b  0x2398b NdrSendReceive + 0x2b

更高的可能有趣的线路似乎是:

#   Time of Day Thread  Module  API Return Value    Error   Duration
64922   6:38:44.348 AM  1   DPAPI.dll   SystemFunction040 ( 0x00ac5a30, 8, RTL_ENCRYPT_OPTION_SAME_LOGON )  STATUS_SUCCESS      0.0000402
64923   6:38:44.349 AM  1   CRYPTBASE.dll   RtlInitUnicodeString ( 0x0090e5a8, "\Device\KsecDD" )           0.0000004

64949   6:38:44.349 AM  1   RPCRT4.dll  RtlInitUnicodeString ( 0x0090e0b0, "\RPC Control\protected_storage" )           0.0000000

我发现很难跟踪调用堆栈,但我认为这些最终来自一个名为 CryptQueryObject 的函数。

我发现以下可能相关但不是 Windows8.1 的文章。我删除了 %windir%\Temp 文件夹以防万一,但也没有帮助。

https://support.microsoft.com/en-gb/kb/931908

我记得在某处发现一篇文章暗示延迟可能与来自 CryptQueryObject 的 ActiveDirectory 调用有关,但我找不到链接。

我真的在寻找:

  1. 如何修复我的用户登录,以便加载证书不需要 6 秒
  2. 如何确保我的代码没有问题,这样它就不会再次发生或发生在其他使用系统的人身上

谢谢你的帮助。

4

1 回答 1

1

这个问题现在已经解决了,我我明白为什么了!

当您使用私钥加载 X509Certificate 时,Windows 会将密钥存储到文件中并使用“数据保护”加密文件(因此我监控了对 DPAPI 的调用)。

https://technet.microsoft.com/en-us/library/cc962112.aspx

用于加密文件的密钥称为“主密钥”。它基于您的用户登录,并在您更改使用密码时更新。它也会在 90 天后自动过期。

http://www.passcape.com/index.php?section=docsys&cmd=details&id=28#33

如上面链接所述,主密钥存储在以下目录中:

%APPDATA%/Microsoft/Protect/%SID%

通过查看该目录中文件的“最后修改”日期,我可以看到我的 MasterKey 实际上早于 90 天。因此,每当我加载证书时,它都会尝试将私钥存储在加密文件中,并注意到主密钥已过期,因此它会尝试更新它。

我重新运行 ProcMon,但停止仅从我的测试应用程序过滤活动。然后我可以看到,在“间隙”期间,一个名为 lsass.exe 的进程对我们的 Active Directory 服务器进行了许多 UDP 调用,该进程与安全和密码更新有关(由于堆栈溢出,无法发布另一个链接限制但易于搜索)。

我认为尝试更新主密钥的过程必须涉及到 ActiveDirectory 服务的一些交互。

我远程工作并通过 VPN 连接到我的工作网络。VPN 不允许访问 ActiveDirectory 服务器,因此我认为延迟是由于尝试访问该服务器而失败造成的。

我等到下次去办公室,一旦我连接到网络并可以访问 ActiveDirectory 服务器,加载证书的延迟就消失了。我确认我的 MasterKey 文件也已更新并且不再过期。

我在通过 VPN 连接时重新测试了加载证书,但延迟仍然消失了,所以这似乎证实了只要主密钥没有过期就可以了。

不幸的是,由于无法访问 Active Directory 服务的同样问题,我无法通过 VPN 更改密码。

我的问题解决了。我很欣赏它可能相当独特,但我希望我的调查细节在某些时候能帮助其他人!

于 2016-08-03T07:22:39.697 回答