Automate client certificate one-to-one mapping in IIS 6.0 using C#

In PSS, we occasionally get requests from our customers wherein they want to automatically add entries for client certificate mapping in IIS or Active Directory (AD). That is either a 1-to-1, Many-to-1 or AD mapping for the client certificate authentication for the web site. I recommend going with AD mapping because that eases the management but it finally depends upon one's need.

I am not sure but I feel there is a security breach plus annoyance when an administrator has to laboriously enter the mappings for all the accounts/certificates (I am being specific to 1-to-1/Many-to-1 here).

The concern I feel when dealing with the administrator doing it for 1-to-1 and Many-to-1 are:

a. If there are hundreds of users you need to do this manually for everyone of those accounts and it's a pain.

b. Yes, the above can be automated using a script but then the second concern that arises is that whoever is running the script has to know the passwords for all these accounts to be mapped. I think this doesn't sound good.

I have written a sample application using which users can enter the mappings themselves in the IIS's Client certificate setting, i.e. entries having the client certificate mapped to a windows account (either a local IIS or AD account) and the corresponding password.

So this is how it works:

  • User accesses this web page from their workstation which already has the client certificate installed.
  • They are authenticated over Basic with SSL.
  • Browser sends across the client certificate as part of the HTTP web request.
  • This application gathers the user account, password plus the client certificate from the incoming HTTP web request and does the mapping in IIS.

 

image

I am adding the code here in case someone may want to extract the section for automated scripting instead of using it as a web app.

