Export private key (PKCS#8) of CNG RSA certificate with oldschool .NET

狂风中的少年 提交于 2019-12-17 21:13:27

问题


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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

The file is a sample for reproduction and the password is: 1234

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

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.

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.

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);
...

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

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...


回答1:


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

1) PbeParams.

Yours:

[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];
}

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).

Restoring the fixed byte rgbSalt[RgbSaltSize] is important.

2) NCryptExportKey's pbOutput:

Yours:

[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);

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

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


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 \0 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\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
}


来源:https://stackoverflow.com/questions/55236230/export-private-key-pkcs8-of-cng-rsa-certificate-with-oldschool-net

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!