Tuesday, March 18, 2008

Securing ClickOnce Deployments

 

I have written in the past (http://www.damonpayne.com/PermaLink,guid,da078e56-eb5b-4616-8605-13c38d54efc3.aspx) about some of the plumbing involved in ClickOnce and Client Application Services geared towards preventing unauthorized operation of a client application.  At the time I noted that there is at least one issue with this level of “security”.  While it does keep unauthorized users from running your application, it doesn’t protect your code.  As has been lamented elsewhere, a ClickOnce application deployed on the Internet does not have any supported means of keeping people from getting your code.  Even with obfuscation this may be undesirable: we don’t want people getting a hold of the application manifest file and subsequently the assemblies.

A while back I started down a few paths to secure the code and the Application Deployment Manifest.  I have a pretty decent partial solution for protecting your code.  Read on.

Step 1: Security by Obscurity?

Your ClickOnce deployment consists of a deployment manifest, the executable, and all of the referenced assemblies and content files required to run your Windows Forms or WPF application.  By turning off directory browsing and not having any publicly available links to your .application file you can discourage casual spelunking.   I’m not very satisfied with this as the only aspect of a code protection solution. 

It’s fairly unlikely than an uneducated attacker (outside job) could guess the names of the assemblies that make up your application.  If only you could lock down the .application file…

Eliminating the Obvious Suspects

To get the obvious thoughts out of the way:

1)      You cannot protect a ClickOnce application deployment manifest using Forms authentication.  The client side aspects of ClickOnce that get installed with .NET 2.0 can’t handle Forms authentication.

2)      Windows Authentication is at best a poor option here, and not a real option for most people.  The performance of “Remember me” in an NTLM challenge form is flaky and is not supported in the Internet zone.

3)      ClickOnce does not support HTTP Basic authentication schemes.

An Idea

One nice thing about ClickOnce is that, like Silverlight, it’s really a Client Side technology.  You don’t NEED to be running Windows Server and IIS in order to benefit from these technologies.  However since I do know some Microsoft server side technologies I had an idea.

I wanted to associate a Product Key of some kind with a ClickOnce deployment.  Product Keys are not the final word on application licensing but it is a means of accountability.  Look at Windows Genuine Advantage: you can’t keep people from sharing CD keys but if you see the same CD key being used 1,000 times from different hosts all over the nation, you can shut it down.  Given a test deploy URL:

http://localhost:5785/CarSpot/MyApp.application

It would be nice to be able to do something like this:

http://localhost:5785/CarSpot/MyApp.application?ProductKey=ZZZZ-ZZZZ-ZZZZ-ZZZZ

… and have it be meaningful.

HttpHandler Attempt 1

To keep people from getting at my .application file, I thought a simple IHttpHandler implementation configured to intercept requests for the .application extension could be the answer. 

Web.config and sample code:

<add verb="GET,HEAD" path="*.application" type="ClickOnceHandler"/>

    public void ProcessRequest(HttpContext context)

    {

        string key = context.Request.QueryString["key"];

        if (ValidateKey(key))

        {

            //Go ahead and give up the .application file

        }

        else

        {

            throw new ApplicationException("Invalid or missing product Key");

        }

    }

I had a notion of allowing users to sign up for the application online by entering a privately exchanged product key in a web page:

The code behind for the install button is simple:

Response.Redirect("Publish/MyApp.application?key=" + _prodKeyTxt.Text);

Sadly, the solution is not this simple.  The Client side ClickOnce libraries exhibit some bizarre behavior.  Using Fiddler and the debugger, this is what I see:

1)      The .application file is requested and the HttpHandler validates the product key.

2)      Mysteriously, ClickOnce requests the entire URL of the .application file again: http://localhost:5785/CarSpot/Publish/MyApp.application?key=123; The HttpHandler validates the key and writes the contents of the .application file to the Response.

3)      Mysteriously (and infuriatingly) ClickOnce requests the .application file again: http://localhost:5785/CarSpot/Publish/MyApp.application ; note the missing piece of the query string.

I was unable to dig up any reason for what ClickOnce is doing here, but I do know that without getting that product key my scheme is ruined.  Looking at the .application file there’s a likely explanation:

<deploymentProvider codebase="http://127.0.0.1:5785/CarSpot /Publish/MyApp.application" />

This is where I left it for a long time, while dealing with some other things at work.

HttpHandler Attempt 2

The next, step, I felt, was to dynamically alter the application deployment file so that the provider codebase as recorded by the client included the product key on the URL.  I added some code to do this:

                XmlDocument doc = new XmlDocument();

                doc.Load(path);

                //Get around the issue of the query strong not being

                doc.DocumentElement["deployment"]["deploymentProvider"].Attributes["codebase"].Value += "?key=123";

