如何枚举 Microsoft CSP 中所有已保存的 RSA 密钥?

信息安全 应用安全 密码学 源代码 视窗 密钥管理
2021-09-04 20:17:31

我有一个应用程序正在创建多个密钥并将它们存储在各种存储中(在本例中为机器存储)。

如何枚举给定 Windows 系统上的所有键?

      CspParameters cspParams = new CspParameters();
        cspParams.KeyContainerName = containerName + " " + g.ToString() ;
        cspParams.Flags = CspProviderFlags.UseMachineKeyStore;

        // Create a new RSA key and save it in the container.  This key will encrypt
        // a symmetric key, which will then be encryped in the XML document.
        RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider(cspParams);
        rsaKey.PersistKeyInCsp = true;
        Console.WriteLine(rsaKey.ToXmlString(false));
        string PublicKeyTest = rsaKey.ToXmlString(false);
4个回答

这些密钥存储在本文底部列出的位置。许多网络管理员不知道这些文件的用途,并且网络上的一些论坛帖子错误地建议人们删除这些文件。当然,此类操作的影响是特定于实现/应用程序的。我无法使用以下代码读取文件(可能需要进行一些更改)

  var files = System.IO.Directory.GetFiles(@"C:\ProgramData\Application Data\Microsoft\Crypto\RSA\MachineKeys\");

        foreach (var f in files)
        {           
            RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider();
            var readFile = File.OpenRead(  f.ToString());
            byte[] FileOut = new byte[readFile.Length];
            readFile.Read( FileOut, 0, (int)readFile.Length-1);
            rsaKey.ImportCspBlob(FileOut);

        }

似乎需要“用户状态迁移工具”工具才能将此数据从一台计算机移动到另一台计算机。此外,在这样的移动之后,一些工具需要将 CryptoAPI 中的密钥暴露给 CNG。

我不知道有什么方法可以查看containerNameCSP 中引用的相关文件。

Microsoft 旧版 CryptoAPI CSP 将私钥存储在以下目录中。

用户私有

%APPDATA%\Microsoft\Crypto\RSA\User SID\ %APPDATA%\Microsoft\Crypto\DSS\User SID\

本地系统私有

%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-18\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-18\

本地服务私有

%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-19\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-19\

网络服务私有

%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\S-1-5-20\ %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\S-1-5-20\

共享私人

%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\DSS\MachineKeys

CNG 将私钥存储在以下目录中。

用户私有
%APPDATA%\Microsoft\Crypto\Keys

本地系统私有 %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\SystemKeys

本地服务私有 %WINDIR%\ServiceProfiles\LocalService

网络服务专用 %WINDIR%\ServiceProfiles\NetworkService

共享私有 %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\Keys

参考:

http://msdn.microsoft.com/en-us/library/bb204778(v=vs.85).aspx

LDAP

如果启用凭据漫游,这些密钥也存储在 LDAP 中

ldifde.exe -s %LOGONSERVER% -f cscverify.ldf -r "(cn=USERNAME)" -l msPKIAccountCredentials,msPKIRoamingTimeStamp,msPKIDPAPIMasterKeys

将此命令中的 USERNAME 一词替换为凭证漫游不起作用的用户名。要确保已执行 Active Directory 复制,请在命令中使用 -s 选项并将 %LOGONSERVER% 替换为用户实际登录的服务器。确保 cscverify.ldf 文件显示导出属性的值。

LDAP 条目的大小由 DIMSRoarmingMaxNumTokens 和 DIMSRoamingMaxTokenSize 注册表项控制(来源

您可以仅使用 C# 枚举密钥容器,但您必须利用 P/Invoke 才能这样做。实际上,这是臭名昭著的KeyPal 实用程序所使用的方法。这是一个列出机器密钥容器名称的小 C# 应用程序。一旦有了名称,就可以利用CspParameters类来实例化与密钥容器对应的 RSA 密钥集。

感谢Pinvoke.net提供CryptAcquireContextCryptGetProvParamCryptReleaseContext的 P/Invoke 签名,以便利用 Windows CryptoAPI 所需的内容。

class Program
{
    static long CRYPT_MACHINE_KEYSET = 0x20;
    static long CRYPT_VERIFYCONTEXT = 0xF0000000;
    static uint CRYPT_FIRST = 1;
    static uint CRYPT_NEXT = 2;

    static uint PROV_RSA_FULL = 1;
    static uint PP_ENUMCONTAINERS = 2;

    [DllImport("advapi32.dll", SetLastError = true)]
    static extern bool CryptGetProvParam(
       IntPtr hProv,
       uint dwParam,
       [MarshalAs(UnmanagedType.LPStr)] StringBuilder pbData,
       ref uint dwDataLen,
       uint dwFlags);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool CryptAcquireContext(
        ref IntPtr hProv,
        string pszContainer,
        string pszProvider,
        uint dwProvType,
        uint dwFlags);

    [DllImport("advapi32.dll", EntryPoint = "CryptReleaseContext", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern bool CryptReleaseContext(
       IntPtr hProv,
       Int32 dwFlags);

    static void Main(string[] args)
    {
        Console.WriteLine("Key Container Names:");
        IEnumerable<string> keyContainerNames = GetKeyContainerNames();
        foreach (string name in keyContainerNames)
        {
            Console.WriteLine(name);
        }

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey();
    }

    public static IEnumerable<string> GetKeyContainerNames()
    {
        var keyContainerNameList = new List<string>();

        IntPtr hProv = IntPtr.Zero;
        uint flags = (uint)(CRYPT_MACHINE_KEYSET | CRYPT_VERIFYCONTEXT);
        if (CryptAcquireContext(ref hProv, null, null, PROV_RSA_FULL, flags) == false)
            throw new Exception("CryptAcquireContext");

        uint bufferLength = 2048;
        StringBuilder stringBuilder = new StringBuilder((int)bufferLength);
        if (CryptGetProvParam(hProv, PP_ENUMCONTAINERS, stringBuilder, ref bufferLength, CRYPT_FIRST) == false)
            return keyContainerNameList;

        keyContainerNameList.Add(stringBuilder.ToString());

        while (CryptGetProvParam(hProv, PP_ENUMCONTAINERS, stringBuilder, ref bufferLength, CRYPT_NEXT))
        {
            keyContainerNameList.Add(stringBuilder.ToString());
        }

        if (hProv != IntPtr.Zero)
        {
            CryptReleaseContext(hProv, 0);
        }

        return keyContainerNameList;
    }
}

一些CSP 允许枚举密钥容器。您必须使用本机代码(C,而不是 C#)来执行此操作,并将CryptGetProvParam()PP_ENUMCONTAINERS标志一起使用。代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <windows.h>
#include <wincrypt.h>

static void
usage(void)
{
        fprintf(stderr,
"Usage: enumkeys.exe [ csp ]\n");
        exit(EXIT_FAILURE);
}

static void
failerr(char *funName)
{
        fprintf(stderr, "%s() failed: 0x%08lX\n",
                funName, (unsigned)GetLastError());
        exit(EXIT_FAILURE);
}

int
main(int argc, char *argv[])
{
        int size;
        char *csp, buf[1024];
        HCRYPTPROV hprov;
        DWORD flags, buf_len;

        if (argc > 2) {
                usage();
        }
        if (argc > 1) {
                csp = argv[1];
        } else {
                csp = MS_STRONG_PROV_A;
        }
        hprov = 0;
        flags = CRYPT_VERIFYCONTEXT | CRYPT_MACHINE_KEYSET;
        if (!CryptAcquireContextA(&hprov, NULL, csp, PROV_RSA_FULL, flags)) {
                failerr("CryptAcquireContext");
        }
        buf_len = sizeof buf;
        if (!CryptGetProvParam(hprov, PP_ENUMCONTAINERS,
                buf, &buf_len, CRYPT_FIRST))
        {
                if (GetLastError() == ERROR_NO_MORE_ITEMS) {
                        printf("No container.\n");
                        exit(EXIT_SUCCESS);
                } else {
                        failerr("CryptGetProvParam");
                }
        }
        for (;;) {
                printf("Container: '%s'\n", buf);
                buf_len = sizeof buf;
                if (!CryptGetProvParam(hprov, PP_ENUMCONTAINERS,
                        buf, &buf_len, CRYPT_NEXT))
                {
                        if (GetLastError() == ERROR_NO_MORE_ITEMS) {
                                break;
                        }
                        failerr("CryptGetProvParam");
                }
        }
        CryptReleaseContext(hprov, 0);
        return EXIT_SUCCESS;
}

(此代码假设关键容器名称适合 1024 字节;这不是不合理的假设。)

对于每个密钥容器,您可能想要“打开”它并获取密钥类型和大小;可能完全导出公钥。这可以通过 .NET 代码完成(用于System.Security.Cryptography.CspParameters指定特定 CSP 上的特定密钥容器)。

重要提示:并非所有 CSP 都支持此类枚举。在某些情况下,现有密钥的集合是不明确的,例如,如果 CSP 动态地询问用户密码,并动态生成密钥对,其中 PRNG 来自容器名称和密码。对于这样的 CSP,“现有”密钥的数量(至少在 potentia 中)实际上是无限的,因此您将无法全部枚举它们。

与@ian 相同的答案,但作为 Powershell 脚本。在这个例子中读取机器键。

$containerPath = "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys"

Get-ChildItem $containerPath | ForEach-Object {
    try { 
        $bytes = [System.IO.File]::ReadAllBytes("$($_.fullName)")
        # Decode $byte[8]-1 bytes from position 40, assuming ASCII encoding.
        $ContainerName = [System.Text.Encoding]::ASCII.GetString($bytes, 40, $bytes[8]-1)
    } catch {
        $ContainerName="No read access"
    }
    [PSCustomObject]@{
        Container = $ContainerName
        FileName = $_.Name
    }
}