Monday 19 November 2012

Dev tool to configure One to One Client Certificate Mappings in IIS 7.5

In my last post, I described a rather tedious and error prone method for configuring One To One client certificate mappings in IIS 7.5, so with a little bit of help I created a small WPF application that should ensure the smooth configuration of one to one client certificate mappings.

There is no validation as this is simply a developer tool and I'm too lazy to add it, you can have a look at this post for an example of textbox validation in WPF.

Xaml
<Window x:Class="IISCERTTOOL.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        Title="MainWindow" Height="302" Width="477">

    <Grid>
        <Button Content="Add One to One Certificate Mapping" Height="23" HorizontalAlignment="Left" Margin="242,212,0,0" Name="Add" VerticalAlignment="Top" Width="191" Click="Add_Click" />
        <Label Content="UserName" Height="28" HorizontalAlignment="Left" Margin="46,50,0,0" Name="label1" VerticalAlignment="Top" Width="81" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="163,50,0,0" Name="UserName" VerticalAlignment="Top" Width="241" />
        <Label Content="Password" Height="28" HorizontalAlignment="Left" Margin="46,84,0,0" Name="label2" VerticalAlignment="Top" Width="81" />
        <PasswordBox Height="23" HorizontalAlignment="Left" Margin="163,89,0,0" Name="Password" VerticalAlignment="Top" Width="241" />
        <Label Content="Certificate" Height="28" HorizontalAlignment="Left" Margin="46,118,0,0" Name="label3" VerticalAlignment="Top" Width="81" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="163,123,0,0" Name="Certificate" VerticalAlignment="Top" Width="241" />
        <Label Content="WebSite" Height="28" HorizontalAlignment="Left" Margin="46,152,0,0" Name="label4" VerticalAlignment="Top" Width="81" />
        <ComboBox Height="23" HorizontalAlignment="Left" Margin="163,152,0,0" Name="WebSite" VerticalAlignment="Top" Width="241" />
        <Button Content="..." Height="20" HorizontalAlignment="Left" Margin="416,123,0,0" Name="SelectCertificate" VerticalAlignment="Top" Width="17" FontStyle="Normal" Click="SelectCertificate_Click" />
    </Grid>
</Window>

