X509Certificate2.Import with NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG

前端 未结 1 465
情深已故
情深已故 2021-01-20 11:29

I have a PFX certificate with CNG key inside (KSP provider information is specified in the PFX). I can\'t find a way to import the certificate in .NET in the way that would

相关标签:
1条回答
  • 2021-01-20 12:00

    The best flow I've found:

    • Open the PFX Exportable (sets the exportable bit, but not the plaintext exportable bit)
    • Export encrypted PKCS#8
    • Import encrypted PKCS#8 with overwrite, no finalize
    • Change the export policy
    • Finalize (to commit the overwrite)
    • Now if you ask the cert for its key it's plaintext exportable.

    In net45 this takes a lot of code (thankfully Export private key (PKCS#8) of CNG RSA certificate with oldschool .NET did a lot of the work for me). netcoreapp30 would do better, except that the import+alter+finalize would still need to be manual P/Invokes.

    Tested with ECDsa, since that was the easiest way to force the CNG->CAPI bridge to not be utilized:

    internal static partial class Program
    {
        internal static void Main(string[] args)
        {
            X509Certificate2 cert = ImportExportable(ECDsaP256_DigitalSignature_Pfx_Windows, "Test", machineScope: false);
    
            try
            {
                bool gotKey = NativeMethods.Crypt32.CryptAcquireCertificatePrivateKey(
                    cert.Handle,
                    NativeMethods.Crypt32.AcquireCertificateKeyOptions.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG,
                    IntPtr.Zero,
                    out SafeNCryptKeyHandle keyHandle,
                    out int keySpec,
                    out bool callerFree);
    
                using (CngKey cngKey = CngKey.Open(keyHandle, 0))
                {
                    Console.WriteLine(cngKey.ExportPolicy);
    
                    Console.WriteLine(
                        Convert.ToBase64String(
                            cngKey.Export(CngKeyBlobFormat.Pkcs8PrivateBlob)));
                }
            }
            finally
            {
                cert.Reset();
            }
        }
    
        private static X509Certificate2 ImportExportable(byte[] pfxBytes, string password, bool machineScope)
        {
            X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable;
    
            if (machineScope)
            {
                flags |= X509KeyStorageFlags.MachineKeySet;
            }
            else
            {
                flags |= X509KeyStorageFlags.UserKeySet;
            }
    
            X509Certificate2 cert = new X509Certificate2(pfxBytes, password, flags);
    
            try
            {
                bool gotKey = NativeMethods.Crypt32.CryptAcquireCertificatePrivateKey(
                    cert.Handle,
                    NativeMethods.Crypt32.AcquireCertificateKeyOptions.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG,
                    IntPtr.Zero,
                    out SafeNCryptKeyHandle keyHandle,
                    out int keySpec,
                    out bool callerFree);
    
                if (!gotKey)
                {
                    keyHandle.Dispose();
                    throw new InvalidOperationException("No private key");
                }
    
                if (!callerFree)
                {
                    keyHandle.SetHandleAsInvalid();
                    keyHandle.Dispose();
                    throw new InvalidOperationException("Key is not persisted");
                }
    
                using (keyHandle)
                {
                    // -1 == CNG, otherwise CAPI
                    if (keySpec == -1)
                    {
                        using (CngKey cngKey = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.None))
                        {
                            // If the CNG->CAPI bridge opened the key then AllowPlaintextExport is already set.
                            if ((cngKey.ExportPolicy & CngExportPolicies.AllowPlaintextExport) == 0)
                            {
                                FixExportability(cngKey, machineScope);
                            }
                        }
                    }
                }
            }
            catch
            {
                cert.Reset();
                throw;
            }
    
            return cert;
        }
    
        internal static void FixExportability(CngKey cngKey, bool machineScope)
        {
            string password = nameof(NativeMethods.Crypt32.AcquireCertificateKeyOptions);
            byte[] encryptedPkcs8 = ExportEncryptedPkcs8(cngKey, password, 1);
            string keyName = cngKey.KeyName;
    
            using (SafeNCryptProviderHandle provHandle = cngKey.ProviderHandle)
            {
                ImportEncryptedPkcs8Overwrite(
                    encryptedPkcs8,
                    keyName,
                    provHandle,
                    machineScope,
                    password);
            }
        }
    
        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");
    
        private static unsafe byte[] ExportEncryptedPkcs8(
            CngKey cngKey,
            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,
                };
    
                using (var keyHandle = cngKey.Handle)
                {
                    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 unsafe void ImportEncryptedPkcs8Overwrite(
            byte[] encryptedPkcs8,
            string keyName,
            SafeNCryptProviderHandle provHandle,
            bool machineScope,
            string password)
        {
            SafeNCryptKeyHandle keyHandle;
    
            fixed (char* passwordPtr = password)
            fixed (char* keyNamePtr = keyName)
            fixed (byte* blobPtr = encryptedPkcs8)
            {
                NativeMethods.NCrypt.NCryptBuffer* buffers = stackalloc NativeMethods.NCrypt.NCryptBuffer[2];
    
                buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
                    cbBuffer = checked(2 * (password.Length + 1)),
                    pvBuffer = new IntPtr(passwordPtr),
                };
    
                if (buffers[0].pvBuffer == IntPtr.Zero)
                {
                    buffers[0].cbBuffer = 0;
                }
    
                buffers[1] = new NativeMethods.NCrypt.NCryptBuffer
                {
                    BufferType = NativeMethods.NCrypt.BufferType.PkcsName,
                    cbBuffer = checked(2 * (keyName.Length + 1)),
                    pvBuffer = new IntPtr(keyNamePtr),
                };
    
                NativeMethods.NCrypt.NCryptBufferDesc desc = new NativeMethods.NCrypt.NCryptBufferDesc
                {
                    cBuffers = 2,
                    pBuffers = (IntPtr)buffers,
                    ulVersion = 0,
                };
    
                NativeMethods.NCrypt.NCryptImportFlags flags =
                    NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_OVERWRITE_KEY_FLAG |
                    NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_DO_NOT_FINALIZE_FLAG;
    
                if (machineScope)
                {
                    flags |= NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_MACHINE_KEY_FLAG;
                }
    
                int errorCode = NativeMethods.NCrypt.NCryptImportKey(
                    provHandle,
                    IntPtr.Zero,
                    NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
                    ref desc,
                    out keyHandle,
                    new IntPtr(blobPtr),
                    encryptedPkcs8.Length,
                    flags);
    
                if (errorCode != 0)
                {
                    keyHandle.Dispose();
                    throw new Win32Exception(errorCode);
                }
    
                using (keyHandle)
                using (CngKey cngKey = CngKey.Open(keyHandle, CngKeyHandleOpenOptions.None))
                {
                    const CngExportPolicies desiredPolicies =
                        CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;
    
                    cngKey.SetProperty(
                        new CngProperty(
                            "Export Policy",
                            BitConverter.GetBytes((int)desiredPolicies),
                            CngPropertyOptions.Persist));
    
                    int error = NativeMethods.NCrypt.NCryptFinalizeKey(keyHandle, 0);
    
                    if (error != 0)
                    {
                        throw new Win32Exception(error);
                    }
                }
            }
        }
    }
    
    internal static class NativeMethods
    {
        internal static class Crypt32
        {
            internal enum AcquireCertificateKeyOptions
            {
                None = 0x00000000,
                CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG = 0x00040000,
            }
    
            [DllImport("crypt32.dll", SetLastError = true)]
            internal static extern bool CryptAcquireCertificatePrivateKey(
                IntPtr pCert,
                AcquireCertificateKeyOptions dwFlags,
                IntPtr pvReserved,
                out SafeNCryptKeyHandle phCryptProvOrNCryptKey,
                out int dwKeySpec,
                out bool pfCallerFreeProvOrNCryptKey);
        }
    
        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,
                PkcsName = 45,
                PkcsSecret = 46,
            }
    
            [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
            internal static extern int NCryptOpenStorageProvider(
                out SafeNCryptProviderHandle phProvider,
                string pszProviderName,
                int dwFlags);
    
            internal enum NCryptImportFlags
            {
                None = 0,
                NCRYPT_MACHINE_KEY_FLAG = 0x00000020,
                NCRYPT_OVERWRITE_KEY_FLAG = 0x00000080,
                NCRYPT_DO_NOT_FINALIZE_FLAG = 0x00000400,
            }
    
            [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
            internal static extern int NCryptImportKey(
                SafeNCryptProviderHandle hProvider,
                IntPtr hImportKey,
                string pszBlobType,
                ref NCryptBufferDesc pParameterList,
                out SafeNCryptKeyHandle phKey,
                IntPtr pbData,
                int cbData,
                NCryptImportFlags dwFlags);
    
            [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
            internal static extern int NCryptFinalizeKey(SafeNCryptKeyHandle hKey, int dwFlags);
        }
    }
    
    0 讨论(0)
提交回复
热议问题