用于安装 SSL 证书的 IIS 管理基对象包装器






4.44/5 (9投票s)
2004年2月13日
6分钟阅读

65530

1460
一个用于 IIS Admin Base Object 的 COM 互操作包装器,可用于以编程方式安装 IIS 5.0 中的 SSL 证书。
引言
在 IIS 5.0 中以编程方式安装 SSL 证书似乎是一项简单的任务。人们可能会认为可以使用 ADSI 使用 Adsutil.vbs 或 C# 中的 System.DirectoryServices
来设置 SSLCertHash
属性。虽然这种方法看起来很合乎逻辑,但很快就会想起您使用的是 Microsoft 产品,并发现架构错误地将 SSLCertHash
属性指定为扩展的空终止字符串,而不是二进制数据。快速搜索 MS KB 会找到 如何:为 Internet 信息服务 (IIS) 以编程方式安装 SSL 证书,这证实了 ADSI 的这个小怪癖。太好了,Microsoft 为我们解决这个小任务提供了一个只能通过 C/C++ 访问的 COM 接口(IMSAdminBase
)。听起来很棘手。
使用 Microsoft .NET Framework,可以创建一个 COM 可调用包装器/运行时可调用包装器 (RCW),以允许 VBScript 和 C# 使用 IMSAdminBase
接口。我对 COM 的了解非常有限,而且我最不想做的就是弄 C,所以我决定看看 .NET Framework 中的 COM 互操作究竟是什么。
以编程方式在 IIS 5.0 中安装 SSL 证书涉及以下任务
- 将证书生成/加载到本地计算机证书存储中
- 获取证书缩略图
- 设置 SSLCertHash 和 SSLStore Name 元数据库属性
随附的解决方案包含一个 C# 示例工具和一个 VBScript 示例,用于使用自定义的 COM 可调用包装器/运行时可调用包装器 (RCW) 安装 SSL 证书。
生成 SSL 证书和证书存储
有多种方法可以获取 IIS 的 SSL 证书。本文仅涵盖生成自签名证书。在 .NET Framework SDK 和 Platform SDK 中包含了一个名为 makecert.exe 的工具,该工具非常适合生成假的(自签名的)证书。
IIS SSL 证书需要以下参数
makecert.exe -a SHA1 -ss my -sr LocalMachine -n
"CN="%ComputerName% -b 01/01/2000 -e 01/01/2050
-eku 1.3.6.1.5.5.7.3.1 -sky exchange
-sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12
证书的主题名称和过期日期是可配置的。完整的主题名称可以包含“CN=Name,OU=Container,O=Company,L=City,S=State,C=Country”。我使用了 %ComputerName% 来生成一个使用 ComputerName
环境变量的主题名称的证书。-ss my -sr LocalMachine 开关将生成的证书保存在本地计算机的个人证书存储 (MY) 中。如果您导入自己的证书,请确保将其存储在此处。
Platform SDK 可再发行组件:CAPICOM
可以使用 CryptoAPI 访问本地计算机证书存储,但我发现使用 CAPICOM COM 客户端要容易得多。您可以从 Microsoft 下载站点 下载 CAPICOM 库。
要安装 CAPICOM,请将 CAPICOM.DLL 从 CAPICOM.CAB 提取到您的 system32 目录,然后执行“regsvr32.exe CAPICOM.DLL”。调试符号也应复制到 system32 目录,以便在调试模式下运行 C# 项目。
背景
运行时可调用包装器由 .NET 用于访问 COM 组件。MSDN 提供了大量关于该主题的文档。如果您有兴趣,我建议您阅读那里的几篇文章。您可能经常在不知情的情况下使用 RCW。Visual Studio .NET 在您向项目添加 COM 引用时会自动为您生成 RCW。或者,您也可以使用IMSAdminBase
接口提供 TLB。下一步是搜索 Platform SDK 中是否有接口定义语言 (IDL) 源的痕迹。IDL 源可以通过 MIDL 编译器命令行工具进行编译,以生成类型库文件 (TLB)。当 Platform SDK 只为 IMSAdminBase
接口提供 C 头文件时,编译 IDL 会很快变得困难。
自定义运行时可调用包装器
有了 Iadmw.h 头文件,我开始创建一个自定义 RCW。MSAdminBase 项目包含 COM 互操作包装器。所有互操作项目都以...开始
using System.Runtime.InteropServices;
导入 COM 接口实际上非常简单。
接口头文件
DEFINE_GUID(CLSID_MSAdminBase_W, 0xa9e69610, 0xb80d, 0x11d0, 0xb9, 0xb9, 0x0, 0xa0, 0xc9, 0x22, 0xe7, 0x50); #if defined(__cplusplus) && !defined(CINTERFACE) MIDL_INTERFACE("70B51430-B6CA-11d0-B9B9-00A0C922E750") IMSAdminBaseW : public IUnknown { ...Interface Methods... END_INTERFACE }
.NET 包装器
[ComImport, Guid("a9e69610-b80d-11d0-b9b9-00a0c922e750")]
public class MSAdminBase {}
[ComImport, Guid("70B51430-B6CA-11d0-B9B9-00A0C922E750"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMSAdminBase {
...Interface Methods
}
CLSID
需要一个公共类,并且没有构造函数。该接口需要实现 IUnknown
。
这个过程中更耗时的是包装接口方法。我确实发现您不必完全包装整个方法签名,但所有方法都需要按顺序在 C# 接口包装器中声明。
接口方法
virtual /* [local] */ HRESULT STDMETHODCALLTYPE SetData( /* [in] */ METADATA_HANDLE hMDHandle, /* [string][in][unique] */ LPCWSTR pszMDPath, /* [in] */ PMETADATA_RECORD pmdrMDData) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE GetData( /* [in] */ METADATA_HANDLE hMDHandle, /* [string][in][unique] */ LPCWSTR pszMDPath, /* [out][in] */ PMETADATA_RECORD pmdrMDData, /* [out] */ DWORD *pdwMDRequiredDataLen) = 0
virtual HRESULT STDMETHODCALLTYPE AddKey( /* [in] */ METADATA_HANDLE hMDHandle, /* [string][in][unique] */ LPCWSTR pszMDPath
.NET 方法
void SetData(IntPtr hMDHandle,
[MarshalAs(UnmanagedType.LPWStr)] String pszMDPath,
ref METADATA_RECORD pmdrMDData);
void GetData(IntPtr hMDHandle,
[MarshalAs(UnmanagedType.LPWStr)] String pszMDPath,
[MarshalAs(UnmanagedType.Struct)] ref METADATA_RECORD pmdrMDData,
out UInt32 pdwMDRequiredDataLen);
// Skipped
void AddKey();
MarshalAs
属性可用于告知 CLR 如何在 .NET 和 COM 之间进行对象封送。它并非总是必需的,但在查看匈牙利命名法变量名时会很有帮助。接口方法使用的所有 struct
s 也需要一个包装器。
所有常量、enum
s 和 struct
s 都在 IIScnfg.h 中定义。METADATA_RECORD
结构包含有关元数据库条目的信息。它用作 SetData
方法的输入参数,以及用于从 GetData
方法中检索元数据库数据的输入/输出参数。
typedef struct _METADATA_RECORD { DWORD dwMDIdentifier; DWORD dwMDAttributes; DWORD dwMDUserType; DWORD dwMDDataType; DWORD dwMDDataLen; unsigned char *pbMDData; DWORD dwMDDataTag; }
我强烈建议查看 NET Framework Developer's Guide COM Data Types (VS.NET Help)。DWORD
是一个 UInt32
,而无符号 char *
是一个 IntPtr
。
元数据封送
COM 互操作处理非托管内存。有效使用 try
..finally
块有助于资源清理。使用 Marshal
类分配的所有内存和打开的句柄必须手动释放,CLR 垃圾回收器不会为您代劳。由于 METADATA_RECORD
结构包含指向 MetaData 的指针,因此所有元数据库条目数据类型在调用接口方法之前都需要封送到非托管内存。
从托管代码到非托管代码
字符串元数据
Windows 2000 使用 Unicode 字符串(每个字符 2 字节),并且 METADATA_RECORD
结构要求 MetaData 长度字段包含空终止字符。
stringData += '\0';
metaDataRecord.dwMDDataLen = (UInt32)Encoding.Unicode.GetByteCount(stringData);
metaDataRecord.pbMDData = Marshal.StringToCoTaskMemUni(stringData);
二进制元数据
封送二进制元数据很简单。使用 Marshal.Copy
方法。
metaDataRecord.dwMDDataLen = (UInt32)binaryData.Length;
metaDataRecord.pbMDData = Marshal.AllocCoTaskMem(binaryData.Length);
Marshal.Copy(binaryData, 0, metaDataRecord.pbMDData,
(int)metaDataRecord.dwMDDataLen);
MultiSz 元数据
MultiSz 元数据被封送为一系列空终止字符串,在最后一个元素之后有一个最终的空终止字符。我在空终止字符上遇到了麻烦,所以我将此数据类型封送为二进制元数据。
ArrayList multiSzData = new ArrayList();
foreach(string stringData in stringArrayData)
{
// (Add null terminated)
multiSzData.AddRange(Encoding.Unicode.GetBytes(stringData + '\0'));
}
// (Add null terminated)
multiSzData.AddRange(new byte[2]{0x00,0x00});
binaryData = (byte[])multiSzData.ToArray(Type.GetType("System.Byte"));
// Allocate Binary Data Memory
metaDataRecord.dwMDDataLen = (UInt32)binaryData.Length;
metaDataRecord.pbMDData = Marshal.AllocCoTaskMem(binaryData.Length);
// Copy Binary Data to Unmanaged Memory
Marshal.Copy(binaryData, 0, metaDataRecord.pbMDData,
(int)metaDataRecord.dwMDDataLen);
DWORD 元数据
DWORD
元数据可以通过分配 4 字节的非托管内存并调用 Marshal.WriteInt32
方法来封送。
metaDataRecord.dwMDDataLen = (uint)Marshal.SizeOf(typeof(UInt32));
metaDataRecord.pbMDData =
Marshal.AllocCoTaskMem((int)metaDataRecord.dwMDDataType);
Marshal.WriteInt32(metaDataRecord.pbMDData, uintData);
释放非托管内存
始终使用 finally
块来释放非托管内存。
finally
{
if(metaDataRecord.pbMDData != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(metaDataRecord.pbMDData);
}
}
使用代码
COM 互操作项目 MSAdminBase 可用作 .NET 项目的运行时可调用包装器,也可作为 VB/VBScript 项目的 COM 可调用包装器。
C# - 使用运行时可调用包装器
使用 MSAdminBaseClass
(MSAdminBase.dll)以编程方式安装 SSL 证书非常简单。注意:必须注册 CAPICOM DLL 才能使此示例正常工作。
- 声明命名空间并将引用添加到项目或已编译的 DLL interop.MSAdminBase.dll。
using Windows.Services.Iis.Metabase;
- 实例化一个新的
MSAdminBaseClass
- 调用
MSAdminBaseClass.SetMetabaseData
方法,分别用于 **SSLCertHash** (5506) 元数据库条目和 **SSLStoreName** (5511) 元数据库条目。
// Open Metabase Interface
MSAdminBaseClass adminBaseClass = new MSAdminBaseClass();
// Set SSL Certificate
adminBaseClass.SetMetabaseData(SslCertHashId, metaDataPath,
thumbprintByteArray);
adminBaseClass.SetMetabaseData(SslStoreNameId, metaDataPath, "MY");
SetMetabaseData
方法签名是
public void SetMetabaseData(uint metabaseDataId,
string metabaseDataPath, object data)
**SSLCertHash** 条目是二进制元数据类型,需要证书缩略图作为 byte[]
。CAPICOM COM 库包含一些有用的类,可以将十六进制字符串的缩略图转换为 byte[]
。
// Get Hex String Thumbprint
string hexThumbprint = certificate.Thumbprint;
Console.WriteLine("SSL Certificate Thumbprint: " + hexThumbprint);
// Convert Hex String to Byte[]
Utilities certUtilities = new Utilities();
string binaryThumbprint = certUtilities.HexToBinary(hexThumbprint);
thumbprintByteArray =
(byte[])certUtilities.BinaryStringToByteArray(binaryThumbprint);
**SSLStoreName** 条目是字符串元数据类型,对于 IIS SSL 证书应始终为“MY”。
VBScript - 使用 COM 可调用包装器
- 将 interop.MSAdminBase.dll 复制到 system32 目录并运行 RegAsm.exe interop.MSAdminBase.dll /tlb:interop.MSAdminBase.tlb
CreateObject("IIS.MSAdminBase")
- 调用
MSAdminBaseClass.SetMetabaseData
方法,分别用于 **SSLCertHash** (5506) 元数据库条目和 **SSLStoreName** (5511) 元数据库条目。
' Open Metabase
Dim metaBase
Set metaBase = CreateObject("IIS.MSAdminBase")
' Set SSL Certificate
metaBase.SetMetabaseData SSLCertHashId, "/W3SVC/1", thumbprintByteArray
metaBase.SetMetabaseData SSLStoreNameId, "/W3SVC/1", SSLStoreName
关注点
COM 互操作包装器可以定制以支持任何 IMSAdminBase
方法。我只实现了安装 SSL 证书所需的方法。通常应使用 ADSI 配置 IIS 元数据库,但此包装器在少数无法使用 ADSI 的情况下非常有用。
历史
2004 年 2 月 8 日 - 首次发布。