X509Certificate2.Import 和 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG

X509Certificate2.Import with NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG

我有一个 PFX 证书,里面有 CNG 密钥(KSP 提供商信息在 PFX 中指定)。我找不到以允许以纯文本(MS-CAPI 格式)导出私钥的方式在 .NET 中导入证书的方法。

var cert = new X509Certificate2(pfxBytes,password,X509KeyStorageFlags.Exportable);

然后我使用此句柄通过调用带有启用标志的 CryptAcquireCertificatePrivateKey 函数来获取私钥上下文以允许 CNG 密钥。调用成功。

当我调用 NCryptExportKey 时,调用失败并出现 0x8009000b 错误:

Key not valid for use in specified state.

为了对此进行调试,我调用了 NCryptGetProperty function to get export policy and indeed, NCRYPT_ALLOW_EXPORT_FLAG flag is enabled, but NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG is not. Tried to call the NCryptSetProperty 函数以在导出策略 属性 中启用此标志,但调用失败并出现相同的 0x8009000b 错误。

问题:如何在 .NET 中从文件导入 pfx 文件而不保留密钥并为 CNG 密钥启用 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG 标志?

p.s。由于某些原因,我仅限于 .NET 4.0/4.5。

我发现的最佳流程:

  • 打开PFX Exportable(设置可导出位,但不是明文可导出位)
  • 导出加密的 PKCS#8
  • 导入加密的 PKCS#8 并覆盖,没有最终确定
  • 更改出口政策
  • 完成(提交覆盖)
  • 现在,如果您向证书索取其密钥,它是可以明文导出的。

在 net45 中,这需要大量代码(谢天谢地 为我做了很多工作)。 netcoreapp30 会做得更好,除了 import+alter+finalize 仍然需要手动 P/Invokes.

已使用 ECDsa 进行测试,因为这是强制不使用 CNG->CAPI 桥的最简单方法:

internal static partial class Program
{
    internal static void Main(string[] args)
    {
        X509Certificate2 cert = ImportExportable(ECDsaP256_DigitalSignature_Pfx_Windows, "Test", machineScope: false);

        try
        {
            bool gotKey = NativeMethods.Crypt32.CryptAcquireCertificatePrivateKey(
                cert.Handle,
                NativeMethods.Crypt32.AcquireCertificateKeyOptions.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG,
                IntPtr.Zero,
                out SafeNCryptKeyHandle keyHandle,
                out int keySpec,
                out bool callerFree);

            using (CngKey cngKey = CngKey.Open(keyHandle, 0))
            {
                Console.WriteLine(cngKey.ExportPolicy);

                Console.WriteLine(
                    Convert.ToBase64String(
                        cngKey.Export(CngKeyBlobFormat.Pkcs8PrivateBlob)));
            }
        }
        finally
        {
            cert.Reset();
        }
    }

    private static X509Certificate2 ImportExportable(byte[] pfxBytes, string password, bool machineScope)
    {
        X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable;

        if (machineScope)
        {
            flags |= X509KeyStorageFlags.MachineKeySet;
        }
        else
        {
            flags |= X509KeyStorageFlags.UserKeySet;
        }

        X509Certificate2 cert = new X509Certificate2(pfxBytes, password, flags);

        try
        {
            bool gotKey = NativeMethods.Crypt32.CryptAcquireCertificatePrivateKey(
                cert.Handle,
                NativeMethods.Crypt32.AcquireCertificateKeyOptions.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG,
                IntPtr.Zero,
                out SafeNCryptKeyHandle keyHandle,
                out int keySpec,
                out bool callerFree);

            if (!gotKey)
            {
                keyHandle.Dispose();
                throw new InvalidOperationException("No private key");
            }

            if (!callerFree)
            {
                keyHandle.SetHandleAsInvalid();
                keyHandle.Dispose();
                throw new InvalidOperationException("Key is not persisted");
            }

            using (keyHandle)
            {
                // -1 == CNG, otherwise CAPI
                if (keySpec == -1)
                {
                    using (CngKey cngKey = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.None))
                    {
                        // If the CNG->CAPI bridge opened the key then AllowPlaintextExport is already set.
                        if ((cngKey.ExportPolicy & CngExportPolicies.AllowPlaintextExport) == 0)
                        {
                            FixExportability(cngKey, machineScope);
                        }
                    }
                }
            }
        }
        catch
        {
            cert.Reset();
            throw;
        }

