X509Certificate2.Import 与 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG

Posted

技术标签:

【中文标题】X509Certificate2.Import 与 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG【英文标题】:X509Certificate2.Import with NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG 【发布时间】:2019-12-07 17:33:30 【问题描述】:

我有一个带有 CNG 密钥的 PFX 证书(KSP 提供者信息在 PFX 中指定)。我找不到以允许以纯文本(MS-CAPI 格式)导出私钥的方式在 .NET 中导入证书的方法。

var cert = new X509Certificate2(pfxBytes,password,X509KeyStorageFlags.Exportable);

然后我使用此句柄通过调用 CryptAcquireCertificatePrivateKey 函数和启用标志来获取私钥上下文以允许 CNG 密钥。调用成功。

当我调用NCryptExportKey 时,调用失败并出现 0x8009000b 错误:

密钥在指定状态下无效。

为了调试它,我调用了NCryptGetProperty 函数来获取导出策略,实际上,NCRYPT_ALLOW_EXPORT_FLAG 标志已启用,但NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG 未启用。尝试调用 NCryptSetProperty 函数在导出策略属性中启用此标志,但调用失败并出现相同的 0x8009000b 错误。

问题:如何从文件中导入 .NET 中的 pfx 文件而不保留密钥并为 CNG 密钥启用 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG 标志?

附言由于某些原因,我仅限于 .NET 4.0/4.5。

【问题讨论】:

【参考方案1】:

我发现的最佳流程:

打开 PFX Exportable(设置可导出位,但不设置明文可导出位) 导出加密的 PKCS#8 导入加密的 PKCS#8 并覆盖,没有最终确定 更改出口政策 完成(提交覆盖) 现在,如果您向证书询问其密钥,则它可以明文导出。

在 net45 中,这需要大量代码(谢天谢地 Export private key (PKCS#8) of CNG RSA certificate with oldschool .NET 为我做了很多工作)。 netcoreapp30 会做得更好,只是 import+alter+finalize 仍然需要手动 P/Invokes。

使用 ECDsa 进行测试,因为这是强制不使用 CNG->CAPI 桥的最简单方法:

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

【讨论】:

我会看看这个。代码太多。另外,我尝试了这种方法:使用PFXImportCertStore 函数加载 PFX。它成功了,我可以从获取的句柄创建X509Store 的实例,可以查看存储中的证书。但是CryptAcquireCertificatePrivateKey 似乎无法从PFXImportCertStore 创建的内存存储中解析证书的句柄。 @Crypt32 它也可以使用 hExportKey 参数工作,但我从未研究过。相同的流程,但可能需要更少的代码。 跟进:我能够转换代码以摆脱所有 unsafefixed 语句和指针。而且我发现我不需要再次调用CryptAcquirePrivateKeyContext(这不起作用)。相反,我可以在 ImportEncryptedPkcs8Overwrite 方法完成并提交更改后立即在 keyHandle 中使用它。无论如何,它至少适用于 ECDSA 密钥。

以上是关于X509Certificate2.Import 与 NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG的主要内容,如果未能解决你的问题,请参考以下文章

x509的详细特征

尝试在 PHP 中使用 x.509 证书对 SOAP 调用进行数字签名

证书和编码

OpenSSL之X509系列

如何使用 WCF 签署 X509 令牌

X509_STORE 和 X509_STORE_CTX 有啥区别?