This code is also attached to this post as well.

 using System;
 using System.Data;
 using System.Configuration;
 using System.Web;
 using System.Web.Security;
 using System.Security.Cryptography.X509Certificates;
 using System.Web.UI;
 using System.Web.UI.WebControls;
 using System.Web.UI.WebControls.WebParts;
 using System.Web.UI.HtmlControls;
 using System.DirectoryServices;
  
 /* This sample application is to automate One-to-One Client certificate mapping in IIS 6.0.
  * User should be able to access this site from the browser and select the client certificate
  * in their machine which will be mapped to their account on the IIS server for 1-to-1 mapping. 
  * You need to deploy this application on the IIS server which is hosting the website(s) which 
  * needs client certificate mapping, preferably under its own virtual directory.
  *
  * Important:
  * - Have the authentication for this web application configured to use Basic along with SSL.
  * - Have the "Accept client certificates" or "Require client certificates" selected under 
  *   <Website> -> Properties -> Directory Security -> Secure communications -> Edit -> Client certificates
  * - Ensure the website that we want the mapping for is mentioned in the web.config file associated with
  *   this application under <appSettings>
  * - In the Web.config file we are impersonating an Administrator account for this application. 
  *   <identity impersonate="true" userName="Administrator" password="myadminpassword"/>
  *   This is done because non-admin users cannot modify the IIS metabase. If you do not want users to map
  *   entries themselves through web page you can change this to <identity impersonate="true" />.
  *   In such a case only admins can add the mappings for their user accounts. Non-admins won't be able to 
  *   add the client mapping entries.
  *   This is valid for both domain or local Windows NT accounts.
  * - This app is written using .Net 2.0, ASP.Net 2.0 and above in mind. You should be able to make it work
  *   with ASP.Net 1.1 as well.
  * - You may prefer to run this application under its own dedicated application pool to ensure stability and security.
  * 
  * DISCLAIMER: The code is not tested for production scenarios so use it at your own risk. 
  *             In case one wants to use batch scripting etc or some kind of console app instead 
  *             of web app you can extract the code section from this page which should work fine for the job.
  */
  
 public partial class _Default : System.Web.UI.Page 
 {
     protected void Page_Load(object sender, EventArgs e)
       {
         Response.Write("<B>Client Certificate One-to-One Mapping Application:</B><BR><HR>");
         Response.Write("Serial number: " + Request.ClientCertificate.SerialNumber + "</BR><HR>");
         Response.Write("Issuer: " + Request.ClientCertificate.Issuer + "</BR><HR>");
         Response.Write("Subject Name: " + Request.ClientCertificate.Subject + "</BR><HR>");
         if (Request.ClientCertificate.IsPresent)
         {
             Response.Write("Validity<BR>");
             Response.Write("&nbsp;&nbsp;&nbsp;&nbsp;Not before: " + Request.ClientCertificate.ValidFrom + "</BR>");
             Response.Write("&nbsp;&nbsp;&nbsp;&nbsp;Not after: " + Request.ClientCertificate.ValidUntil + "</BR><HR>");
         }
         else
             Response.Write("<B>There is no client certificate sent along with the request!</B><HR>");
  
         Response.Write("Authenticated User: " + Request.ServerVariables["AUTH_USER"] + "</BR><HR>");
         Response.Write("Authentication Type: " + Request.ServerVariables["AUTH_TYPE"] + "</BR><HR>");
     }
     protected void Button1_Click(object sender, EventArgs e)
     {
         string user = Request.ServerVariables["AUTH_USER"];
         string password = Request.ServerVariables["AUTH_PASSWORD"];
         string clientCertMappingName = "Mapping for " + user;  // <--- Our One-to-One Mapping name for the entry
         HttpClientCertificate cert = Request.ClientCertificate;
         /*
           If you want to map a client certificate located on the disk instead of the one as part of the 
           HTTP Web request try the code below.
           
           X509Certificate certificate = X509Certificate2.CreateFromCertFile(@"c:\cert.cer");
           X509Certificate certificate = cert.Certificate;
           byte[] certHash = certificate.GetRawCertData();
         */
         byte[] certHash = Request.ClientCertificate.Certificate;
         try
         {
         //Get the name of the Web site for which mapping has to be done from the App settings in the web.config file.
         string friendlyWebsiteName = ConfigurationManager.AppSettings["websitename"].ToString();
  
         //Get the Site Identifier based on the friendly name of the Web Site.
         string siteId = getsiteid(friendlyWebsiteName);
  
         if (siteId != null)
             {
             string sitePath = "IIS://localhost/W3SVC/" + siteId + "/IIsCertMapper";
             using (DirectoryEntry de = new DirectoryEntry(sitePath))
             {
                 de.Invoke("CreateMapping", new object[] { certHash, user, password, clientCertMappingName, true });
             }
             Response.Write("Account Mapped: <B>" + Request.ServerVariables["AUTH_USER"] + "</B></BR>");
             Response.Write("Mapping Name: <B>" + "Mapping for " + Request.ServerVariables["AUTH_USER"] + "</B></BR>");
             Response.Write("Web Site: <B>" + friendlyWebsiteName + "</B></BR>");
             }
         else
             {
             Response.Write("<B>Web Site does not have a valid Site ID. Ensure we have the correct site name in the config file for this app.</B>");
             }
         }
         
         catch (System.Runtime.InteropServices.COMException)
         {
             Response.Write("A COM exception occurred while setting up the mapping.");
         }
         catch (SystemException)
         {
             Response.Write("An error occurred while setting up the mapping.");
         }
         catch (Exception)
         {
             Response.Write("An error occurred while setting up the mapping.");
         }
        
     }
  
     public string getsiteid(string websitename)
     {
         DirectoryEntry root = new DirectoryEntry("IIS://localhost/W3SVC");
         try
         {
             string siteid = null;
             foreach (DirectoryEntry de in root.Children)
             {
                 if (de.SchemaClassName == "IIsWebServer")
                 {
                     if (websitename.ToUpper() == de.Properties["ServerComment"].Value.ToString().ToUpper())
                         siteid = de.Name;
                 }
             }
             if (siteid == null) return null;
             return siteid;
         }
         catch
         {
             return null;
         }
         finally
         {
             root.Close();
         }
     }
 }

 

Ciao

Nice weekend!

Code.zip