Monday, 31 December 2012

Programmatically change the default sound device in Windows 7

I got a new monitor last week and it came with integrated speakers, which I didn't think I would use, but it occurred to me that it would be handy to use these speakers rather than my Logitech 5.1 system when browsing the internet. I normally have the speakers off at the sub woofer for one main reason, the system uses 30 Watts in stand by, which roughly equates to £30 a year in wasted electricity and since I normally use my desktop only to watch movies or TV series this is not a major inconvenience, but sometimes it's annoying. At any rate, I was looking for a way of changing the default  sound device using PowerShell, but it looks like it's not possible, however, it is possible to do it programmatically.

I'm reproducing the code here because I thought it was interesting. This is not my code, the original article can be found here.

PolicyConfig.h:
// ----------------------------------------------------------------------------
// PolicyConfig.h
// Undocumented COM-interface IPolicyConfig.
// Use for set default audio render endpoint
// @author EreTIk
// ----------------------------------------------------------------------------
 
#pragma once
 
interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8")
IPolicyConfig;
class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9")
CPolicyConfigClient;
// ----------------------------------------------------------------------------
// class CPolicyConfigClient
// {870af99c-171d-4f9e-af0d-e63df40c2bc9}
//
// interface IPolicyConfig
// {f8679f50-850a-41cf-9c72-430f290290c8}
//
// Query interface:
// CComPtr[IPolicyConfig] PolicyConfig;
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient));
//
// @compatible: Windows 7 and Later
// ----------------------------------------------------------------------------
interface IPolicyConfig : public IUnknown
{
public:
 
    virtual HRESULT GetMixFormat(
        PCWSTR,
        WAVEFORMATEX **
    );
 
    virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
        PCWSTR,
        INT,
        WAVEFORMATEX **
    );
 
    virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(
        PCWSTR
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
        PCWSTR,
        WAVEFORMATEX *,
        WAVEFORMATEX *
    );
 
    virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
        PCWSTR,
        INT,
        PINT64,
        PINT64
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
        PCWSTR,
        PINT64
    );
 
    virtual HRESULT STDMETHODCALLTYPE GetShareMode(
        PCWSTR,
        struct DeviceShareMode *
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetShareMode(
        PCWSTR,
        struct DeviceShareMode *
    );
 
    virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
        PCWSTR,
        const PROPERTYKEY &,
        PROPVARIANT *
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
        PCWSTR,
        const PROPERTYKEY &,
        PROPVARIANT *
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
        __in PCWSTR wszDeviceId,
        __in ERole eRole
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
        PCWSTR,
        INT
    );
};
 
interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620")
IPolicyConfigVista;
class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862")
CPolicyConfigVistaClient;
// ----------------------------------------------------------------------------
// class CPolicyConfigVistaClient
// {294935CE-F637-4E7C-A41B-AB255460B862}
//
// interface IPolicyConfigVista
// {568b9108-44bf-40b4-9006-86afe5b5a620}
//
// Query interface:
// CComPtr[IPolicyConfigVista] PolicyConfig;
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient));
//
// @compatible: Windows Vista and Later
// ----------------------------------------------------------------------------
interface IPolicyConfigVista : public IUnknown
{
public:
 
    virtual HRESULT GetMixFormat(
        PCWSTR,
        WAVEFORMATEX **
    );  // not available on Windows 7, use method from IPolicyConfig
 
    virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
        PCWSTR,
        INT,
        WAVEFORMATEX **
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
        PCWSTR,
        WAVEFORMATEX *,
        WAVEFORMATEX *
    );
 
    virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
        PCWSTR,
        INT,
        PINT64,
        PINT64
    );  // not available on Windows 7, use method from IPolicyConfig
 
    virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
        PCWSTR,
        PINT64
    );  // not available on Windows 7, use method from IPolicyConfig
 
    virtual HRESULT STDMETHODCALLTYPE GetShareMode(
        PCWSTR,
        struct DeviceShareMode *
    );  // not available on Windows 7, use method from IPolicyConfig
 
    virtual HRESULT STDMETHODCALLTYPE SetShareMode(
        PCWSTR,
        struct DeviceShareMode *
    );  // not available on Windows 7, use method from IPolicyConfig
 
    virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
        PCWSTR,
        const PROPERTYKEY &,
        PROPVARIANT *
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
        PCWSTR,
        const PROPERTYKEY &,
        PROPVARIANT *
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
        __in PCWSTR wszDeviceId,
        __in ERole eRole
    );
 
    virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
        PCWSTR,
        INT
    );  // not available on Windows 7, use method from IPolicyConfig
};
Console App Code:

#include "stdio.h"
#include "wchar.h"
#include "tchar.h"
#include "windows.h"
#include "Mmdeviceapi.h"
#include "PolicyConfig.h"
#include "Propidl.h"
#include "Functiondiscoverykeys_devpkey.h"
 
HRESULT SetDefaultAudioPlaybackDevice(LPCWSTR devID)
{
 IPolicyConfigVista *pPolicyConfig;
 ERole reserved = eConsole;
 
    HRESULT hr = CoCreateInstance(__uuidof(CPolicyConfigVistaClient),
  NULL, CLSCTX_ALL, __uuidof(IPolicyConfigVista), (LPVOID *)&pPolicyConfig);
 if (SUCCEEDED(hr))
 {
  hr = pPolicyConfig->SetDefaultEndpoint(devID, reserved);
  pPolicyConfig->Release();
 }
 return hr;
}
 
// EndPointController.exe [NewDefaultDeviceID]
int _tmain(int argc, _TCHAR* argv[])
{
 // read the command line option, -1 indicates list devices.
 int option = -1;
 if (argc == 2) option = atoi((char*)argv[1]);
 
 HRESULT hr = CoInitialize(NULL);
 if (SUCCEEDED(hr))
 {
  IMMDeviceEnumerator *pEnum = NULL;
  // Create a multimedia device enumerator.
  hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
   CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnum);
  if (SUCCEEDED(hr))
  {
   IMMDeviceCollection *pDevices;
   // Enumerate the output devices.
   hr = pEnum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &pDevices);
   if (SUCCEEDED(hr))
   {
    UINT count;
    pDevices->GetCount(&count);
    if (SUCCEEDED(hr))
    {
     for (int i = 0; i < count; i++)
     {
      IMMDevice *pDevice;
      hr = pDevices->Item(i, &pDevice);
      if (SUCCEEDED(hr))
      {
       LPWSTR wstrID = NULL;
       hr = pDevice->GetId(&wstrID);
       if (SUCCEEDED(hr))
       {
        IPropertyStore *pStore;
        hr = pDevice->OpenPropertyStore(STGM_READ, &pStore);
        if (SUCCEEDED(hr))
        {
         PROPVARIANT friendlyName;
         PropVariantInit(&friendlyName);
         hr = pStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
         if (SUCCEEDED(hr))
         {
          // if no options, print the device
          // otherwise, find the selected device and set it to be default
          if (option == -1) printf("Audio Device %d: %ws\n",i, friendlyName.pwszVal);
          if (i == option) SetDefaultAudioPlaybackDevice(wstrID);
          PropVariantClear(&friendlyName);
         }
         pStore->Release();
        }
       }
       pDevice->Release();
      }
     }
    }
    pDevices->Release();
   }
   pEnum->Release();
  }
 }
 return hr;
}
As mentioned above this is not my code, copied from here.

Wednesday, 26 December 2012

Unable to activate Windows 8 - 0x8007232B - DNS name does not exist.

So I finally decided to install Windows 8 on one of the work desktops and I could not activate it, I would get this error.
Error code: 0x8007232B
Error description: DNS name does not exist.
It turns out that because I had used the MSDN iso to install this machine I had to change the license as it seems that Windows 8 gets installed with a temporary key when using the MSDN isos.