Code behind.
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;
 using Microsoft.Web.Administration;
 using System.IO;
 using System.Diagnostics;
 
 
 namespace IISCERTTOOL
 {
     /// <summary>
     /// Interaction logic for MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
         public MainWindow()
         {
             InitializeComponent();
 
             LoadWebSites();
         }
 
         private void SelectCertificate_Click(object sender, RoutedEventArgs e)
         {
             try
             {
                 Microsoft.Win32.OpenFileDialog dialog = new Microsoft.Win32.OpenFileDialog();
 
                 dialog.AddExtension = true;
                 dialog.CheckFileExists = true;
                 dialog.Filter = "cer files (*.cer)|*.cer";
                 dialog.Multiselect = false;
 
                 if ((bool)dialog.ShowDialog())
                 {
                     Certificate.Text = dialog.FileName;
                 }
             }
             catch (Exception ex)
             {
                 DisplayException(ex);
             }
 
         }
 
         private void Add_Click(object sender, RoutedEventArgs e)
         {
             try
             {
                 string publicKey = ReadKey(Certificate.Text.Trim());
 
                 if (!string.IsNullOrEmpty(publicKey) && ConfigureOneToOneCertMapping(UserName.Text.Trim(), Password.Password.Trim(), publicKey, WebSite.SelectedValue.ToString()))
                 {
                     MessageBox.Show("One to One Mapping successfully added");
                 }
                 else
                 {
                     MessageBox.Show("One to One Mapping was not added");
                 }
             }
             catch (Exception ex)
             {
                 DisplayException(ex);
             }
         }
         /// <summary>
         /// Read certificate file into a string
         /// </summary>
         private string ReadKey(string path)
         {
             StringBuilder publicKey = new StringBuilder();
 
             try
             {
                 string[] file = File.ReadAllLines(path).Skip(1).ToArray();
 
                 for (int i = 0; i < file.Length - 1; i++)
                 {
                     publicKey.Append(file[i]);
                 }
             }
             catch (Exception ex)
             {
                 DisplayException(ex);
             }
 
             return publicKey.ToString();
         }
 
         /// <summary>
         /// Grab all Websites on the server and populate the dropdown combobox.
         /// </summary>
         private void LoadWebSites()
         {
             try
             {
                 List<string> sites = new List<string>();
 
                 using (ServerManager serverManager = new ServerManager())
                 {
                     foreach (Site s in serverManager.Sites)
                     {
                         sites.Add(s.Name);
                     }
                 }
 
                 WebSite.ItemsSource = sites;
             }
             catch (Exception ex)
             {
                 DisplayException(ex);
             }
 
         }
 
         /// <summary>
         /// Configure OneToOne Certificate Mapping for client authenticated SSL/TLS
         /// </summary>
         private bool ConfigureOneToOneCertMapping(string UserName, string Password, string PublicKey, string WebSiteName)
         {
             bool result = false;
 
             using (ServerManager serverManager = new ServerManager())
             {
                 try
                 {
                     Configuration config = serverManager.GetApplicationHostConfiguration();
 
                     ConfigurationSection iisClientCertificateMappingAuthenticationSection = config.GetSection("system.webServer/security/authentication/iisClientCertificateMappingAuthentication", WebSiteName);
                     iisClientCertificateMappingAuthenticationSection["enabled"] = true;
                     iisClientCertificateMappingAuthenticationSection["oneToOneCertificateMappingsEnabled"] = true;
 
                     ConfigurationElementCollection oneToOneMappingsCollection = iisClientCertificateMappingAuthenticationSection.GetCollection("oneToOneMappings");
                     ConfigurationElement addElement = oneToOneMappingsCollection.CreateElement("add");
                     addElement["enabled"] = true;
                     addElement["userName"] = UserName;
                     addElement["password"] = Password;
                     addElement["certificate"] = PublicKey;
                     oneToOneMappingsCollection.Add(addElement);
 
                     ConfigurationSection accessSection = config.GetSection("system.webServer/security/access", WebSiteName);
                     accessSection["sslFlags"] = @"Ssl, SslNegotiateCert";
 
                     serverManager.CommitChanges();
 
                     result = true;
                 }
                 catch (Exception ex)
                 {
                     DisplayException(ex);
                 }
 
                 return result;
 
             }
 
         }
 
         /// <summary>
         /// Gets details of exception and displays them in a textbox.
         /// </summary>
         private static void DisplayException(Exception ex)
         {
             StringBuilder sb = new StringBuilder();
 
             StackTrace trace = new StackTrace();
 
             sb.AppendLine("Exception Occurred.");
 
             sb.AppendLine(string.Format("{0}.{1}",
                 trace.GetFrame(1).GetMethod().ReflectedType.Name, trace.GetFrame(1).GetMethod().Name));
 
 
             sb.AppendLine(string.Format("Source: {0}.", ex.Source));
             sb.AppendLine(string.Format("Type: {0}.", ex.GetType()));
             sb.AppendLine(string.Format("Message: {0}.", ex.Message));
 
             if (ex.InnerException != null)
             {
                 sb.AppendLine(string.Format("Inner Exception: {0}.", ex.InnerException.Message));
             }
 
             MessageBox.Show(sb.ToString());
         }
     }
 }

Saturday 17 November 2012

Configure SSL Mutual (Two-way) Authentication in IIS 7.5 using client certificates (One-to-One Mapping)

I do know that it's not possible to have SSL mutual authentication without using client certificates, but I thought that I'd throw as many  definitions as possible in a shameless effort to gain more traffic from Google.
At any rate, in this post I discuss how to set up mutual authentication, two-way authentication or SSL with client certificates, whichever way you call it, for the record in Wikepedia it's termed client-authenticated handshake, so I guess it should be a called client authenticated SSL.

Firstly, there are a couple of pre-requisites.
  1. Server Certificate from a trusted CA.
  2. Client Certificate from a trusted CA.
I would suggest following my excellent series on CAs, which starts here, but alas it's mostly oriented for IIS 6, so it's not exactly terribly useful, it does create a CA which is the basis but not much more. Similarly, this post details the usage of makecert to create self-signed certificates but again it's geared towards IIS 6, the certificate generation commands will work though. You can also use openSSL, details here to create self-signed certificates.

I going to assume that the server certificate has already been installed on the server and assigned to a website, see this post for details.

The first thing to do is to navigate to the configuration editor form IIS Manager (This can be invoked by running inetmgr)
Select system.webServer/security/authentication/iisClientCertificateMappingAuthentication.
Now is when things get a little bit strange. Click on the ellipsis button to be presented  with this screen.
     
