Tuesday, December 18, 2012

ClickOnce and Expiring Code Signing Certificates


There’s a really nasty bug in .NET 2.0′s ClickOnce deployment technology. If you deploy a smart client as availaible “off line” (launchable from the start menu) and you sign your manifest with a certificate from an official certificate authority (such as Thawte or VeriSign), you cannot renew your certificate and deploy to the same location! If you do, the next time someone starts your client it will barf up an error and won’t start. Examining the log will reveal the problem in the somewhat cryptic message “The deployment identity does not match the subscription”.
The problem is that the certificate authorities issue “renewed” certificates with a different private key. This makes up part of the ClickOnce app’s “identity” and ClickOnce validates this when starting an app to prevent tampering.
The only workaround is to uninstall and reinstall the app via the add/remove programs control panel applet. This really puts a kink in the whole “click once” thing, doesn’t it? I ran into this issue recently at work, as our code signing certificate was set to expire. Since I’m experienced enough to never trust Microsoft to do the right thing, I tested the “renewed” certificate in a test environment. Several Google searches later, I see that I’m not alone in discovering this problem.
To avoid having over 200 users fart around in the “add/remove programs” control panel applet, I came up with a kludge to have the client application uninstall itself and launch the ClickOnce installer, signed with the new certificate, from a new location. So after deploying the client signed with the new certificate, I deploy an update with the old certificate that contains the “reinstall” logic. Part of this trickery involves obtaining the “Public Key Token” for your app (I believe this comes from the certificate used to sign the ClickOnce manifest). The following snippet illustrates how to determine the public key token programmatically:
/// <summary>
/// Gets the public key token for the current ClickOnce app.
/// </summary>
private static string GetPublicKeyToken()
{
    ApplicationSecurityInfo asi =
        new ApplicationSecurityInfo(
                    AppDomain.CurrentDomain.ActivationContext);
    byte[] pk = asi.ApplicationId.PublicKeyToken;
    StringBuilder pkt = new StringBuilder();
    for (int i = 0; i < pk.GetLength(0); i++)
        pkt.Append(String.Format("{0:x}", pk[i]));
    return pkt.ToString();
}
Next, we need to find the uninstall string in the registry, based on the PublicKeyToken:
/// <summary>
/// Gets the uninstall string for the current ClickOnce app from the
/// Windows Registry.
/// </summary>
/// <param name="PublicKeyToken">The public key token of the app.
/// </param>
/// <returns>The command line to execute that will uninstall the app.
/// </returns>
private static string GetUninstallString(string PublicKeyToken, 
  out string DisplayName)
{
    string uninstallString = null;
    string searchString = "PublicKeyToken=" + PublicKeyToken;
    RegistryKey uninstallKey = Registry.CurrentUser.OpenSubKey(
        "SoftwareMicrosoftWindowsCurrentVersionUninstall");
    string[] appKeyNames = uninstallKey.GetSubKeyNames();
    DisplayName = null;
    foreach(string appKeyName in appKeyNames)
    {
        RegistryKey appKey = uninstallKey.OpenSubKey(appKeyName);
        uninstallString = (string)appKey.GetValue("UninstallString");
        DisplayName = (string)appKey.GetValue("DisplayName");
        appKey.Close();
        if(uninstallString.Contains(searchString))
            break;
    }
    uninstallKey.Close();
    return uninstallString;
}
I then launch the uninstaller, using the Process class, and use some Interop calls to the Win32 API to find the uninstaller window and automatically “push” the “OK” button (would have been nice if the was a /silent switch so I wouldn’t have to do this). Finally, I launch ClickOnce for the new version of the client, signed with the new certificate, to update the user’s workstation. A zip file with this source code can be found here:ClickOnceReinstall.zip Using these utility classes, I just insert the following code into the client’s startup routine to make it reinstall itself:
// Self-uninstall
Utils.DeploymentUtils.UninstallMe();
Utils.DeploymentUtils.AutoInstall(
  "http://host-name/deployment-folder/MyApp.application");
Application.Exit();
return;
source : http://www.jamesharte.com/blog/?p=11

No comments:

Post a Comment