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

守給你的承諾、 提交于 2019-11-28 14:33:25
bartonjs

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