Big delay while calling EnvelopedCms constructor

Hi all,

 

You may experience a big delay when calling EnvelopedCms constructor in your .NET application if you have networking problems. For example, if DNS server is not available, a call to the constructor may take 30 to 90 seconds!!!

 

I could repro the issue by running the following code:

 DateTime before = DateTime.Now;

EnvelopedCms envelopedCms = new EnvelopedCms();

MessageBox.Show("Duration = " + DateTime.Now.Subtract(before).TotalMilliseconds.ToString() + " milliseconds");

 

In my test environment for instance, if DNS is correctly set, the duration of the first call to the constructor of EnvelopedCms will take ~1 second, while subsequent calls to the constructor in the same instance of the process will take ~0 secs. Now, if I set the DNS to a dummy 1.1.1.1, the first call to the constructor will take ~85 secs, more or less the same as subsequent calls.

 

I debugged the issue, and saw that the delay is being caused by a couple calls to CryptFindOIDInfo API that the constructor makes behind the scenes to do an OID lookup. It calls CryptFindOIDInfo with certain parameters that will always cause the API to go to the network trying to reach Active Directory. This API is documented to behave that way:

 

CryptFindOIDInfo Function

"

The CryptFindOIDInfo function performs a lookup in the active directory to retrieve the friendly names of OIDs under the following conditions:

•The key type in the dwKeyType parameter is set to CRYPT_OID_INFO_OID_KEY or CRYPT_OID_INFO_NAME_KEY.

•No group identifier is specified in the dwGroupId parameter or the GroupID refers to EKU OIDs, policy OIDs or template OIDs.

Network retrieval of the friendly name can be suppressed by calling the function with the CRYPT_OID_DISABLE_SEARCH_DS_FLAG flag.  

"

And the constructor won't use CRYPT_OID_DISABLE_SEARCH_DS_FLAG when calling the API.

 

Note that this same issue applies to other .NET Security classes like SignedCms or RSACryptoServiceProvider. The following article proposes a workaround for this issue when working with RSACryptoServiceProvider, but it won't apply to EnvelopedCms or SignedCms scenarios:

Problem: Delay while calling RSACryptoServiceProvider SignData or VerifyData methods

 

In order to workaround the issue with EnvelopedCms, you just need to run the following code once with an admin user. The code will register locally the OIDs that EnvelopedCms constructor is looking for in AD. That way it won’t need to go to the network for the info. You may create a tiny command line EXE that you can run before running your app:

 

 using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.Security.Cryptography.X509Certificates;

using System.Security.Cryptography.Pkcs;

using System.Runtime.InteropServices;

using System.Diagnostics;

using Microsoft.Win32;