However, this introduces another issue.  When you choose to give your ClickOnce application Full Permissions (Otherwise why not go the XBAP route?) you will find that you are signing it.   Look what Visual Studio did for me:

<publisherIdentity name="CN=CORNZOR\Damon" issuerKeyHash="" /><Signature Id="StrongNameSignature" {nothing to see here}</Signature></asmv1:assembly>

Yes, my laptop is named cornzor.  At any rate, this signature is there for your clients’ protection: if a man in the middle alters the XML payload of the .application file it will not verify and ClickOnce will refuse to do anything further.  Since I’m not a man in the middle I assumed there must be a way to dynamically do what I want.

http://msdn2.microsoft.com/en-us/library/acz3y3te.aspx

http://msdn2.microsoft.com/en-us/library/bb384246.aspx

I started out with the two links above, researching how the manifests are generated, and initially experimenting with manually generating un-signed deployment manifests from the command line.  After an infuriating XML character encoding setback, these articles did lead me to the answer.  Now I can protect my .application file in an appropriate fashion.

HttpHandler Complete

The articles above give us the tools we need to put the product key in the application manifest for each client and re-sign on the fly.

    /// <summary>

    /// Execute whatever business logic will make this a valid program key

    /// </summary>

    /// <param name="key"></param>

    /// <returns></returns>

    protected bool ValidateKey(string key)

    {

        return key.Equals("123");

    }

    /// <summary>

    /// Create a new signature in the deployment manifest

    /// </summary>

    /// <param name="path"></param>

    /// <param name="pfxPath"></param>

    protected void ReSign(string path, string pfxPath)

    {

        string mageArgFormat = "-Sign {0} -CertFile {1} -ToFile {2}";

        string mageExe = @"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\mage.exe";

        string mageArgs = string.Format(mageArgFormat, path, pfxPath, path);

        ProcessStartInfo info = new ProcessStartInfo();

        info.Arguments = mageArgs;

        info.FileName = mageExe;

        info.UseShellExecute = false;

        info.RedirectStandardError = true;

        info.RedirectStandardOutput = true;

        Process p = Process.Start(info);

        p.WaitForExit();

        using (StreamReader stdOutReader = p.StandardOutput)

        {

            string stdOut = stdOutReader.ReadToEnd();

            //Error handling here

        }

 

        using (StreamReader stdErrReader = p.StandardError)

        {

            string stdErr = stdErrReader.ReadToEnd();

            //Error handling here

        }

    }

This method uses mage.exe to re-sign the application manifest after we’ve modified it using the BuildClientApplicationFile method.  There are two things to note about this method.  You will see that I’m referring to mage.exe from the Visual Studio directory here.  Obviously, installing Visual Studio on servers is not ideal.  Mage.exe also ships separately with the SDKs as indicated in the two articles I’ve linked, which is a more palatable install than Visual Studio.  I have not yet tested to see if mage.exe will properly operate on Windows Server 2003 by itself.

The second note on this method is that we are passing in a fully qualified path to a .pfx key file.  I’ve only tested this using the same .PFX key I used when I initially published the application.

    private static void BuildClientApplicationFile(string path, string signedPath, string key)

    {

        XmlDocument doc = new XmlDocument();

        doc.Load(path);

        //Get around the issue of the query string not being persisted by the client

        doc.DocumentElement["deployment"]["deploymentProvider"].Attributes["codebase"].Value += "?key=" +key;

        doc.Save(signedPath);

    }

This method stores a special version of the application deployment manifest for this particular client.  I didn’t keep this in memory due to ClickOnce’s odd habit of re-requesting the manifest many times.  This means I’ve got one copy of the manifest per client sitting on my server.  In my testing I’ve found two side-effects for publishing updates using this method:

1)      If I re-activate from the URL and updates have been published, the client does not get prompted

2)      If I run the program from the start menu, the client does get prompted for updates.

Here is the main HttpHandler method:

    public void ProcessRequest(HttpContext context)

    {

        string key = context.Request.QueryString["key"];

        if (ValidateKey(key))

        {

            //Just in case the client doesn't know what to do with .application

            context.Response.ContentType = "application/x-ms-application";

 

            //The current version deploy URL

            string path = context.Server.MapPath("~/Publish/CarSpot.MyApp.application");

 

            //Check for existing signed manifest for this user:

            //keeping a specially signed copy of the manifest per client

            string signedFileName = key + ".CarSpot.MyApp.application";

            string signedFileServerPath = "~/Publish/" + signedFileName;

            string signedPath = context.Server.MapPath(signedFileServerPath);

 

            if (!File.Exists(signedPath))

            {

                //Load the generated deployment manifest as an XML document to make it easier

                //to edit.

                BuildClientApplicationFile(path, signedPath,key);

 

                string pfx = context.Server.MapPath("~/Publish/MyGeneratedKey.pfx");

                ReSign(signedPath, pfx);

                //Redirect to original path now that we've created a signed one...

                string redirUrl = context.Request.Url.ToString();

                context.Response.Redirect(redirUrl);

            }

            else

            {

                context.Response.WriteFile(signedPath);

                context.Response.Flush();

            }

        }

        else

        {

            context.Server.Transfer("~/Error.aspx?errormessage=Missing or invalid Application key");

        }

    }

