Search code examples
c#x509certificate

C# generate Self Signed Cert with Subject Alternative Name?


We have an app that generates a Self Signed Cert but now with Chrome 58 we need to add the Subject Alternative Name. The Cert is generated using C# but invoking the CertCreateSelfSignCertificate function in win32. So far all the examples I am finding are not passing the extensions param and I'm finding it hard to create an extension to pass to generate the SAN.

Note I will be cleaning this up once I get it working This is what I am using to create an Entry and then the Extension:

CERT_ALT_NAME_ENTRY entry = new CERT_ALT_NAME_ENTRY {
        dwAltNameChoice = AlternativeNameType.Dns, // 3
        Name = Marshal.StringToHGlobalUni("127.0.0.1")
    };
    IntPtr entryBlob Marshal.AllocHGlobal(Marshal.SizeOf(typeof(CERT_ALT_NAME_ENTRY)));
    var pvStructInfo = new CERT_ALT_NAME_INFO { cAltEntry = 1, rgAltEntry = entryBlob };

            IntPtr pvEncoded = IntPtr.Zero;
            int pcbEncoded = 0;

            var status = InvokeMethods.CryptEncodeObjectEx(
                   CertEncodingType.X509_ASN_ENCODING | CertEncodingType.PKCS_7_ASN_ENCODING,  // 1 | 0x10000
                   new IntPtr(12),
                   ref pvStructInfo,
                   EncodeObjectFlags.CRYPT_ENCODE_ALLOC_FLAG, // 0x8000
                   IntPtr.Zero,
                   ref pvEncoded,
                   ref pcbEncoded);

            Marshal.FreeHGlobal(entryBlob);

            if (!status)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            var extension = new CERT_EXTENSION
            {
                ExtensionOid = OidSubjectAltName, //2.5.29.17
                IsCritical = false,
                Value = new CRYPTOAPI_BLOB
                {
                    Length = (uint)pcbEncoded,
                    Data = pvEncoded
                }
            };
            var result = new CertExtensions
            {
                cExtension = 1,
                rgExtension = extension
            }; 

Structs Used