        return cert;
    }

    internal static void FixExportability(CngKey cngKey, bool machineScope)
    {
        string password = nameof(NativeMethods.Crypt32.AcquireCertificateKeyOptions);
        byte[] encryptedPkcs8 = ExportEncryptedPkcs8(cngKey, password, 1);
        string keyName = cngKey.KeyName;

        using (SafeNCryptProviderHandle provHandle = cngKey.ProviderHandle)
        {
            ImportEncryptedPkcs8Overwrite(
                encryptedPkcs8,
                keyName,
                provHandle,
                machineScope,
                password);
        }
    }

    internal const string NCRYPT_PKCS8_PRIVATE_KEY_BLOB = "PKCS8_PRIVATEKEY";
    private static readonly byte[] s_pkcs12TripleDesOidBytes =
        System.Text.Encoding.ASCII.GetBytes("1.2.840.113549.1.12.1.3[=10=]");

    private static unsafe byte[] ExportEncryptedPkcs8(
        CngKey cngKey,
        string password,
        int kdfCount)
    {
        var pbeParams = new NativeMethods.NCrypt.PbeParams();
        NativeMethods.NCrypt.PbeParams* pbeParamsPtr = &pbeParams;

        byte[] 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 = s_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 = s_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,
            };

            using (var keyHandle = cngKey.Handle)
            {
                int result = NativeMethods.NCrypt.NCryptExportKey(
                    keyHandle,
                    IntPtr.Zero,
                    NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
                    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,
                    NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
                    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 unsafe void ImportEncryptedPkcs8Overwrite(
        byte[] encryptedPkcs8,
        string keyName,
        SafeNCryptProviderHandle provHandle,
        bool machineScope,
        string password)
    {
        SafeNCryptKeyHandle keyHandle;

        fixed (char* passwordPtr = password)
        fixed (char* keyNamePtr = keyName)
        fixed (byte* blobPtr = encryptedPkcs8)
        {
            NativeMethods.NCrypt.NCryptBuffer* buffers = stackalloc NativeMethods.NCrypt.NCryptBuffer[2];

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

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

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

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

            NativeMethods.NCrypt.NCryptImportFlags flags =
                NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_OVERWRITE_KEY_FLAG |
                NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_DO_NOT_FINALIZE_FLAG;

            if (machineScope)
            {
                flags |= NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_MACHINE_KEY_FLAG;
            }

            int errorCode = NativeMethods.NCrypt.NCryptImportKey(
                provHandle,
                IntPtr.Zero,
                NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
                ref desc,
                out keyHandle,
                new IntPtr(blobPtr),
                encryptedPkcs8.Length,
                flags);

            if (errorCode != 0)
            {
                keyHandle.Dispose();
                throw new Win32Exception(errorCode);
            }

            using (keyHandle)
            using (CngKey cngKey = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.None))
            {
                const CngExportPolicies desiredPolicies =
                    CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;

                cngKey.SetProperty(
                    new CngProperty(
                        "Export Policy",
                        BitConverter.GetBytes((int)desiredPolicies),
                        CngPropertyOptions.Persist));

                int error = NativeMethods.NCrypt.NCryptFinalizeKey(keyHandle, 0);

                if (error != 0)
                {
                    throw new Win32Exception(error);
                }
            }
        }
    }
}

internal static class NativeMethods
{
    internal static class Crypt32
    {
        internal enum AcquireCertificateKeyOptions
        {
            None = 0x00000000,
            CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG = 0x00040000,
        }

        [DllImport("crypt32.dll", SetLastError = true)]
        internal static extern bool CryptAcquireCertificatePrivateKey(
            IntPtr pCert,
            AcquireCertificateKeyOptions dwFlags,
            IntPtr pvReserved,
            out SafeNCryptKeyHandle phCryptProvOrNCryptKey,
            out int dwKeySpec,
            out bool pfCallerFreeProvOrNCryptKey);
    }

    internal static class NCrypt
    {
        [DllImport("ncrypt.dll", 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);

        [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,
            PkcsName = 45,
            PkcsSecret = 46,
        }

        [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
        internal static extern int NCryptOpenStorageProvider(
            out SafeNCryptProviderHandle phProvider,
            string pszProviderName,
            int dwFlags);

        internal enum NCryptImportFlags
        {
            None = 0,
            NCRYPT_MACHINE_KEY_FLAG = 0x00000020,
            NCRYPT_OVERWRITE_KEY_FLAG = 0x00000080,
            NCRYPT_DO_NOT_FINALIZE_FLAG = 0x00000400,
        }

        [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
        internal static extern int NCryptImportKey(
            SafeNCryptProviderHandle hProvider,
            IntPtr hImportKey,
            string pszBlobType,
            ref NCryptBufferDesc pParameterList,
            out SafeNCryptKeyHandle phKey,
            IntPtr pbData,
            int cbData,
            NCryptImportFlags dwFlags);

        [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
        internal static extern int NCryptFinalizeKey(SafeNCryptKeyHandle hKey, int dwFlags);
    }
}