X509Certificate2.Import 与 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG
Posted
技术标签:
【中文标题】X509Certificate2.Import 与 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG【英文标题】:X509Certificate2.Import with NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG 【发布时间】:2019-12-07 17:33:30 【问题描述】:我有一个带有 CNG 密钥的 PFX 证书(KSP 提供者信息在 PFX 中指定)。我找不到以允许以纯文本(MS-CAPI 格式)导出私钥的方式在 .NET 中导入证书的方法。
var cert = new X509Certificate2(pfxBytes,password,X509KeyStorageFlags.Exportable);
然后我使用此句柄通过调用 CryptAcquireCertificatePrivateKey 函数和启用标志来获取私钥上下文以允许 CNG 密钥。调用成功。
当我调用NCryptExportKey 时,调用失败并出现 0x8009000b 错误:
密钥在指定状态下无效。
为了调试它,我调用了NCryptGetProperty 函数来获取导出策略,实际上,NCRYPT_ALLOW_EXPORT_FLAG
标志已启用,但NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG
未启用。尝试调用 NCryptSetProperty 函数在导出策略属性中启用此标志,但调用失败并出现相同的 0x8009000b 错误。
问题:如何从文件中导入 .NET 中的 pfx 文件而不保留密钥并为 CNG 密钥启用 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG
标志?
附言由于某些原因,我仅限于 .NET 4.0/4.5。
【问题讨论】:
【参考方案1】:我发现的最佳流程:
打开 PFX Exportable(设置可导出位,但不设置明文可导出位) 导出加密的 PKCS#8 导入加密的 PKCS#8 并覆盖,没有最终确定 更改出口政策 完成(提交覆盖) 现在,如果您向证书询问其密钥,则它可以明文导出。在 net45 中,这需要大量代码(谢天谢地 Export private key (PKCS#8) of CNG RSA certificate with oldschool .NET 为我做了很多工作)。 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\0");
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);
【讨论】:
我会看看这个。代码太多。另外,我尝试了这种方法:使用PFXImportCertStore
函数加载 PFX。它成功了,我可以从获取的句柄创建X509Store
的实例,可以查看存储中的证书。但是CryptAcquireCertificatePrivateKey
似乎无法从PFXImportCertStore
创建的内存存储中解析证书的句柄。
@Crypt32 它也可以使用 hExportKey 参数工作,但我从未研究过。相同的流程,但可能需要更少的代码。
跟进:我能够转换代码以摆脱所有 unsafe
和 fixed
语句和指针。而且我发现我不需要再次调用CryptAcquirePrivateKeyContext
(这不起作用)。相反,我可以在 ImportEncryptedPkcs8Overwrite
方法完成并提交更改后立即在 keyHandle
中使用它。无论如何,它至少适用于 ECDSA 密钥。以上是关于X509Certificate2.Import 与 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG的主要内容,如果未能解决你的问题,请参考以下文章