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;
This entry was posted in .NET. Bookmark the permalink.

13 Responses to ClickOnce and Expiring Code Signing Certificates

  1. Robert Pace says:

    Hey I just wanted to compliment you on your blog. We have a similar situation with our click once application and while researching how to easily uninstall the thing for 50+ users I came across this and it was very helpful. Kudos to you!

  2. Johnny Funch says:

    Hi,

    I was originally going to use your suggestion above but luckily MIcrosoft came up with a ‘workaround’…

    The solution above is only great if you can get to all the computers with the clickonce application installed. Ours is installed in cars with a very unstable GPRS connection…

    Just so you know…

    http://support.microsoft.com/Default.aspx?kbid=925521

    The C++ program on the page above works well although it shouldn’t have been a problem in the first place :)

    tnx
    ~j

  3. jim says:

    Johnny,

    I did see the MS article while researching this. Unfortunately, I’m using a certificate from Thawte, so the C++ program won’t work in my case. That only works if you “self sign”. So, I opted for “method 1″ :)

    Thanks,
    Jim

  4. Pingback: Jim Harte’s Blog » Blog Archive » Another Workaround for Expired ClickOnce Code Signing Certificates

  5. Steve says:

    Great piece of work and documentation. We have 140 locations with an average of 3 workstations per location. I’ve been using VS 2005 since beta 1. The Click-Once technology is terrific but, if anything goes wrong, you’re hosed. I have had the self-signing fail (you don’t know until it is deployed and tested) because I added components to the app that it didn’t consider the “same” as before. Lately, I have had to publish twice (bumping the ver each time) to get the clients to install.
    Anyway, thanks for your info here. I will keep this link somewhere safe.

  6. Freddie says:

    Hey Jim, i just wanted to thank you a lot for your blog. it helps a lot. i have a question though, i incorporated your self-uninstall in my clickonce application. i noticed that it does not remove the application in the C:\Documents and Settings\user name\Local Settings\Apps\2.0 folder. unlike if you uninstall manually the application in the add/remove programs it clears the said folder. is it ok? thank you very much again. have a nice day! =)

  7. Michael Jones says:

    I read your comment above: “I deploy an update with the old certificate that contains the GÇ£reinstallGÇ¥ logic”. I can’t understand how you are able to publish an update if the original certificate has expired. Did I misunderstand something? I am not able to update my app after the certificate has expired.

  8. jim says:

    Michael, I haven’t looked at this in months and I don’t remember having a problem publishing with the old certificate. Sorry.

    You may want to consider using this workaround instead:

    http://blogs.msdn.com/danielma/archive/2007/03/19/clickonce-and-expired-certificates.aspx

    It’s less “kludgy”, IMHO.

  9. Eric says:

    Hi Jim, Thanks for the blog, its been very helpful. Although the new sign tool technique is probably the way to go I’m still interested in your uninstall technique. However the link to ClickOnceReinstall.zip appears to be dead. Do you plan to make it available again? If so I’d like to give it a try.

    PS Sorry for trying to register, I was confused!

  10. jim says:

    Eric – try again, it should work now. I recently changed hosting companies and lost that file in the shuffle!

  11. Paul Ballew says:

    Nice solution. One comment though: the format string for getting the PublicKeyToken has a bug if there’s a 0 in the token. It will drop the 0 if it’s the first part of the byte. This code will fix it:

    String.Format(“{0:x2}”, pk[i]);

    Thanks Jim!

    Paul

  12. Pingback: Visual Studio ClickOnce deployment – certificate expiration - Programmers Goodies

  13. Pingback: download click once application from browser - Programmers Goodies