namespace WindowsFormsApplication1

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }



        private void button1_Click(object sender, EventArgs e)

        {

            DateTime before = DateTime.Now;



            // Repro the issue

            EnvelopedCms envelopedCms = new EnvelopedCms();



            MessageBox.Show("Duration = " + DateTime.Now.Subtract(before).TotalMilliseconds.ToString() + " milliseconds");

        }



        private void button2_Click(object sender, EventArgs e)

        {

            try

            {

                // Register all the OIDs that EnvelopedCms constructor tries to use with CryptFindOIDInfo

                RegisterOID(szOID_RSA_data);

                RegisterOID(szOID_RSA_DES_EDE3_CBC);

            }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

                return;

            }

        }



        public void RegisterOID(string oid)

        {

            // Get OID info

            IntPtr info = CryptFindOIDInfo(CRYPT_OID_INFO_OID_KEY, oid, 0);



            if (info.Equals(IntPtr.Zero))

            {

                // Error

                throw new Exception("CryptFindOIDInfo didn't find OID \"" + oid + "\"");

            }



            // Register it

            bool success = CryptRegisterOIDInfo(info, CRYPT_INSTALL_OID_INFO_BEFORE_FLAG);

            if (!success)

            {

                // Error

                throw new Exception("CryptRegisterOIDInfo failed for OID \"" + oid + "\". Error #" + Marshal.GetLastWin32Error().ToString());

            }



            // Modify registry info (this is because .NET uses CRYPT_OID_INFO_NAME_KEY to look for the OID, 

            // instead of CRYPT_OID_INFO_OID_KEY, and in these cases it is looking by OID, and not by name)

            CRYPT_OID_INFO oidInfo = new CRYPT_OID_INFO();

            Marshal.PtrToStructure(info, oidInfo);



            bool isWow64 = false;

            IsWow64Process(Process.GetCurrentProcess().Handle, ref isWow64);



            string strRegKey = "";

            if (isWow64)

            {

                // We are running x86 app in x64 bit Windows

                strRegKey = "Software\\Wow6432Node\\Microsoft\\Cryptography\\OID\\EncodingType 0\\CryptDllFindOIDInfo\\" + oidInfo.pszOID + "!" + oidInfo.dwGroupId.ToString();

            }

            else

            {

                // We are running x86 app in x86 Windows or x64 app in x64 Windows

                strRegKey = "Software\\Microsoft\\Cryptography\\OID\\EncodingType 0\\CryptDllFindOIDInfo\\" + oidInfo.pszOID + "!" + oidInfo.dwGroupId.ToString();

            }



            RegistryKey Key = null;

            Key = Registry.LocalMachine.CreateSubKey(strRegKey);

            Key.SetValue("Name", oidInfo.pszOID);

            Key.Close();

        }



        public const int CRYPT_OID_INFO_OID_KEY = 1;

        public const int CRYPT_OID_INFO_NAME_KEY = 2;

        public const string szOID_RSA_data = "1.2.840.113549.1.7.1";

        public const string szOID_RSA_DES_EDE3_CBC = "1.2.840.113549.3.7";

        public const int CRYPT_INSTALL_OID_INFO_BEFORE_FLAG = 1;



        [StructLayout(LayoutKind.Sequential)]

        public struct CRYPTOAPI_BLOB

        {

            public int cbData;

            public IntPtr pbData;

        } 



        [StructLayout(LayoutKind.Sequential)]

        public class CRYPT_OID_INFO

        {

            public int cbSize;

            [MarshalAs(UnmanagedType.LPStr)]

            public string pszOID;

            [MarshalAs(UnmanagedType.LPWStr)]

            public string pwszName;

            public int dwGroupId;

            public int dwValueOrAlgidordwLength;

            public CRYPTOAPI_BLOB ExtraInfo;

            [MarshalAs(UnmanagedType.LPWStr)]

            public string pwszCNGAlgid;

            [MarshalAs(UnmanagedType.LPWStr)]

            public string pwszCNGExtraAlgid;

        } 



        [DllImport("crypt32.dll", SetLastError=true)]

        public static extern IntPtr CryptFindOIDInfo(

            int dwKeyType,

            string pvKey,

            uint dwGroupId

        );



        [DllImport("crypt32.dll", SetLastError = true)]

        public static extern bool CryptRegisterOIDInfo(

            IntPtr pInfo,

            int dwFlags

        );



        [DllImport("kernel32.dll", SetLastError = true)]

        public static extern bool IsWow64Process(

            IntPtr hProcess,

            ref bool Wow64Process

        );         

    }

} 

 

 

Note that this code won't work as is on Windows Server 2003 or Windows XP. The pwszCNGAlgid and pwszCNGExtraAlgid fields of CRYPT_OID_INFO are not available on OS versions older than Vista:

 

CRYPT_OID_INFO Structure

pwszCNGAlgid

Windows Server 2003, Windows XP, and Windows 2000: This member is not available.

pwszCNGExtraAlgid

Windows Server 2003, Windows XP, and Windows 2000: This member is not available.

 

So we should use this class instead:

         [StructLayout(LayoutKind.Sequential)]
        public class CRYPT_OID_INFO
        {
            public int cbSize;
            [MarshalAs(UnmanagedType.LPStr)]
            public string pszOID;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string pwszName;
            public int dwGroupId;
            public int dwValueOrAlgidordwLength;
            public CRYPTOAPI_BLOB ExtraInfo;
        }

 

I hope this helps.

Regards,

 

Alex (Alejandro Campos Magencio)