Похоже, вы ввели две основные ошибки при портировании и одну вызываете собственный метод:
1) PbeParams
.
Ваш:
[StructLayout(LayoutKind.Sequential)]
internal struct PbeParams
{
internal const int RgbSaltSize = 8;
internal CryptPkcs12PbeParams Params;
internal byte[] rgbSalt;
}
CoreFX:
[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);
CoreFX:
[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
, недопустимый указатель.
Сведение исправленного кода взаимодействия в один файл, удаление комментариев и неиспользуемых элементов enum (для уменьшения размера записи) и исправление его (затем упрощение использования, поскольку вы можете использовать string
(гарантированный \0
терминатор) вместо 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\0");
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
}