IIS Admin Base Object Wrapper for installing SSL Certificates






4.57/5 (7 votes)
Feb 20, 2004
6 min read

140928

1287
A COM Interop Wrapper for the IIS Admin Base Object that can be used to programmatically install SSL Certificates in IIS 5.0
Introduction
Installing SSL Certificates in IIS 5.0 seems to be an easy task. One would
think to use ADSI to set the SSLCertHash
property using Adsutil.vbs
or System.DirectoryServices
in C#. Although that approach seems
logical, one quickly remembers they are using a Microsoft product and discovers
that the schema incorrectly specifies the SSLCertHash
property as
an expanded null terminated string, instead of as binary data. A quick search of
the MS KB will pull up HOW TO:
Programmatically Install SSL Certificates for Internet Information Server
(IIS) confirming this little idiosyncrasy with ADSI. Great, a COM only
interface (IMSAdminBase
) that is accessible through C/C++ is what
Microsoft leaves us to solve this little task. Sounds nasty.
Using the Microsoft .NET Framework it's possible to create a COM callable
wrapper/runtime callable wrapper (RCW) to allow VBScript and C# to use the
IMSAdminBase
interface. My COM knowledge is very limited and
messing with C was the last thing I wanted to do, so I decided see what COM
Interop in the .NET Framework was all about.
Installing SSL Certificates in IIS 5.0 Programmatically involves the following tasks:
- Generate/Load Certificate into Local Computer Certificate Store
- Get the Certificate Thumbprint
- Set the SSLCertHash and SSLStore Name Metabase Properties
The attached solution contains a C# sample tool and a VBScript sample for installing SSL Certificates using the custom COM callable wrapper/runtime callable wrapper (RCW).
Generating SSL Certificates and the Certificate Store
There are numerous ways to get a SSL Certificate for IIS. This article only covers generating a self-signed certificate. Included in the .NET Framework SDK and the Platform SDK is a tool called makecert.exe that works great for generating fake (self-signed) certificates.
IIS SSL Certificates need the following parameters:
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
The certificate's subject name and expiration date are configurable. The full subject name can include "CN=Name,OU=Container,O=Company,L=City,S=State,C=Country". The -ss my -sr LocalMachine switches save the generated certificate to the Personal Certificate Store (MY) for the Local Computer. If you import your own certificate, make sure its stored there.
Platform SDK Redistributable: CAPICOM
It's possible to access the Local Machine Certificate Store using the CryptoAPI but I found using the CAPICOM COM client to be much easier. You can download the CAPICOM library from the Microsoft download site.
To install CAPICOM extract "CAPICOM.DLL" from CAPICOM.CAB to your system32 directory, then execute "regsvr32.exe CAPICOM.DLL". The debug symbols should also be copied to the system32 directory for running C# projects in debug mode.
Background
Runtime callable wrappers are used by .NET to access COM components. MSDN has
a great deal of documentation on the subject. I would suggest reading some
articles over there if you are interested. You may have of used RCWs without
noticing, Visual Studio .NET generates RCWs for you when add a COM Reference to
a project. Alternatively, you could use the Type Library Importer (Tlbimp.exe)
tool to generate a wrapper from a type library file (TLB). So where is the IIS
Admin Base Object in the COM Reference list? Luckily, our friends at Microsoft
don't seem to ship a TLB for the IMSAdminBase
Interface. The next
step involved searching the Platform SDK for any trace of Interface Definition
Language (IDL) source. IDL source can be compiled by the MIDL compiler
command-line tool to generate the type library file (TLB). Compiling the IDL
quickly becomes difficult when the Platform SDK only provides C header files for
the IMSAdminBase
Interface.
Custom Runtime Callable Wrapper
Armed with the Iadmw.h header file I started to create a custom RCW. The MSAdminBase project contains the COM Interop wrappers. All Interop projects begin with...
using System.Runtime.InteropServices;
Importing COM Interfaces are actually pretty easy.
Interface header file
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 Wrapper
[ComImport, Guid("a9e69610-b80d-11d0-b9b9-00a0c922e750")]
public class MSAdminBase
{}
[ComImport, Guid("70B51430-B6CA-11d0-B9B9-00A0C922E750"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMSAdminBase
{
...Interface Methods
}
The CLSID requires a public class with no constructor. The interface requires
implementing IUnknown
.
The more time consuming aspect of this process is wrapping the Interface methods. I did find out that you don't have to fully wrap the entire method signature, but all the methods need to be declared sequentially in the C# interface wrapper.
Interface Methods
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 Methods
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();
The MarshalAs attribute can be used to tell the CLR how to Marshal objects between .NET and COM. It is not always required but does help when looking at the hungarian notation variable names. All structs used by the interface methods also need a wrapper.
All Constants, Enums, and Structs are defined in IIScnfg.h. The
METADATA_RECORD
structure contains information about a metabase
entry. It is used as an input parameter by the SetData
method and
as an input/output parameter by methods that retrieve data from the metabase in
GetData
.
typedef struct _METADATA_RECORD {
DWORD dwMDIdentifier;
DWORD dwMDAttributes;
DWORD dwMDUserType;
DWORD dwMDDataType;
DWORD dwMDDataLen;
unsigned char *pbMDData;
DWORD dwMDDataTag; }
I highly recommend looking at the NET Framework Developer's Guide
(VS.NET Help): COM Data Types. DWORD
s are
a UInt32 and unsigned char *
is a IntPtr
.
MetaData Marshaling
COM Interop works with unmanaged memory. Effective use of
try..finally
blocks can help with resource cleanup. All memory
allocated with the Marshal class and open handles must be manually freed, the
CLR garbage collector will not do it for you. Since the
METADATA_RECORD
structure contains a pointer to MetaData, all
metabase entry data types will need to be marshaled to unmanaged memory before
calling the Interface method.
From Managed Code to Unmanaged Code
String MetaData
Windows 2000 uses Unicode strings (2 bytes per character) and the
METADATA_RECORD
structure requires the MetaData length field to
include the null-terminated character.
stringData += '\0';
metaDataRecord.dwMDDataLen =
(UInt32)Encoding.Unicode.GetByteCount(stringData);
metaDataRecord.pbMDData = Marshal.StringToCoTaskMemUni(stringData);
Binary MetaData
Marshalling binary MetaData is simple. Use the Marshal.Copy
method.
metaDataRecord.dwMDDataLen = (UInt32)binaryData.Length;
metaDataRecord.pbMDData = Marshal.AllocCoTaskMem(binaryData.Length);
Marshal.Copy(binaryData, 0, metaDataRecord.pbMDData,
(int)metaDataRecord.dwMDDataLen);
MultiSz MetaData
MultiSz MetaData is marshaled as a string array of null-terminated strings that has final null-terminated character after the last element. I had trouble with the null-terminated characters so I marshalled this data type as binary MetaData
ArrayList multiSzData = new ArrayList();
foreach(string stringData in stringArrayData)
{
// (Add null// (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
// Copy Binary Data to Unmanaged Memory
Marshal.Copy(binaryData, 0, metaDataRecord.pbMDData,
(int)metaDataRecord.dwMDDataLen);
DWORD MetaData
DWORD MetaData can be marshaled by allocating 4 bytes of unmanaged memory and
calling the Marshal.WriteInt32
method.
metaDataRecord.dwMDDataLen = (uint)Marshal.SizeOf(typeof(UInt32));
metaDataRecord.pbMDData = Marshal.AllocCoTaskMem(
(int)metaDataRecord.dwMDDataType);
Marshal.WriteInt32(metaDataRecord.pbMDData, uintData);;
Freeing Unmanged Memory
Always use finally blocks to free unmanaged memory.
finally
{
if(metaDataRecord.pbMDData != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(metaDataRecord.pbMDData);
}
}
Using the code
The COM Interop project MSAdminBase can be used as runtime callable wrapper for .NET projects or a COM callable wrapper for VB/VBScript projects.
C# - Using the Runtime Callable Wrapper
Using the MSAdminBaseClass (MSAdminBase.dll) to programatically install SSL certificates is simple. Note: The CAPICOM dll must be registered for this sample to work.
- Declare the namespace and Add a reference to either the project or the
compiled dll interop.MSAdminBase.dll.
using Windows.Services.Iis.Metabase;
- Instantiate a new MSAdminBaseClass
- Call the
MSAdminBaseClass.SetMetabaseData
method for the SSLCertHash (5506) metabase entry and the SSLStoreName (5511) metabase entry.
// Open Metabase Interface
MSAdminBaseClass adminBaseClass = new MSAdminBaseClass();
// Set SSL Certificate
adminBaseClass.SetMetabaseData(SslCertHashId,
metaDataPath, thumbprintByteArray);
adminBaseClass.SetMetabaseData(SslStoreNameId, metaDataPath, "MY");
The method signature for SetMetabaseData is
public void SetMetabaseData(uint metabaseDataId, string
metabaseDataPath, object data)
The SSLCertHash entry is a binary MetaData type and requires a
certificate thumbprint as a byte[]
. The CAPICOM COM library
includes a few useful classes to convert from a thumbprint for hex string to a
byte[]ode>.
// // 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);
The SSLStoreName entry in a string MetaData type and should always be "MY" for IIS SSL Certificates.
VBScript - Using the COM Callable Wrapper
- Copy interop.MSAdminBase.dll to the system32 directory and run RegAsm.exe interop.MSAdminBase.dll /tlb:interop.MSAdminBase.tlb
CreateObject("IIS.MSAdminBase")
- Call the
MSAdminBaseClass.SetMetabaseData
method for the SSLCertHash (5506) metabase entry and the SSLStoreName (5511) metabase entry.
' Open Metabase
Dim metaBase
Set metaBase = CreateObject("IIS.MSAdminBase")
' Set SSL Certificate
metaBase.SetMetabaseData SSLCertHashId, "/W3SVC/1", thumbprintByteArray
metaBase.SetMetabaseData SSLStoreNameId, "/W3SVC/1", SSLStoreName
Points of Interest
The COM Interop wrapper can be customized to support any IMSAdminBase
method. I only implemented the methods required to install SSL
Certificates. ADSI should normally be used to configure the IIS Metabase but
this wrapper comes in handy for the few times it's not possible.
History
- 2/8/2004 - Initial Release