The very long sequence of letters is the base64 representation of the client certificate and the simplest way of obtaining this is to export the client certificate as base64 certificate (.cer) and then copy the contents of the file.
So that from this "certificate":
-----BEGIN CERTIFICATE-----
MIIFcjCCBVVgVwIBVgIKG4kc/QVVVVVVNzVNBgkqckiG9w0BVQUFVDBCMRMwEQYK
CZImiZPyLGQBGRYDY29tMRMwEQYKCZImiZPyLGQBGRYDZGV2MRYwFVYDVQQDEw1U
-----END CERTIFICATE-----
The following would need to be pasted. It is imperative that this is in one line.
MIIFcjCCBVVgVwIBVgIKG4kc/QVVVVVVNzVNBgkqckiG9w0BVQUFVDBCMRMwEQYK
CZImiZPyLGQBGRYDY29tMRMwEQYKCZImiZPyLGQBGRYDZGV2MRYwFVYDVQQDEw1U
The username and password are the credentials that will be used when the client certificate is used to authenticate, if using a domain account remember to include the domain e.g. test\testaccount 

Finally, ensure that you set the correct SSL Settings are set for the website.


This should ensure that only SSL Client Authenticated access is allowed to the server.

I must say that I'm a bit disappointed by the whole process, it's bad enough to have to play about with the actual certificate itself in all its base 64 glory, but what the fuxx0r Microsoft, passwords in the clear???

This is doubly annoying because, it's not stored in clear text in the configuration file (C:\Windows\System32\inetsrv\config\applicationHost.config), in this file it's encrypted with AES, so why show in the clear from the GUI?

Not entirely sure why this has been changed  from IIS 6, but this just goes to show that higher software versions are not always better, see this post to see how IIS 6 did not have any of this rubbish.

Clear Certificate Revokation List (CRL) Cache

While investigating yesterday's post I had to republish the CRL a few times, the issue was that it would not be refreshed on the server as well, which was really annoying, particularly because it took me a few minutes to work out what was going on. At any rate, I found that there is a very simple command that clears the cache:
C:\>certutil -setreg chain\ChainCacheResyncFiletime @now
Software\Microsoft\Cryptography\OID\EncodingType 0\CertDllCreateCertificateChain
Engine\Config\ChainCacheResyncFiletime:

Old Value:
  ChainCacheResyncFiletime REG_BINARY = 17/11/2012 10:45

New Value:
  ChainCacheResyncFiletime REG_BINARY = 17/11/2012 10:47
CertUtil: -setreg command completed successfully.
The CertSvc service may need to be restarted for changes to take effect.

Friday 16 November 2012

Disable Certificate Revokation List (CRL) Checking in IIS 7.x for client certificates

Roughly a year ago I was pulling my hair out trying to sort out some SSL issues with IIS 6, one of which necessitated disabling CRL checking and I thought that I should find out how to do the same in IIS 7.x, so here it is (I realize that I should try to find out what has changed for IIS 8, now):

I created a domain certificate request from IIS, assigned the certificate to a website and then run the following command, which shows the current state of the binding.
C:\>netsh http show sslcert

SSL Certificate bindings:
-------------------------

    IP:port                 : 0.0.0.0:443
    Certificate Hash        : 86fc14086c953edac86b8d8f9022c8baae2ad6f6
    Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
    Certificate Store Name  : MY
    Verify Client Certificate Revocation    : Enabled
    Verify Revocation Using Cached Client Certificate Only    : Disabled
    Usage Check    : Enabled
    Revocation Freshness Time : 0
    URL Retrieval Timeout   : 0
    Ctl Identifier          : (null)
    Ctl Store Name          : (null)
    DS Mapper Usage    : Disabled
    Negotiate Client Certificate    : Disabled
Annoyingly, there isn't a modify flag, which means that the certificate binding needs to be deleted first and then re-added.
So first, the certificate binding must be deleted:
C:\>netsh http delete sslcert ipport=0.0.0.0:443

SSL Certificate successfully deleted
Then it must be re-added:
C:\>netsh http add sslcert ipport=0.0.0.0:443 certhash=86fc14086c953edac86b8d8f9022c8baae2ad6f6
appid={4dc3e181-e14b-4a21-b022-59fc669b0914}
certstore=MY verifyclientcertrevocation=disable

SSL Certificate successfully added
A final check to ensure that this has worked:
C:\>netsh http show sslcert

