使用oldschool .NET导出CNG RSA证书的私钥(PKCS#8)

时间:2019-03-19 08:13:54

标签: .net certificate rsa pinvoke cng

我有一个PKCS#12(PFX)文件,该文件是具有CNG RSA密钥的证书,并且想要导出私钥。

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

该文件是用于复制的示例,密码为:1234

我已经尝试导出RsaParameters以及从CngKey导出私钥-operation not supported

问题是缺少CngExportPolicies.AllowPlaintextExport标志。我还没有成功通过本地NCrypt调用设置标志,因为密钥处于最终状态。

在另一个问题注释(Cannot export RSA private key parameters, the requested operation is not supported)中,该过程(导出,导入和设置标记并导出)有一个提示,该提示还指向几行.NET核心代码。

我试图将代码移植到oldschool 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 个答案:

答案 0 :(得分:3)

您似乎引入了两个主要的移植错误,其中一个调用了本机方法:

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的{​​{1}}:

您的用户:

pbOutput

CoreFX:

[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); ,而您的版本为ref byte pbOutput,这使该值因指针间接寻址而不同。

3)第一次导出调用需要C ref byte[] pbOutput,而不是有效的指针。


将更正后的互操作代码压缩到一个文件中,删除注释和未使用的枚举成员(以减少帖子大小)并进行修复(然后简化用法,因为您可以使用NULL(保证string终止符)而不是\0(无终止符保证))在.NET Framework 4.7.2上产生此错误:

ReadOnlySpan<char>