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 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 to 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". I used %ComputerName% to generate a certificate with a subject name that uses the ComputerName
environment variable. 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 it's 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 often used RCWs without noticing. Visual Studio .NET generates RCWs for you when you 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 HRESULT STDMETHODCALLTYPE SetData(
METADATA_HANDLE hMDHandle,
LPCWSTR pszMDPath,
PMETADATA_RECORD pmdrMDData) = 0;
virtual HRESULT STDMETHODCALLTYPE GetData(
METADATA_HANDLE hMDHandle,
LPCWSTR pszMDPath,
PMETADATA_RECORD pmdrMDData,
DWORD *pdwMDRequiredDataLen) = 0
virtual HRESULT STDMETHODCALLTYPE AddKey(
METADATA_HANDLE hMDHandle,
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);
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 struct
s used by the interface methods also need a wrapper.
All constants, enum
s, and struct
s 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 COM Data Types (VS.NET Help). DWORD
s are a UInt32
and unsigned char *
is an 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 marshaled this data type as binary MetaData.
ArrayList multiSzData = new ArrayList();
foreach(string stringData in stringArrayData)
{
multiSzData.AddRange(Encoding.Unicode.GetBytes(stringData + '\0'));
}
multiSzData.AddRange(new byte[2]{0x00,0x00});
binaryData = (byte[])multiSzData.ToArray(Type.GetType("System.Byte"));
metaDataRecord.dwMDDataLen = (UInt32)binaryData.Length;
metaDataRecord.pbMDData = Marshal.AllocCoTaskMem(binaryData.Length);
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 Unmanaged 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 a 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 programmatically 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.
MSAdminBaseClass adminBaseClass = new MSAdminBaseClass();
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[]
.
string hexThumbprint = certificate.Thumbprint;
Console.WriteLine("SSL Certificate Thumbprint: " + hexThumbprint);
Utilities certUtilities = new Utilities();
string binaryThumbprint = certUtilities.HexToBinary(hexThumbprint);
thumbprintByteArray =
(byte[])certUtilities.BinaryStringToByteArray(binaryThumbprint);
The SSLStoreName entry is 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.
Dim metaBase
Set metaBase = CreateObject("IIS.MSAdminBase")
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.