使用 oldschool .NET 导出 CNG RSA 证书的私钥 (PKCS#8)
Export private key (PKCS#8) of CNG RSA certificate with oldschool .NET
我有一个 PKCS #12 (PFX) 文件,它是一个带有 CNG RSA 密钥的证书,我想导出私钥。
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
文件为复制样本,密码为:1234
我已经尝试导出 RsaParameters
以及从 CngKey
导出私钥但没有成功 - operation not supported
.
问题是缺少 CngExportPolicies.AllowPlaintextExport
标志。由于密钥处于最终状态,因此我也没有成功使用本机 NCrypt
调用设置标志。
在另一个问题评论 () 中有关于流程(导出、导入和设置标志和导出)的提示,它也指向几行 .NET 核心代码。
我试图将代码移植到旧式 C# .NET(例如没有 Span<T>
),但在以下调用中得到 invalid argument
:
internal static unsafe bool ExportPkcs8KeyBlob(
SafeNCryptKeyHandle keyHandle,
string password,
int kdfCount,
out int bytesWritten,
out byte[] allocated)
{
using (var stringHandle = new SafeUnicodeStringHandle(password))
{
var pbrParamsPtr =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(NativeMethods.NCrypt.PbeParams)));
var pbeParams = new NativeMethods.NCrypt.PbeParams();
fixed (byte* oidPtr = s_pkcs12TripleDesOidBytes)
{
var salt = new byte[8];
RandomNumberGenerator.GetBytes(salt);
pbeParams.rgbSalt = salt;
pbeParams.Params.cbSalt = pbeParams.rgbSalt.Length;
pbeParams.Params.iIterations = kdfCount;
var buffers = stackalloc NativeMethods.NCrypt.NCryptBuffer[3];
buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
cbBuffer = checked(2 * (password.Length + 1)),
pvBuffer = stringHandle.DangerousGetHandle(),
};
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,
};
Marshal.StructureToPtr(pbeParams, pbrParamsPtr, true);
buffers[2] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgParam,
cbBuffer = Marshal.SizeOf(typeof(NativeMethods.NCrypt.PbeParams)),
pvBuffer = pbrParamsPtr
};
var desc = new NativeMethods.NCrypt.NCryptBufferDesc
{
cBuffers = 3,
pBuffers = (IntPtr)buffers,
ulVersion = 0,
};
var pbOutput = Array.Empty<byte>();
var errorCode = NativeMethods.NCrypt.NCryptExportKey(
keyHandle,
IntPtr.Zero,
NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
ref desc,
ref pbOutput,
0,
out int numBytesNeeded,
0);
if (errorCode != 0)
{
throw new Win32Exception(errorCode);
...
我为完整代码创建了一个存储库,其中的单元测试在 github 处失败:https://github.com/lennybacon/CngPfxKeyExport
欢迎任何我从 .Net Core 转换失败或填充错误数据或指针的提示,因为关于用法的文档似乎非常罕见...
您似乎引入了两个主要的移植错误和一个调用本机方法的错误:
1) PbeParams
.
你的:
[StructLayout(LayoutKind.Sequential)]
internal struct PbeParams
{
internal const int RgbSaltSize = 8;
internal CryptPkcs12PbeParams Params;
internal byte[] rgbSalt;
}
核心外汇:
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PBE_PARAMS
{
internal const int RgbSaltSize = 8;
internal CRYPT_PKCS12_PBE_PARAMS Params;
internal fixed byte rgbSalt[RgbSaltSize];
}
你记忆中的布局是在CRYPT_PKCS12_PBE_PARAMS值之后是一个指向更多数据的指针。 CoreFX 版本的布局是紧接在 CRYPT_PKCS12_PBE_PARAMS 之后的是 8 个字节的盐占位符,这是加密 API 所期望的(因为它不采用 pbSalt)。
恢复 fixed byte rgbSalt[RgbSaltSize]
很重要。
2) NCryptExportKey
的 pbOutput
:
你的:
[DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
internal static extern int NCryptExportKey(
SafeNCryptKeyHandle hKey,
IntPtr hExportKey,
string pszBlobType,
ref NCryptBufferDesc pParameterList,
ref byte[] pbOutput,
int cbOutput,
[Out] out int pcbResult,
int dwFlags);
核心外汇:
[DllImport(Interop.Libraries.NCrypt, CharSet = CharSet.Unicode)]
internal static extern ErrorCode NCryptExportKey(
SafeNCryptKeyHandle hKey,
IntPtr hExportKey,
string pszBlobType,
ref NCryptBufferDesc pParameterList,
ref byte pbOutput,
int cbOutput,
[Out] out int pcbResult,
int dwFlags);
值得注意的是,CoreFX 版本是 ref byte pbOutput
而你的版本是 ref byte[] pbOutput
,使得值因指针间接而不同。
3) 第一次调用导出需要 C NULL
,不是有效指针。
将更正后的互操作代码压缩到一个文件中,删除注释和未使用的枚举成员(以减少 post 大小)并修复它(然后简化使用,因为您可以使用 string
(保证 [=23=]
终止符)而不是 ReadOnlySpan<char>
(无终止符保证))在 .NET Framework 4.7.2 上产生这个:
internal static class CngEncryptedExport
{
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[=14=]");
internal static void Go()
{
using (var cert = new X509Certificate2(s_pfx, PfxPassword, X509KeyStorageFlags.Exportable))
using (RSA rsa = cert.GetRSAPrivateKey())
{
RSACng rsaCng = (RSACng)rsa;
using (CngKey key = rsaCng.Key)
{
Console.WriteLine(key.ExportPolicy);
Console.WriteLine(
Convert.ToBase64String(
ExportPkcs8KeyBlob(key.Handle, "123", 21)));
}
}
}
private static unsafe byte[] ExportPkcs8KeyBlob(
SafeNCryptKeyHandle keyHandle,
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,
};
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 class NativeMethods
{
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,
PkcsSecret = 46,
}
}
}
// PFX and password omitted
}
我有一个 PKCS #12 (PFX) 文件,它是一个带有 CNG RSA 密钥的证书,我想导出私钥。
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
文件为复制样本,密码为:1234
我已经尝试导出 RsaParameters
以及从 CngKey
导出私钥但没有成功 - operation not supported
.
问题是缺少 CngExportPolicies.AllowPlaintextExport
标志。由于密钥处于最终状态,因此我也没有成功使用本机 NCrypt
调用设置标志。
在另一个问题评论 (
我试图将代码移植到旧式 C# .NET(例如没有 Span<T>
),但在以下调用中得到 invalid argument
:
internal static unsafe bool ExportPkcs8KeyBlob(
SafeNCryptKeyHandle keyHandle,
string password,
int kdfCount,
out int bytesWritten,
out byte[] allocated)
{
using (var stringHandle = new SafeUnicodeStringHandle(password))
{
var pbrParamsPtr =
Marshal.AllocHGlobal(Marshal.SizeOf(typeof(NativeMethods.NCrypt.PbeParams)));
var pbeParams = new NativeMethods.NCrypt.PbeParams();
fixed (byte* oidPtr = s_pkcs12TripleDesOidBytes)
{
var salt = new byte[8];
RandomNumberGenerator.GetBytes(salt);
pbeParams.rgbSalt = salt;
pbeParams.Params.cbSalt = pbeParams.rgbSalt.Length;
pbeParams.Params.iIterations = kdfCount;
var buffers = stackalloc NativeMethods.NCrypt.NCryptBuffer[3];
buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
cbBuffer = checked(2 * (password.Length + 1)),
pvBuffer = stringHandle.DangerousGetHandle(),
};
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,
};
Marshal.StructureToPtr(pbeParams, pbrParamsPtr, true);
buffers[2] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgParam,
cbBuffer = Marshal.SizeOf(typeof(NativeMethods.NCrypt.PbeParams)),
pvBuffer = pbrParamsPtr
};
var desc = new NativeMethods.NCrypt.NCryptBufferDesc
{
cBuffers = 3,
pBuffers = (IntPtr)buffers,
ulVersion = 0,
};
var pbOutput = Array.Empty<byte>();
var errorCode = NativeMethods.NCrypt.NCryptExportKey(
keyHandle,
IntPtr.Zero,
NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
ref desc,
ref pbOutput,
0,
out int numBytesNeeded,
0);
if (errorCode != 0)
{
throw new Win32Exception(errorCode);
...
我为完整代码创建了一个存储库,其中的单元测试在 github 处失败:https://github.com/lennybacon/CngPfxKeyExport
欢迎任何我从 .Net Core 转换失败或填充错误数据或指针的提示,因为关于用法的文档似乎非常罕见...
您似乎引入了两个主要的移植错误和一个调用本机方法的错误:
1) PbeParams
.
你的:
[StructLayout(LayoutKind.Sequential)]
internal struct PbeParams
{
internal const int RgbSaltSize = 8;
internal CryptPkcs12PbeParams Params;
internal byte[] rgbSalt;
}
核心外汇:
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PBE_PARAMS
{
internal const int RgbSaltSize = 8;
internal CRYPT_PKCS12_PBE_PARAMS Params;
internal fixed byte rgbSalt[RgbSaltSize];
}
你记忆中的布局是在CRYPT_PKCS12_PBE_PARAMS值之后是一个指向更多数据的指针。 CoreFX 版本的布局是紧接在 CRYPT_PKCS12_PBE_PARAMS 之后的是 8 个字节的盐占位符,这是加密 API 所期望的(因为它不采用 pbSalt)。
恢复 fixed byte rgbSalt[RgbSaltSize]
很重要。
2) NCryptExportKey
的 pbOutput
:
你的:
[DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
internal static extern int NCryptExportKey(
SafeNCryptKeyHandle hKey,
IntPtr hExportKey,
string pszBlobType,
ref NCryptBufferDesc pParameterList,
ref byte[] pbOutput,
int cbOutput,
[Out] out int pcbResult,
int dwFlags);
核心外汇:
[DllImport(Interop.Libraries.NCrypt, CharSet = CharSet.Unicode)]
internal static extern ErrorCode NCryptExportKey(
SafeNCryptKeyHandle hKey,
IntPtr hExportKey,
string pszBlobType,
ref NCryptBufferDesc pParameterList,
ref byte pbOutput,
int cbOutput,
[Out] out int pcbResult,
int dwFlags);
值得注意的是,CoreFX 版本是 ref byte pbOutput
而你的版本是 ref byte[] pbOutput
,使得值因指针间接而不同。
3) 第一次调用导出需要 C NULL
,不是有效指针。
将更正后的互操作代码压缩到一个文件中,删除注释和未使用的枚举成员(以减少 post 大小)并修复它(然后简化使用,因为您可以使用 string
(保证 [=23=]
终止符)而不是 ReadOnlySpan<char>
(无终止符保证))在 .NET Framework 4.7.2 上产生这个:
internal static class CngEncryptedExport
{
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[=14=]");
internal static void Go()
{
using (var cert = new X509Certificate2(s_pfx, PfxPassword, X509KeyStorageFlags.Exportable))
using (RSA rsa = cert.GetRSAPrivateKey())
{
RSACng rsaCng = (RSACng)rsa;
using (CngKey key = rsaCng.Key)
{
Console.WriteLine(key.ExportPolicy);
Console.WriteLine(
Convert.ToBase64String(
ExportPkcs8KeyBlob(key.Handle, "123", 21)));
}
}
}
private static unsafe byte[] ExportPkcs8KeyBlob(
SafeNCryptKeyHandle keyHandle,
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,
};
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 class NativeMethods
{
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,
PkcsSecret = 46,
}
}
}
// PFX and password omitted
}