In order to change the license, run the following command (from a command prompt or PowerShell) with elevated permissions (Run as Administrator):
slmgr.vbs /ipk mypro-ductk-key00-00000
Now Windows 8 can be activated.

Friday, 21 December 2012

Slow vs Fast Hashing Algorithms in C#

In a project I'm working on at the moment the passwords were hashed using the MD5 hash, in fact the code is pretty much line for line the code found here, one problem with this code. 

There is no salting, which means that users using the same passwords would have the same password, e.g. QWERT12345 will always hash to bb760a3ee6d5abe0b0e58d7977d25527.

A bigger problem, this hashing algorithm is incredibly fast. My ancient Intel p8600 @ 2.4 GHz takes about 18 seconds to calculate 1 million of these hashes, hashing QWERT12345, and this is running on a single core and this is absolutely nothing to write home about, compared to what a GPU can do, see this article.

There are quite a few options to solve this problem, namely using a more secure way of hashing passwords, but I chose using PBKDF2, mainly because it's part of the framework.

This method allows changes to the number of iterations and/or the salt length. Ensure that the separator is not part of the base64 character set, namely don't use +,/ or =.

Here is the code I used:

public const int keyLength = 32;
public const int iterations = 10000;
public string PBKDF2Password(string password)
{
    StringBuilder sb = new StringBuilder();

    using (var bytes = new Rfc2898DeriveBytes(password, keyLength, iterations))
    {
        byte[] salt = bytes.Salt;
        byte[] key = bytes.GetBytes(keyLength);

        sb.Append(string.Format("{0}:", iterations));
        sb.Append(string.Format("{0}:", Convert.ToBase64String(salt)));
        sb.Append(string.Format("{0}", Convert.ToBase64String(key)));

    }

    return sb.ToString();
}

Using this method, with the above values, it takes my laptop 0.3 seconds to calculate a single salted hash, which roughly means that we've gone from 50k hashes per second to 3 per second. The beauty of this hashing algorithm is that you can increase the number of iterations as hardware gets more and more powerful, so now you just need to decide how long you want your users to wait before they can log in to your system.

A method like this can be used for checking the password for validity.

public bool CheckPassword(string password, string hash)
{
    bool result = false;
    byte[] saltArray, key;
    int iterations;

    string[] hashComponents = hash.Split(':');

    iterations = Convert.ToInt32(hashComponents[0]);
    saltArray = Convert.FromBase64String(hashComponents[1]);
    key = Convert.FromBase64String(hashComponents[2]);

    using (var bytes = new Rfc2898DeriveBytes(password, saltArray, iterations))
    {
        byte[] newKey = bytes.GetBytes(keyLength);

        if (newKey.SequenceEqual(key))
        {
            result = true;
        }
    }
    return result;
}

Monday, 17 December 2012

Start (and Stop) Microsoft Dynamics CRM 2011 Services using PowerShell

Today I spent a good deal of time trying to sort out some MS Dynamics CRM 2011 issues and  I had to start some services time and time again, so I wondered whether I could just script the whole thing and it turns out that you can, but a one liner will do to restart all MS Dynamics CRM services.

get-service | where {$_.Name -like "*CRM*" -or $_.Name -like "W3SVC"}| ForEach-Object {Restart-Service –DisplayName $_.DisplayName}

As you can probably imagine there is a Start-Service and a Stop-Service cmdlet in PowerShell.

Friday, 14 December 2012

Set User Regional Settings in Microsoft Dynamics CRM 2011

A few months ago we had an issue where all test users had US regional settings, which was a bit of a problem since the users could not change their own settings, customer requirement, so I used the method below to set it right. 

