C# 使用主题备用名称生成自签名证书?

C# generate Self Signed Cert with Subject Alternative Name?

我们有一个生成自签名证书的应用程序,但现在 Chrome 58 我们需要添加主题备用名称。证书是使用 C# 生成的,但在 win32 中调用 CertCreateSelfSignCertificate 函数。到目前为止,我发现的所有示例都没有传递扩展参数,而且我发现很难创建一个扩展来传递以生成 SAN。

请注意,我会在它运行后清理它 这就是我用来创建条目然后扩展的内容:

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

使用的结构

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

完整代码 - 从我找到的几个例子拼凑而成

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

好吧,这将是 C#/C++ 互操作的地狱,如果不知道指针、结构和类 C 数组在 C++ 中的工作方式以及编组在互操作中的工作方式,就很难理解。

您错误地定义了 CERT_ALT_NAME_ENTRY 结构。它在 C++ 定义中使用 union。将联合转换为 C# 时,它们必须与联合中的最大结构大小(即 CRYPTOAPI_BLOB 和 8 字节)和其他字段大小对齐:8 + 4 = 12 字节。您的结构签名只有 8 个字节。

rgExtension CERT_EXTENSIONS 结构中的成员不接受单个 CERT_EXTENSION 结构,实际上它是一个指向 CERT_EXTENSION 结构数组的指针。这意味着 rgExtension 成员必须定义为 IntPtr.

解法:

放下整个代码并使用下面的示例。

正确的结构签名定义:

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签名(没试过Ex版):

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

以及整个故事的评论:

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

请注意:提供的代码不会释放非托管资源。您必须在调用 CertCreateSelfSignCertificate 函数后释放它们。