使用 oldschool .NET 导出 CNG RSA 证书的私钥 (PKCS#8) [英] Export private key (PKCS#8) of CNG RSA certificate with oldschool .NET

查看:40
本文介绍了使用 oldschool .NET 导出 CNG RSA 证书的私钥 (PKCS#8)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个 PKCS #12 (PFX) 文件,它是一个带有 CNG RSA 密钥的证书,我想导出私钥.

I have a PKCS #12 (PFX) file that is a certificate with CNG RSA key and want to export the private key.

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

文件为复制样本,密码为:1234

我已经尝试导出 RsaParameters 以及从 CngKey 导出私钥但没有成功 - 操作不支持.

I already tried to export the RsaParameters as well as exporting the private key from the CngKey with no success - operation not supported.

问题是缺少 CngExportPolicies.AllowPlaintextExport 标志.由于密钥处于最终状态,因此我在使用原生 NCrypt 调用设置标志时也没有成功.

The issue is the missing CngExportPolicies.AllowPlaintextExport flag. I also had no success in setting the flag with native NCrypt calls as the key is in a finalized state.

在另一个问题评论(无法导出 RSA 私钥参数,不支持请求的操作),它也指向几行 .NET 核心代码.

There was a hint on the process (export, import & set flag & export) in another questions comment (Cannot export RSA private key parameters, the requested operation is not supported) that also points to a few lines of .NET core code.

我尝试将代码移植到 oldschool C# .NET(例如,没有 Span<T>),但在以下调用中获得了 invalid argument:

I tried to port the code to oldschool C# .NET (without Span<T> for instance) but get a invalid argument on the following call:

    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

I created a repository for the complete code with a unit test that fails at github: https://github.com/lennybacon/CngPfxKeyExport

欢迎任何提示我从 .Net Core 转换失败或填充错误的数据或指针,因为有关使用的文档似乎非常罕见...

Any hints where I failed in the conversion from .Net Core or stuffed wrong data or pointers are welcome as documentation on the usage seems to be very rare...

推荐答案

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

You seem to have introduced two main porting errors and one calling the native method:

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 个字节的占位符用于 salt,这是加密 API 所期望的(因为它不需要 pbSalt).

The layout in memory of yours is that after the CRYPT_PKCS12_PBE_PARAMS value is a pointer to more data. The layout of the CoreFX version is that directly after CRYPT_PKCS12_PBE_PARAMS is 8 bytes of placeholder for the salt, which is what the crypto API expects (since it doesn't take pbSalt).

恢复固定字节rgbSalt[RgbSaltSize]很重要.

2) NCryptExportKeypbOutput:

你的:

[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,使得值因指针间接而不同.

Notably, the CoreFX version was ref byte pbOutput and yours is ref byte[] pbOutput, making the value differ by a pointer indirection.

3) 第一次调用 export 需要 C NULL,不是有效的指针.

3) The first call to export wants C NULL, not valid pointer.

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

Squishing your corrected interop code into one file, dropping the comments and unused enum members (for reducing post size) and fixing it up (then simplifying the usage since you can use string (guaranteed terminator) instead of ReadOnlySpan<char> (no terminator guarantee)) yields this on .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");

    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
}

这篇关于使用 oldschool .NET 导出 CNG RSA 证书的私钥 (PKCS#8)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