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

时间:2019-06-27 17:01:21

标签: c# certificate pinvoke private-key

我正在尝试使用不同的导出策略重新加载证书私钥以解决此issue。我重用了此answer中的代码以导出私钥,然后将其导入并将导出策略设置为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\0");

        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,
                }
            }
        }
    }
}

证书先导出,然后导入。但是,导入的私钥无法重新分配给原始证书。我收到“提供的密钥与该证书的公共密钥不匹配”或“仅支持实现ICspAsymmetricAlgorithm的非对称密钥”的提示。我在做错什么吗?

1 个答案:

答案 0 :(得分: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

在Windows 7或8.1上,CAPI(CspParameters背后的库)根本无法理解CNG中的键; (理论上)它在10上对此有支持,但是您绝对必须告诉它密钥存在于CNG(CspParameters.ProviderName)中。

此处的代码在ProviderType 24的“ Microsoft RSA和AES增强加密服务提供程序”中创建了一个新的CAPI密钥,而该刚好与您的CNG密钥具有相同的本地密钥名称。

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

// Attempt #2

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

PrivateKey属性仅支持get或set中的CAPI。该集合真的很危险,因为它不会修改证书对象,它会修改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相同的“每个人都突然知道这一点”的更新更改,除了更明显的是您要求证书存储进行更改)