SSL Certificate bindings:
-------------------------

    IP:port                 : 0.0.0.0:443
    Certificate Hash        : 86fc14086c953edac86b8d8f9022c8baae2ad6f6
    Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
    Certificate Store Name  : MY
    Verify Client Certificate Revocation    : Disabled
    Verify Revocation Using Cached Client Certificate Only    : Disabled
    Usage Check    : Enabled
    Revocation Freshness Time : 0
    URL Retrieval Timeout   : 0
    Ctl Identifier          : (null)
    Ctl Store Name          : (null)
    DS Mapper Usage    : Disabled
    Negotiate Client Certificate    : Disabled
This is useful for situations where firewalls prevent checking of CRLs, it's no use if all servers have an up to date CRL (at least IE will not not let you use a revoked client certificate to authenticate with)

Wednesday 14 November 2012

WinRM QuickConfig fails. Error Number: -2144108387 0x8033809D

In a previous post I talked about running a PowerShell script with different credentials on a remote server. This seemed to be working fine, except for one server, so just to be on the save side I tried reconfiguring winrm.

Ordinarily this is accomplished with a simple command:
winrm qc
 However, I was getting an error when trying to do this:
WinRM already is set up to receive requests on this machine.
WSManFault
    Message = WinRM cannot process the request. The following error occured while using Negotiate authentication: An unknown security error occurred.
 Possible causes are:
  -The user name or password specified are invalid.
  -Kerberos is used when no authentication method and no user name are specified.
  -Kerberos accepts domain user names, but not local user names.
  -The Service Principal Name (SPN) for the remote computer name and port does not exist.
  -The client and remote computers are in different domains and there is no trust between the two domains.
 After checking for the above issues, try the following:
  -Check the Event Viewer for events related to authentication.
  -Change the authentication method; add the destination computer to the WinRM TrustedHosts configuration setting or use HTTPS transport.
 Note that computers in the TrustedHosts list might not be authenticated.
   -For more information about WinRM configuration, run the following command: winrm help config.

Error number:  -2144108387 0x8033809D
An unknown security error occurred.
The solution suggested on various places was to set up SPNs for the server, which I did:
setspn -s HTTP/myserver
setspn -s HTTP/myserver.dev.com
setspn -s HTTPS/myserver
setspn -s HTTPS/myserver.dev.com
Alas, this made no difference, after a little bit more digging I realized what the problem was, I was trying to remote to the current server, i.e. from myserver I was trying to run a remote script on myserver [sic], so I added myserver to the TrustedHosts like this:

winrm set winrm/config/client '@{TrustedHosts="myserver"}'

Tuesday 13 November 2012

Check-out/Check-in code programmatically in TFS 2010

We are using TFS 2010 to store MS Dynamics CRM 2011 solutions. I created a tool to export the solutions from the server to a file, but in an effort to automate and possibly speed up build deployments to test from our development environment I created a class with a few methods to check in and out files and directories.

The CommonHelper.WriteMessage and LogException methods, use a trace listener from System.Diagnostics to the write to a log file. The LogException method is in essence described in this post. It contains the code inside the catch statement.

