无法使用 NCryptExportKey 和 NCryptImportKey 重新导入私钥

Cannot re-import private key with NCryptExportKey and NCryptImportKey

我正在尝试重新加载具有不同导出策略的证书私钥来修复此问题 . I reused the code from this 以导出私钥,然后将其导入并将导出策略设置为 AllowPlainTextExport。有了它,我应该能够用重新导入的私钥重建原始证书,并在必要时导出其参数。这是我现在的代码:

using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace TestRsaCngExportImport
{
    class Program
    {
        internal const string NcryptPkcs8PrivateKeyBlob = "PKCS8_PRIVATEKEY";
        private const int NcryptDoNotFinalizeFlag = 0x00000400;
        public const string MicrosoftSoftwareKeyStorageProvider = "Microsoft Software Key Storage Provider";
        private static readonly byte[] pkcs12TripleDesOidBytes = Encoding.ASCII.GetBytes("1.2.840.113549.1.12.1.3[=10=]");

        static void Main(string[] args)
        {
            var certificate = CreateCertificate();
            FixPrivateKey(certificate);
        }        

        public static void FixPrivateKey(X509Certificate2 certificate)
        {
            var cngKey = (RSACng)RSACertificateExtensions.GetRSAPrivateKey(certificate);
            var exported = ExportPkcs8KeyBlob(cngKey.Key.Handle, "", 1);
            var importedKeyName = ImportPkcs8KeyBlob(exported, "", 1);

            // Attempt #1

            CspParameters parameters = new CspParameters();
            parameters.KeyContainerName = importedKeyName;
            var rsaKey = new RSACryptoServiceProvider(parameters);
            certificate.PrivateKey = rsaKey; // public key doesn't match the private key

            // Attempt #2

            var rsaCngKey = new RSACng(CngKey.Open(importedKeyName));
            certificate.PrivateKey = rsaCngKey; // Only asymmetric keys that implement ICspAsymmetricAlgorithm are supported.

            // Attempt #3
            certificate.PrivateKey = null;
            X509Certificate2 certWithKey = certificate.CopyWithPrivateKey(rsaKey); // The provided key does not match the public key for this certificate.
        }

        private static X509Certificate2 CreateCertificate()
        {
            var keyParams = new CngKeyCreationParameters();
            keyParams.KeyUsage = CngKeyUsages.Signing;
            keyParams.Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
            keyParams.ExportPolicy = CngExportPolicies.AllowExport; // here I don't have AllowPlaintextExport
            keyParams.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None));
            var cngKey = CngKey.Create(CngAlgorithm.Rsa, Guid.NewGuid().ToString(), keyParams);
            var rsaKey = new RSACng(cngKey);
            var req = new CertificateRequest("cn=mah_cert", rsaKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); // requires .net 4.7.2
            var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
            return cert;
        }

        private unsafe static string ImportPkcs8KeyBlob(byte[] exported, string password, int kdfCount)
        {
            var pbeParams = new NativeMethods.NCrypt.PbeParams();
            var pbeParamsPtr = &pbeParams;
            var salt = new byte[NativeMethods.NCrypt.PbeParams.RgbSaltSize];
            using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
                rng.GetBytes(salt);
            pbeParams.Params.cbSalt = salt.Length;
            Marshal.Copy(salt, 0, (IntPtr)pbeParams.rgbSalt, salt.Length);
            pbeParams.Params.iIterations = kdfCount;

            var keyName = Guid.NewGuid().ToString("D");
            fixed (char* passwordPtr = password)
            fixed (char* keyNamePtr = keyName)
            fixed (byte* oidPtr = pkcs12TripleDesOidBytes)
            {
                NativeMethods.NCrypt.NCryptOpenStorageProvider(out var safeNCryptProviderHandle, MicrosoftSoftwareKeyStorageProvider, 0);
                NativeMethods.NCrypt.NCryptBuffer* buffers = stackalloc NativeMethods.NCrypt.NCryptBuffer[4];

                buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
                    cbBuffer = checked(2 * (password.Length + 1)),
                    pvBuffer = (IntPtr)passwordPtr,
                };

                if (buffers[0].pvBuffer == IntPtr.Zero)
                {
                    buffers[0].cbBuffer = 0;
                }

                buffers[1] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgOid,
                    cbBuffer = pkcs12TripleDesOidBytes.Length,
                    pvBuffer = (IntPtr)oidPtr,
                };

                buffers[2] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgParam,
                    cbBuffer = sizeof(NativeMethods.NCrypt.PbeParams),
                    pvBuffer = (IntPtr)pbeParamsPtr,
                };

                buffers[3] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsKeyName,
                    cbBuffer = checked(2 * (keyName.Length + 1)),
                    pvBuffer = (IntPtr)keyNamePtr,
                };

                var desc2 = new NativeMethods.NCrypt.NCryptBufferDesc
                {
                    cBuffers = 4,
                    pBuffers = (IntPtr)buffers,
                    ulVersion = 0,
                };

                var result = NativeMethods.NCrypt.NCryptImportKey(safeNCryptProviderHandle, IntPtr.Zero, NcryptPkcs8PrivateKeyBlob, ref desc2, out var safeNCryptKeyHandle, exported, exported.Length, NcryptDoNotFinalizeFlag);
                if (result != 0)
                    throw new Win32Exception(result);

                var exportPolicyBytes = BitConverter.GetBytes(
                  (int)(CngExportPolicies.AllowExport |
                        CngExportPolicies.AllowPlaintextExport |
                        CngExportPolicies.AllowArchiving |
                        CngExportPolicies.AllowPlaintextArchiving));

                NativeMethods.NCrypt.NCryptSetProperty(safeNCryptKeyHandle, "Export Policy", exportPolicyBytes, exportPolicyBytes.Length, CngPropertyOptions.Persist);
                NativeMethods.NCrypt.NCryptFinalizeKey(safeNCryptKeyHandle, 0);

                return keyName;
            }
        }

        private static unsafe byte[] ExportPkcs8KeyBlob(SafeNCryptKeyHandle keyHandle, string password, int kdfCount)
        {
            var pbeParams = new NativeMethods.NCrypt.PbeParams();
            var pbeParamsPtr = &pbeParams;
            var salt = new byte[NativeMethods.NCrypt.PbeParams.RgbSaltSize];
            using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
                rng.GetBytes(salt);
            pbeParams.Params.cbSalt = salt.Length;
            Marshal.Copy(salt, 0, (IntPtr)pbeParams.rgbSalt, salt.Length);
            pbeParams.Params.iIterations = kdfCount;

            fixed (char* stringPtr = password)
            fixed (byte* oidPtr = pkcs12TripleDesOidBytes)
            {
                NativeMethods.NCrypt.NCryptBuffer* buffers =
                    stackalloc NativeMethods.NCrypt.NCryptBuffer[3];

                buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
                    cbBuffer = checked(2 * (password.Length + 1)),
                    pvBuffer = (IntPtr)stringPtr,
                };

                if (buffers[0].pvBuffer == IntPtr.Zero)
                {
                    buffers[0].cbBuffer = 0;
                }

                buffers[1] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgOid,
                    cbBuffer = pkcs12TripleDesOidBytes.Length,
                    pvBuffer = (IntPtr)oidPtr,
                };

                buffers[2] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgParam,
                    cbBuffer = sizeof(NativeMethods.NCrypt.PbeParams),
                    pvBuffer = (IntPtr)pbeParamsPtr,
                };

                var desc = new NativeMethods.NCrypt.NCryptBufferDesc
                {
                    cBuffers = 3,
                    pBuffers = (IntPtr)buffers,
                    ulVersion = 0,
                };

                int result = NativeMethods.NCrypt.NCryptExportKey(keyHandle, IntPtr.Zero, NcryptPkcs8PrivateKeyBlob, ref desc, null, 0, out int bytesNeeded, 0);
                if (result != 0)
                    throw new Win32Exception(result);

                byte[] exported = new byte[bytesNeeded];
                result = NativeMethods.NCrypt.NCryptExportKey(keyHandle, IntPtr.Zero, NcryptPkcs8PrivateKeyBlob, ref desc, exported, exported.Length, out bytesNeeded, 0);

                if (result != 0)
                    throw new Win32Exception(result);

                if (bytesNeeded != exported.Length)
                    Array.Resize(ref exported, bytesNeeded);
                return exported;
            }
        }

        private static class NativeMethods
        {
            internal static class NCrypt
            {
                public const string NCryptLibraryName = "ncrypt.dll";

                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptCreatePersistedKey(SafeNCryptProviderHandle hProvider, [Out] out SafeNCryptKeyHandle phKey, string pszAlgId, string pszKeyName, int dwLegacyKeySpec, CngKeyCreationOptions dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptOpenStorageProvider([Out] out SafeNCryptProviderHandle phProvider, [MarshalAs(UnmanagedType.LPWStr)] string pszProviderName, int dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, ref NCryptBufferDesc pParameterList, byte[] pbOutput, int cbOutput, [Out] out int pcbResult, int dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptImportKey(SafeNCryptProviderHandle hProvider, IntPtr hImportKey, string pszBlobType, ref NCryptBufferDesc pParameterList, [Out] out SafeNCryptKeyHandle phKey, [MarshalAs(UnmanagedType.LPArray)] byte[] pbData, int cbData, int dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, string pszProperty, [MarshalAs(UnmanagedType.LPArray)] byte[] pbInput, int cbInput, CngPropertyOptions dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, string pszProperty, string pbInput, int cbInput, CngPropertyOptions dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, string pszProperty, IntPtr pbInput, int cbInput, CngPropertyOptions dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptFinalizeKey(SafeNCryptKeyHandle hKey, int dwFlags);
                [DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
                internal static extern int NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, IntPtr pParameterList, byte[] pbOutput, int cbOutput, [Out] out int pcbResult, int dwFlags);

                [StructLayout(LayoutKind.Sequential)]
                internal unsafe struct PbeParams
                {
                    internal const int RgbSaltSize = 8;
                    internal CryptPkcs12PbeParams Params;
                    internal fixed byte rgbSalt[RgbSaltSize];
                }

                [StructLayout(LayoutKind.Sequential)]
                internal struct CryptPkcs12PbeParams
                {
                    internal int iIterations;
                    internal int cbSalt;
                }

                [StructLayout(LayoutKind.Sequential)]
                internal struct NCryptBufferDesc
                {
                    public int ulVersion;
                    public int cBuffers;
                    public IntPtr pBuffers;
                }

                [StructLayout(LayoutKind.Sequential)]
                internal struct NCryptBuffer
                {
                    public int cbBuffer;
                    public BufferType BufferType;
                    public IntPtr pvBuffer;
                }

                internal enum BufferType
                {
                    PkcsAlgOid = 41,
                    PkcsAlgParam = 42,
                    PkcsAlgId = 43,
                    PkcsKeyName = 45,
                    PkcsSecret = 46,
                }
            }
        }
    }
}

证书被导出然后导入。但是,导入的私钥不能重新分配给原始证书。我得到 "The provided key does not match the public key for this certificate" 或 "Only asymmetric keys that implement ICspAsymmetricAlgorithm are supported"。我做错了什么吗?

// Attempt #1

CspParameters parameters = new CspParameters();
parameters.KeyContainerName = importedKeyName;
var rsaKey = new RSACryptoServiceProvider(parameters);
certificate.PrivateKey = rsaKey; // public key doesn't match the private key

CAPI(CspParameters 背后的库)在 Windows 7 或 8.1 上根本无法理解 CNG 中的键;它(理论上)在 10 上支持它,但你肯定必须告诉它密钥位于 CNG 中 (CspParameters.ProviderName)。

此处的代码在 "Microsoft RSA and AES Enhanced Cryptographic Service Provider" 中创建了一个 ProviderType 24 的新 CAPI 密钥,恰好与您的 CNG 密钥具有相同的本地密钥名称。

您没有指定 UseExistingOnly 标志,并且密钥不存在,因此创建了一个新密钥...这就是 public 密钥与证书中的内容不匹配的原因。

// Attempt #2

var rsaCngKey = new RSACng(CngKey.Open(importedKeyName));
certificate.PrivateKey = rsaCngKey; // Only asymmetric keys that implement ICspAsymmetricAlgorithm are supported.

PrivateKey属性只支持CAPI,无论是get还是set。该集合使用起来非常危险,因为它不会修改证书对象,它会修改 Windows 证书存储系统中证书的状态...这意味着它还会影响任何其他现在或未来的对象在同一个 (Windows) 证书上运行。

// Attempt #3
certificate.PrivateKey = null;
X509Certificate2 certWithKey = certificate.CopyWithPrivateKey(rsaKey); // The provided key does not match the public key for this certificate.

这与尝试 1 创建的新随机密钥相同。


如果您删除尝试 1,然后合并 2 和 3,您应该以

结束
var rsaCngKey = new RSACng(CngKey.Open(importedKeyName));
X509Certificate2 certWithKey = certificate.CopyWithPrivateKey(rsaCngKey);

这应该有效。 (如果您已经将证书导入到证书库中,则只需将 certWithKey 添加到证书库中,这将具有与 cert.set_PrivateKey 相同的 "everyone suddenly knows about this" 更新更改,除了它的方式更明显的是你要求证书商店进行更改)