Two things to bear in mind:
  1. If you are looping through all users, make sure that you filter out SYSTEM and INTEGRATION users, better safe than sorry.
  2. You can avoid this (wrong regional settings) by setting the system regional settings to your desired setting before adding the users (not an option if an upgrade, but it's worth bearing in mind).
It might be a good idea just to pass the user id rather than the whole entity, but there you go.

private void SetUserSettingsToUK(IOrganizationService service, Entity user)
{
      Guid userId = new Guid();

      RetrieveUserSettingsSystemUserRequest settingsReq = new RetrieveUserSettingsSystemUserRequest();
      RetrieveUserSettingsSystemUserResponse settingsRsp = new RetrieveUserSettingsSystemUserResponse();

      settingsReq.ColumnSet = new ColumnSet(true);

      UpdateUserSettingsSystemUserRequest settingsUpdReq = new UpdateUserSettingsSystemUserRequest();
      UpdateUserSettingsSystemUserResponse settingsUpdRsp = new UpdateUserSettingsSystemUserResponse();

      Entity userSettings = new Entity("usersettings");
      userId = user.Id;

      settingsReq.EntityId = userId;
      settingsReq.ColumnSet = new ColumnSet(true);

      settingsRsp = (RetrieveUserSettingsSystemUserResponse)service.Execute(settingsReq);

      userSettings = (Entity)settingsRsp.Entity;

      //helplanguageid appears not to be set so we don't check for it.

      if (userSettings.Attributes["currencysymbol"].ToString() != "£"

   || Convert.ToInt32(userSettings.Attributes["localeid"]) != 2057
   || userSettings.Attributes["timeformatstring"].ToString().ToLower() != "hh:mm"
   || userSettings.Attributes["dateformatstring"].ToString().ToLower() != "dd/mm/yyyy"
   || Convert.ToInt32(userSettings.Attributes["uilanguageid"]) != 1033
   || Convert.ToInt32(userSettings.Attributes["longdateformatcode"]) != 1
   || Convert.ToInt32(userSettings.Attributes["negativecurrencyformatcode"]) != 1)
      {
        userSettings.Attributes["dateformatstring"] = "dd/MM/yyyy";
        userSettings.Attributes["timeformatstring"] = "HH:mm";
        userSettings.Attributes["currencysymbol"] = "£";
        userSettings.Attributes["negativecurrencyformatcode"] = 1;
        userSettings.Attributes["longdateformatcode"] = 1;
        userSettings.Attributes["localeid"] = 2057;
        userSettings.Attributes["helplanguageid"] = 1033;
        userSettings.Attributes["uilanguageid"] = 1033;

        settingsUpdReq.Settings = userSettings;
        settingsUpdReq.UserId = userId;
        try
        {

          settingsUpdRsp = (UpdateUserSettingsSystemUserResponse)service.Execute(settingsUpdReq);

        }
        catch (Exception ex)
        {
          StringBuilder sb = new StringBuilder();
          sb.AppendLine(string.Format("Unable to set user settings for user {0}.", user["fullname"]));
          sb.AppendLine(string.Format("Exception:{0}.", ex.Message));
          if (ex.InnerException != null)
          {
            sb.AppendLine(string.Format("InnerException: {0}", ex.InnerException.Message));
          }
          log.Debug(sb.ToString());
          log.Debug(sb.ToString());

        }

      }

    }

Sunday, 9 December 2012

Set Regional Settings for Sharepoint 2010 Sites

Somebody at work forgot to change the regional settings to UK and since we have quite a few sites on our Sharepoint installation, I thought it would be a good idea to script the procedure rather than do it manually, so here it is:

The script takes a single parameter, which is the SharePoint base Url and sets all sites within to UK regional settings, change the GetCultureInfo line to your Locale.

 param ($url) 

 $snapin ="Microsoft.sharepoint.powershell" 

 if (-not $url) 
 { 
 write-host "Please Invoke script like this:  SetRegionalSettings.ps1 -url `"http://sp.dev.com/`"" 
 exit 
 } 

 if ( (Get-PSSnapin -Name $snapin -ErrorAction SilentlyContinue) -eq $null ) 
 {     
  Add-PSSnapin $snapin -ErrorAction SilentlyContinue 
 } 
 
 try 
 { 
  $sites = Get-SPweb -site $url -limit all 
 } 
 catch 
 { 
  Write-Error "An error occurred while retrieving the sharepoint sites for $url" 
 } 
 if ($sites -ne $null) 
 { 
  foreach ($site in $sites) 
  { 
   try 
    { 
     $site.Locale = [System.Globalization.CultureInfo]::GetCultureInfo("en-GB");    
     Write-Host "Changing Regional Settings for Site $($site.Name) to $($site.Locale.DisplayName)"   
     $site.Update() 
    } 
    catch  
    { 
     Write-Error "An error occurred while changing regional settings" 
    } 
  } 
 } 

Monday, 3 December 2012

SSL Mutual (Two-way) Authentication for WCF services in IIS 7.5 using client certificates

In a previous post, I described  how to configure SSL client Authentication in IIS 7.5 using a One-to-One Mapping.

In this post I describe how to set up a client authenticated WCF web service.

Firstly, there are three pre-requisites.
  1. Server Certificate from trusted CA.
  2. Client Certificate from trusted CA.
  3. WCF service deployed to a website that has been configured to use SSL, see this post for details on how to configure a website to use SSL.
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.

In any case, the first step is to ensure that the website is set to require SSL and client certificates:


The second step is to enable Anonymous Authentication. This might sound like a bad idea and to a certain extent it is, using a one to one mapping is a better idea, but I've not got that working yet. What is achieved with this configuration is that any user that has a client certificate from a trusted CA will be be able to use the WCF service.



Third step is to change the configuration file of the WCF service to this (Use your own name and Contract):

  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="TransportSecurity">
          <security mode="Transport">
            <transport clientCredentialType="Certificate"/>
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <services>
      <service name="API.Service" behaviorConfiguration="behaviour">
        <endpoint name="AnEndPoint" address="" binding="wsHttpBinding"
               contract="API.IService"  bindingConfiguration="TransportSecurity">
        </endpoint>
       </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="behaviour">
          <serviceMetadata   httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

And finally the client that consumes the web service needs to have this on the configuration file:

<system.serviceModel> 
 <bindings> 
  <wsHttpBinding> 
  <binding name="TransportSecurity"> 
   <security mode="Transport"> 
    <transport clientCredentialType="Certificate" /> 
   </security> 
  </binding> 
  </wsHttpBinding> 
 </bindings> 
 <client> 
  <endpoint address="https://fqdn/service.svc" behaviorConfiguration="behaviour" 
  binding="wsHttpBinding" bindingConfiguration="TransportSecurity" 
  contract="API.IService" name="TransportSecurity" /> 
 </client> 
 <behaviors> 
  <endpointBehaviors> 
   <behavior name="behaviour"> 
    <clientCredentials> 
     <clientCertificate findValue="577aeb8b603af8453b80c55e9ac4abdfd4cf9c6c" 
     storeLocation="CurrentUser" x509FindType="FindByThumbprint" />
    </clientCredentials> 
   </behavior> 
  </endpointBehaviors> 
 </behaviors> 
</system.serviceModel>

Note that you can also use a browser to test this.

Saturday, 1 December 2012

We are where we are


A few weeks back somebody responded to an email where I complained about a few things that the project should have done, with the classic, we are where we are.

This was my reply. Nothing like a bit of quantum mechanics (Copenhagen interpretation style) in the morning:

We ALWAYS are where we are, how could it possibly be otherwise?

At best we could be in all possible places (should really be states) at once, if we happen to be microscopic (probably nanoscopic or even picoscopic would be more accurate) and only as long as nobody tries to measure where we are (measure our state). But this (measuring of our state) would cause the wave function to collapse into where we actually are (the state the system is in), thus breaking the superposition of states (being in all possible places at once).

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.