internal struct CertExtensions
        {
            public uint cExtension;
            public CERT_EXTENSION rgExtension;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        internal struct CRYPTOAPI_BLOB
        {
            public uint Length;
            public IntPtr Data;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        internal class CERT_EXTENSION
        {
            [MarshalAs(UnmanagedType.LPWStr)]
            public string ExtensionOid;

            public bool IsCritical;
            public CRYPTOAPI_BLOB Value;
        }

  [StructLayoutAttribute(LayoutKind.Sequential)]
        internal struct CERT_ALT_NAME_INFO
        {
            /// DWORD->unsigned int
            public uint cAltEntry;

            public IntPtr rgAltEntry;
        }

        [StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        internal struct CERT_ALT_NAME_ENTRY
        {
            public AlternativeNameType dwAltNameChoice;
            public IntPtr Name;
        }

Full Code - Pieced together from a few examples I found

private static CertExtensions CreateExtensions(IList<CERT_ALT_NAME_ENTRY> items)
        {

            IntPtr itemBlob = Marshal.AllocHGlobal(items.Count * Marshal.SizeOf(typeof(CERT_ALT_NAME_ENTRY)));

            for (int i = 0; i < items.Count; i++)
            {
                var offset = (IntPtr)((long)itemBlob + i * Marshal.SizeOf(typeof(CERT_ALT_NAME_ENTRY)));
                Marshal.StructureToPtr(items[i], offset, false);
            }

            var pvStructInfo = new CERT_ALT_NAME_INFO { cAltEntry = (uint)items.Count, rgAltEntry = itemBlob };

            IntPtr pvEncoded = IntPtr.Zero;
            int pcbEncoded = 0;

            var status = InvokeMethods.CryptEncodeObjectEx(
                   CertEncodingType.X509_ASN_ENCODING | CertEncodingType.PKCS_7_ASN_ENCODING,
                   new IntPtr(12),
                   ref pvStructInfo,
                   EncodeObjectFlags.CRYPT_ENCODE_ALLOC_FLAG,
                   IntPtr.Zero,
                   ref pvEncoded,
                   ref pcbEncoded);


            Marshal.FreeHGlobal(itemBlob);

            if (!status)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            var extension = new CERT_EXTENSION
            {
                ExtensionOid = OidSubjectAltName,
                IsCritical = false,
                Value = new CRYPTOAPI_BLOB
                {
                    Length = (uint)pcbEncoded,
                    Data = pvEncoded
                }
            };
            var result = new CertExtensions
            {
                cExtension = 1,
                rgExtension = extension
            };

            return result;
        }

Solution

  • Well, this going to be a hell of C#/C++ interop and is hard to understand without knowing how pointers, structs and C-like arrays work in C++ and how marshalong works in interop.

    You have incorrectly defined CERT_ALT_NAME_ENTRY structure. It used union in C++ definition. When translating unions to C# they must be aligned to the largest struct size in union (which is CRYPTOAPI_BLOB and which is 8 bytes) and plus other field size: 8 + 4 = 12 bytes. Your struct signature is only 8 bytes.

    rgExtension member in CERT_EXTENSIONS structure doesn't accept single CERT_EXTENSION struct, actually it is a pointer to an array of CERT_EXTENSION structs. This means that rgExtension member must be defined as IntPtr.

    Solution:

    Drop your entire code and use examples below.

    Correct struct signature definitions:

    using System;
    using System.Runtime.InteropServices;
    
    namespace TestApp {
        static class Wincrypt {
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public struct CRYPTOAPI_BLOB {
                public UInt32 cbData;
                public IntPtr pbData;
            }
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]    
            public struct CERT_EXTENSION {
                [MarshalAs(UnmanagedType.LPStr)]
                public String pszObjId;
                public Boolean fCritical;
                public CRYPTOAPI_BLOB Value;
            }
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public struct CERT_EXTENSIONS {
                public UInt32 cExtension;
                public IntPtr rgExtension;
            }
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public struct CERT_ALT_NAME_INFO {
                public UInt32 cAltEntry;
                public IntPtr rgAltEntry;
            }
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public struct CERT_ALT_NAME_ENTRY {
                public UInt32 dwAltNameChoice;
                // since there is no direct translation from C-like unions in C#
                // make additional struct to represent union options.
                public CERT_ALT_NAME_UNION Value;
            }
            // create mapping to dwAltNameChoice
            public const UInt32 CERT_ALT_NAME_OTHER_NAME = 1;
            public const UInt32 CERT_ALT_NAME_RFC822_NAME = 2;
            public const UInt32 CERT_ALT_NAME_DNS_NAME = 3;
            public const UInt32 CERT_ALT_NAME_X400_ADDRESS = 4;
            public const UInt32 CERT_ALT_NAME_DIRECTORY_NAME = 5;
            public const UInt32 CERT_ALT_NAME_EDI_PARTY_NAME = 6;
            public const UInt32 CERT_ALT_NAME_URL = 7;
            public const UInt32 CERT_ALT_NAME_IP_ADDRESS = 8;
            public const UInt32 CERT_ALT_NAME_REGISTERED_ID = 9;
    
            [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
            public struct CERT_ALT_NAME_UNION {
                [FieldOffset(0)]
                public IntPtr pOtherName;
                [FieldOffset(0)]
                public IntPtr pwszRfc822Name;
                [FieldOffset(0)]
                public IntPtr pwszDNSName;
                [FieldOffset(0)]
                public CRYPTOAPI_BLOB DirectoryName;
                [FieldOffset(0)]
                public IntPtr pwszURL;
                [FieldOffset(0)]
                public IntPtr IPAddress;
                [FieldOffset(0)]
                public IntPtr pszRegisteredID;
            }
            // not really used in this scenario, but is necessary when want to add
            // UPN alt name, for example.
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            public struct CERT_OTHER_NAME {
                [MarshalAs(UnmanagedType.LPStr)]
                public String pszObjId;
                public CRYPTOAPI_BLOB Value;
            }
        }
    }
    

    CryptEncodeObject signature (haven't tried Ex version):

    using System;
    using System.Runtime.InteropServices;
    
    namespace TestApp {
        static class Crypt32 {
            [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern Boolean CryptEncodeObject(
                [In] UInt32 CertEncodingType,
                [In] UInt32 lpszStructType,
                [In, Out]ref Wincrypt.CERT_ALT_NAME_INFO pvStructInfo,
                [Out] Byte[] pbEncoded,
                [In, Out] ref UInt32 cbEncoded);
        }
    }
    

    and the whole story with comments:

    using System;
    using System.ComponentModel;
    using System.Runtime.InteropServices;
    
    namespace TestApp {
        class Program {
            static void Main(String[] args) {
                //return;
                // suppose we want to add three alternative DNS names to SAN extension
                String[] dnsNames = { "contoso.com", "www.contoso.com", "mail.contoso.com" };
                // calculate size of CERT_ALT_NAME_ENTRY structure. Since it is C-like
                // struct, use Marshal.SizeOf(), not C# sizeof().
                var altEntrySize = Marshal.SizeOf(typeof(Wincrypt.CERT_ALT_NAME_ENTRY));
                // create CERT_ALT_NAME_INFO structure and set initial data:
                // cAltEntry -- number of alt names in the extension
                // rgAltEntry -- starting pointer in unmanaged memory to an array of alt names
                // the size is calculated as: CERT_ALT_NAME_ENTRY size * alt name count
                var altInfo = new Wincrypt.CERT_ALT_NAME_INFO {
                    cAltEntry = (UInt32)dnsNames.Length,
                    rgAltEntry = Marshal.AllocHGlobal(altEntrySize * dnsNames.Length)
                };
                // now create CERT_ALT_NAME_ENTRY for each alt name and copy structure to
                // a pointer allocated in altInfo structure with a shift.
                // Create a loop to save some coding
                for (Int32 i = 0; i < dnsNames.Length; i++) {
                    var altEntry = new Wincrypt.CERT_ALT_NAME_ENTRY {
                        dwAltNameChoice = Wincrypt.CERT_ALT_NAME_DNS_NAME,
                        // use Uni, because the pwszDNSName is defined as LPWStr (unicode)
                        Value = { pwszDNSName = Marshal.StringToHGlobalUni(dnsNames[i]) },
                    };
                    // copy alt name entry to altInfo.rgAltEntry at the specified index.
                    // In unmanaged memory you have to calculate shift based on managed
                    // index and structure size
                    Marshal.StructureToPtr(altEntry, altInfo.rgAltEntry + i * altEntrySize, false);
                }
                // encode CERT_ALT_NAME_INFO to ASN.1 DER byte array
                UInt32 pcbEncoded = 0;
                if (Crypt32.CryptEncodeObject(1, 12, ref altInfo, null, ref pcbEncoded)) {
                    Byte[] encodedSANvalue = new Byte[pcbEncoded];
                    Crypt32.CryptEncodeObject(1, 12, ref altInfo, encodedSANvalue, ref pcbEncoded);
                    // create certificate extension array:
                    var extensions = new Wincrypt.CERT_EXTENSIONS {
                        cExtension = 1,
                        rgExtension = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Wincrypt.CERT_EXTENSION)))
                    };
                    // create SAN extension:
                    var san = new Wincrypt.CERT_EXTENSION {
                        fCritical = false,
                        pszObjId = "2.5.29.17",
                        Value = { cbData = (UInt32)encodedSANvalue.Length, pbData = Marshal.AllocHGlobal(encodedSANvalue.Length) }
                    };
                    // copy SAN bytes to SAN extension:
                    Marshal.Copy(encodedSANvalue,0,san.Value.pbData, encodedSANvalue.Length);
                    // copy CERT_EXTENSION structure to extensions:
                    Marshal.StructureToPtr(san, extensions.rgExtension, false);
                    // use 'extensions' variable in CertCreateSelfSignCertificate call.
                } else {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
            }
        }
    }
    

    One note: the provided code do not release unmanaged resources. You have to release them after calling CertCreateSelfSignCertificate function.