Theresa Hardy
Theresa Hardy

Reputation: 23

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

Upvotes: 2

Views: 3038

Answers (2)

John Gietzen
John Gietzen

Reputation: 49544

I know this is very old, but I had a similar requirement, To Create a Self-Signed SSL Cert for Kestral in C# dotnet core. This is what I've gotten working:

    .ConfigureKestrel(serverOptions =>
    {
        serverOptions.ConfigureHttpsDefaults(listenOptions =>
        {
            listenOptions.ServerCertificate = GetOrAddSelfSigned("cn=MyCert", StoreName.My, StoreLocation.CurrentUser);
        });
    })

...

    private static X509Certificate2 GetOrAddSelfSigned(string subjectName, StoreName storeName, StoreLocation storeLocation)
    {
        var now = DateTimeOffset.Now;
        using (var store = new X509Store(storeName, storeLocation))
        {
            store.Open(OpenFlags.ReadWrite);

            var cert = (from X509Certificate2 c in store.Certificates
                        where c.Subject.Equals(subjectName, StringComparison.OrdinalIgnoreCase)
                        where c.NotBefore <= now && now < c.NotAfter
                        select c).FirstOrDefault();
            if (cert == null)
            {
                using (var key = RSA.Create(2048))
                {
                    var request = new CertificateRequest(subjectName, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

                    request.CertificateExtensions.Add(
                        new X509KeyUsageExtension(
                            X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature,
                            critical: false));

                    request.CertificateExtensions.Add(
                        new X509EnhancedKeyUsageExtension(
                            new OidCollection
                            {
                                new Oid("1.3.6.1.5.5.7.3.1"),
                            },
                            critical: false));

                    var san = new SubjectAlternativeNameBuilder();
                    san.AddIpAddress(IPAddress.Loopback);
                    san.AddIpAddress(IPAddress.IPv6Loopback);
                    san.AddDnsName("localhost");
                    san.AddDnsName(Environment.MachineName);
                    request.CertificateExtensions.Add(san.Build());

                    var password = Guid.NewGuid().ToString();
                    cert = request.CreateSelfSigned(now.AddMinutes(-4), now.AddYears(1));
                    cert = new X509Certificate2(cert.Export(X509ContentType.Pfx, password), password, X509KeyStorageFlags.MachineKeySet);

                    store.Add(cert);
                }
            }

            return cert;
        }

Largely adapted from:

How to create a valid, self-signed X509Certificate2 programmatically, not loading from file in .NET Core

Upvotes: 0

Crypt32
Crypt32

Reputation: 13944

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.

Upvotes: 2

Related Questions