I feel confident this is safe, look at the appref-ms file generated from this install and put in the start menu on the client:

http://127.0.0.1:5785/CarSpot /Publish/MyApp.application?key=123#CarSpot.MyApp.application, Culture=neutral, PublicKeyToken=blahblah, processorArchitecture=msil

The key is part of the URL, so it should be passed to the server every time.

Further reading

Now I’ve locked down my deployment manifest, at least you have to have a valid product key to get access to it.  This is a large step towards completely locking down my application.  Since this is ASP.Net code that I control, I can log the remote hosts that use each product key and disable one if it appears to be too promiscuous. 

What about your other files?  If someone somehow figures out that there’s a file called CarSpot.Snark.Zing.dll in the ~/ClickOnce directory?  I have some ideas for this too, along the same lines as what we’ve done here.  You will note that one of the Publish options for ClickOnce is to “Use .deploy file extension”.  Sounds like an HttpModule could be created to handle the .deploy extension.

Microsoft’s behavior regarding ClickOnce baffles me, but here we have a means of securing your deployments.

Tuesday, March 18, 2008 10:05:56 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [3]  |  Trackback
 Thursday, September 27, 2007

Regarding my last ClickOnce article, I have solved the initial code-theft issue I believe.  There is one more ClickOnce weakness I need to work around, and that is SSL.  If there is a problem with an SSL cert, ClickOnce is not smart enough to handle it.  System.Deployment.Application, I'm not done with you yet...

Thursday, September 27, 2007 12:06:34 PM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Tuesday, September 25, 2007

ClickOnce is so close to greatness, as I’ve said before.  Yes, I know the “official word” is that this is not really meant for software ISVs deploying software to end users but it seems with a few extra things it could indeed be very good for this.  With some features in the Orcas Beta 2, I think ClickOnce is getting much closer to providing the plumbing I’ve been wanting in some cases and home rolling in others.  A new product I’m working on has some Architectural Desires.  I call them “desires” because we’ve been selling desktop applications without these architectural features for a while but I really want to meet some Service Level Requirements that will make my life easier, the customer’s life easier, and our support staff’s life easier.

Environment

·         I have an existing web system that users sign into.  Credentials are stored in an app-specific schema in a MySQL database.  Users of the current desktop application must be in this MySQL database and authenticate over web services.  Users of the new system increasingly do not need access to the web based system.  Putting their credentials in MySQL is polluting this system.

·         We have an OpenLDAP server we are slowing moving credential stores to.

·         We have a Windows2003 server that’s not doing much right now

·         We have a client application that ships with specialized hardware and an install disk.

·         Upgrades go out to customers via installing a new MSI

Requirements/Desires

1.       My software requires some specialized hardware.  I’d like to ship this without install disks.

2.       I’d like the desktop application to be able to have the Role information and authenticate against our existing credential stores.  I don’t want users to be able to run this within a very short time period if we turn them off.

3.       I’d like to stop polluting the existing web system credential store with users that should not ever be able to log into this system, however SOME users of the web based system will also need to be able to authenticate using the client application.

4.       I’d like to be able to keep random people from stealing software.

5.       I’d like to be able to keep random people from installing my software.

 

Solution in Code and UML

Let’s take the big red pill and see where it leads us.

ClickOnce - Part 1

I start with .NET 3.5 Beta 2, VS 2008 Beta 2, and my current environment.  Setting up ClickOnce deployment is easy enough, I set the application to check for updates before it runs, and verify that this works.  Oddly enough with Beta 2, 100% of my testers who have tried to click “Install” rather than installing .NET 3.5 and choosing “launch” have had the install .exe lock up.  Hopefully this gets fixed.

Client Application Services – Server Side

There is a wonderful bit of plumbing available now Called Client Application Services.  In order to use this you need an ASP.Net web site.  You need to add a reference to System.Web.Extensions, and configure your web.config such that the Role and Membership services are enabled.  With .NET 3.5 this is done like so:

      <system.web.extensions>

            <scripting>

                  <webServices>

                        <authenticationService enabled="true" requireSSL="false"/>                   

                        <roleService enabled="true"/>

