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 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 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)
{
Encoding.Unicode.GetBytes(stringData + '\0'));
}
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 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.
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[]ode>.
string hexThumbprint = certificate.Thumbprint;
Console.WriteLine("SSL Certificate Thumbprint: " + hexThumbprint);
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.
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