Feel free to suggests improvements, as I'm not too sure this is the best way of doing it, all I can say is that it works [the excuse of the bad coder :-)].

 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Text;
 using Microsoft.TeamFoundation.Client;
 using Microsoft.TeamFoundation.VersionControl.Client;
 using System.Configuration;
 using System.Net;
 using DeploymentTool;
 
 namespace Deployment.ExportSolutions
 {
     public class TFSOperations
     {
         string collectionUrl, projectPath;
         NetworkCredential credential;
 
         public TFSOperations(string url, string path)
         {
             collectionUrl = url;
             projectPath = path;
 
             //I know, I know but nobody cares.
             string user = ConfigurationManager.AppSettings["user"];
             string password = ConfigurationManager.AppSettings["password"];
             string domain = ConfigurationManager.AppSettings["domain"];
 
             credential = new NetworkCredential(user, password, domain);
         }
 
         public string CollectionUrl
         {
             get { return collectionUrl; }
             set { collectionUrl = value; }
         }
 
         public string ProjectPath
         {
             get { return projectPath; }
             set { projectPath = value; }
         }
 
         public enum PathType
         {
             File,
             Directory
         };
 
         public bool CheckOut(string Path, PathType PathType, int RecursionLevel = 2)
         {
             bool result = false;
 
             try
             {
                 CommonHelper.WriteMessage(string.Format("Check that {0} {1} exists.", (PathType)PathType, Path));
 
                 if (PathType.Equals(PathType.File) && File.Exists(Path))
                 {
                     result = CheckOut(Path, RecursionType.None);
                 }
                 else if (PathType.Equals(PathType.Directory) && Directory.Exists(Path))
                 {
                     result = CheckOut(Path, (RecursionType)RecursionLevel);
                 }
                 else
                 {
                     CommonHelper.WriteMessage(string.Format("{0} {1} does not exist.", (PathType)PathType, Path));
                 }
             }
             catch (Exception ex)
             {
                 CommonHelper.LogException(ex);
             }
 
             return result;
 
         }
 
         public bool CheckIn(string Path, PathType PathType, int RecursionLevel = 2, string Comment = "I'm too lazy to write my own comment")
         {
             bool result = false;
 
             try
             {
                 CommonHelper.WriteMessage(string.Format("Check that {0} {1} exists.", (PathType)PathType, Path));
 
                 if (PathType.Equals(PathType.File) && File.Exists(Path))
                 {
                     result = CheckIn(Path, RecursionType.None, Comment);
                 }
                 else if (PathType.Equals(PathType.Directory) && Directory.Exists(Path))
                 {
                     result = CheckIn(Path, (RecursionType)RecursionLevel, Comment);
                 }
                 else
                 {
                     CommonHelper.WriteMessage(string.Format("{0} {1} does not exist.", (PathType)PathType, Path));
                 }
             }
             catch (Exception ex)
             {
                 CommonHelper.LogException(ex);
             }
 
             return result;
         }
 
         private bool CheckOut(string Path, RecursionType RecursionLevel)
         {
             bool result = false;
 
             CommonHelper.WriteMessage(string.Format("Get All LocalWorkspaceInfos."));
 
             WorkspaceInfo[] wsis = Workstation.Current.GetAllLocalWorkspaceInfo();
 
             foreach (WorkspaceInfo wsi in wsis)
             {
                 //Ensure that all this processing is for the current server.
                 if (!wsi.ServerUri.DnsSafeHost.ToLower().Equals(collectionUrl.ToLower().Replace("http://", "").Split('/')[0]))
                 {
                     continue;
                 }
 
                 Workspace ws = GetWorkspace(wsi);
 
                 CommonHelper.WriteMessage(string.Format("Check-Out {0}.", Path));
 
                 ws.PendEdit(Path, (RecursionType)RecursionLevel);
 
                 CommonHelper.WriteMessage(string.Format("Checked-Out {0}.", Path));
 
                 result = true;
             }
 
             return result;
         }
        
         private bool CheckIn(string Path, RecursionType RecursionLevel, string Comment)
         {
             bool result = false;
 
             try
             {
 
                 CommonHelper.WriteMessage(string.Format("Get All LocalWorkspaceInfos."));
 
                 WorkspaceInfo[] wsis = Workstation.Current.GetAllLocalWorkspaceInfo();
 
                 foreach (WorkspaceInfo wsi in wsis)
                 {
                     //Ensure that all this processing is for the current server.
                     if (!wsi.ServerUri.DnsSafeHost.ToLower().Equals(collectionUrl.ToLower().Replace("http://", "").Split('/')[0]))
                     {
                         continue;
                     }
 
                     Workspace ws = GetWorkspace(wsi);
 
                     var pendingChanges = ws.GetPendingChangesEnumerable(Path, (RecursionType)RecursionLevel);
 
                     WorkspaceCheckInParameters checkinParamenters = new WorkspaceCheckInParameters(pendingChanges, Comment);
 
                     if (RecursionLevel == 0)
                     {
                         CommonHelper.WriteMessage(string.Format("Check-in {0}.", Path));
                     }
                     else
                     {
                         CommonHelper.WriteMessage(string.Format("Check-in {0} with recursion level {1}.", Path, (RecursionType)RecursionLevel));
                     }
 
                     ws.CheckIn(checkinParamenters);
 
                     if (RecursionLevel == 0)
                     {
                         CommonHelper.WriteMessage(string.Format("Checked-in {0}.", Path));
                     }
                     else
                     {
                         CommonHelper.WriteMessage(string.Format("Checked-in {0}  with recursion level {1}.", Path, (RecursionType)RecursionLevel));
                     }
 
                     result = true;
                 }
 
             }
             catch (Exception ex)
             {
                 CommonHelper.LogException(ex);
             }
 
             return result;
         }
 
         private Workspace GetWorkspace(WorkspaceInfo wsi)
         {
 
             TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(wsi.ServerUri, credential);
 
             CommonHelper.WriteMessage(string.Format("Get Workspace."));
 
             Workspace ws = wsi.GetWorkspace(tpc);
 
             return ws;
         }
 
     }
 }