Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

IIS Admin Base Object Wrapper for installing SSL Certificates

0.00/5 (No votes)
12 Feb 2004 1  
A COM Interop wrapper for the IIS Admin Base Object that can be used to programmatically install SSL Certificates in IIS 5.0.

MSAdminBase.jpg

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 /* [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 COM Data Types (VS.NET Help). DWORDs 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)
{
    // (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 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.

  1. Declare the namespace and add a reference to either the project or the compiled DLL interop.MSAdminBase.dll.
    using Windows.Services.Iis.Metabase;
  2. Instantiate a new MSAdminBaseClass
  3. 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[].

// 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 is a string MetaData type and should always be "MY" for IIS SSL Certificates.

VBScript - Using the COM Callable Wrapper

  1. Copy interop.MSAdminBase.dll to the system32 directory and run RegAsm.exe interop.MSAdminBase.dll /tlb:interop.MSAdminBase.tlb
  2. CreateObject("IIS.MSAdminBase")
  3. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here