Now, the Role and Membership providers I have configured are my LDAP based home rolled code.  Discussing these is outside the scope of this article; suffice to say they are straight up Provider implementations.  I will need to set up my Forms authentication a certain way to meet my single sign on goals:

            <authentication mode="Forms">

                  <forms domain=".office.carspot"

                           enableCrossAppRedirects="true"

                           cookieless="UseCookies"

                           slidingExpiration="false"

                           defaultUrl="Cookies.aspx"

                           path="/"

                           protection="Encryption"

                           timeout="1440"

                           name="FooBar"/>

            </authentication>

For testing I am allowing the clients to sign on once for a day.  The domain attribute is important, as the cookie created by my web site will now be passed to anysite.office.carspot.  There is one more thing to configure here.  At this point I intended that I would manually provide the Symmetric Key to Trusted Applications written in PERL or Python etc. to decrypt the data in the cookie.  I therefore manually created some keys for a MachineKey element.  This will still be needed when I cluster this solution.

      <system.web>

            <machineKey validationKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            decryptionKey="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"

            validation="SHA1" decryption="AES" />

Client Application Services – Client Side

With the server side set up, I return to my ClickOnce application.  There will be some work to do in the Properties of my app and some work to do in code.  The first step is to enable Client Application Services.

Only the root directory of the ASP.Net application is required.  This also requires references to System.Web and System.Web.Extensions in my Client application.  This is the first hole I’ve seen thus far in my argument that a Client-only .NET install should be available.  The advanced tab bears some discussion now and also a little later:

In my case I want to make sure they are authenticating to the server at regular intervals.  The Use custom connection string setting is the key to a later problem.  Interested readers may want to use reflector on System.Web.ClientServices.Providers.ClientData,  System.Web.ClientServices.Providers.ClientDataManager, and System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, as well as exploring the new System.IO.IsolatedStorage namespace, unless using Reflector on system assemblies is against some rule, in which case you should not and I never have done so.

Now I need to write some code and see if I can log in.  Note that back in the Services tab I included a Class and Assembly name for a credential provider type.  This tells the plumbing what method to invoke in order to request credentials from the users.  The class must implement IClientFormsAuthenticationCredentialsProvider, in my case this is a login window written in WPF.  Insofar as my research has gone so far, authenticating members and checking roles must be done explicitly.  The code to do so is simple:

bool valid = System.Web.Security.Membership.ValidateUser("", "");

This will cause the plumbing to invoke my Login window and give me an answer as to whether authentication was successful, and since Role management is enabled I should be able to get at my roles as well.  Now I’m going to set up a proxy and try to log in.  Here’s what Fiddler shows me:

 

You can see the AJAX/JSON requests going back and forth, and I’m showing the headers of the response in order to show you the persistant cookie.  The actual body of the response is just {“d”, true}.  Now I’m going to try to get this to authenticate against a Web service written in another language on a different server.

This Cookie is not for you

My thought was to use cookies with a common Domain in order to authenticate against my ASP.NET site via Client Services but have the information available in a usable form to other non-.NET systems.  This turned out to be a pipe dream at first.

·         Authenticating via Login.aspx returns a cookie as expected.  Cookies are turned off for HttpWebRequest by default.  Nothing I could find would get Windows/.NET to nicely populate cookies on my HttpWebRequest once I gave it a CookieContainer.  This is probably desired behavior.

·         Authenticating via Client Services does not create a cookie in the same way IE does anyway (more on this in a moment).

·         Any IE specific hacks I do are likely to be broken in Firefox, and supposedly ClickOnce will work with Firefox later this year.

I started by experimenting with the Use Custom Connection String setting, but realized this may be problematic in ClickOnce since the actual Application directory is not a clean or dependable Path.  When facing a dilemma like this I pull out my handy File System Watcher program and go to work. 

While logging in, this “User_damonpayne.clientdata” file created and changed looks promising, and it turns out to be.  I can already tell by calling Thread.CurrentPrincipal.Identity.Name that my server-side user name “damonpayne”, is indeed available on the Client.  Opening up this .clientdata file reveals the following:

<?xml version="1.0" encoding="utf-8"?>

<ClientData>

      <LastLoggedInUserName></LastLoggedInUserName>

      <LastLoggedInDateUtc>1C6E0B92F1DBE7E</LastLoggedInDateUtc>

      <PasswordHash></PasswordHash>

      <PasswordSalt></PasswordSalt>

      <Roles>

            <item>Default</item>

            <item>Technology</item>

            <item>ScrumMaster</item>