共用方式為


A PostBuild Step that Does Something

Generally at Microsoft, we don't like to have "feature take-back" between versions of shipped products. Take-back is removing core functionality from a product that was previously in place. I'm sure that there are probably dozens if not hundreds of examples within VS, but that doesn't necessarily mean that Microsoft is proud of that fact. And I would hope most of the take-backs were replaced with a comparable technology.

One feature setup project feature that has been removed from VS 2005 was the ability to sign the built output. In VS 2002 and VS 2003, it was possible to sign your output by specifying .spc and .pvk files, which would be used by signcode.exe (a Platform SDK tool) to sign both the built msi as well as the bootstrapper (setup.exe). In VS 2005, however, signcode was removed from the Platform SDK and replaced with signtool.exe. Unfortunately, the setup projects were not able to add the UI and functionality necessary to use signtool.exe in time. It should be noted that using ClickOnce publishing gets the application and deployment manifests signed, and the bootstrapper is signed as well. In this blog entry, we're going to back-track through Microsoft.Common.targets to see how this signing happens, and see how this can be applied to signing the build outputs of setup projects.

Why is signing important? As Robert Flaming points out on his blog, signing is a habit one should get into to create secure setups. Also, it will be expected that the user experience for installing a signed setup is different than installing a non-signed setup.

We begin by building on where we left off last time, and expand on the test.proj example in there. Lets create a new Setup project in VS, called "SetupSigning", and add a new .proj file ("PostBuild.proj") to the project like last time. This time the proj file will need a new target, which we will call SignSetup. There will be 2 files we want to sign: the built msi and also the built setup.exe. For now, we put in place-holders for the actual work to sign the file, leaving us with the following-project file:

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="SignSetup">
        <Message Text="Signing $(BuiltOutputPath)" />
        <Message Text="Signing $(ProjectDir)\$(Configuration)\setup.exe" />
    </Target>
</Project>

Note that for now I have made the assumption that the bootstrapper will be built to the default location, $(ProjectDir)\$(Configuration)\setup.exe. Last time I pointed out a problem with passing the ProjectDir as command-line property to msbuild. For now, we will be working around this by using the following command-line:

 msbuild.exe /p:Configuration="$(Configuration)" /p:BuiltOutputPath="$(BuiltOuputPath)" /p:ProjectDir="$(ProjectDir)." "$(ProjectDir)\PostBuild.proj" 

Building yields the following output:

 Target SignSetup:
    Signing E:\Documents and Settings\Mike\My Documents\Visual Studio 2005\Projects\PostBuildOperations\SetupSigning\Debug\SetupSigning.msi
    Signing E:\Documents and Settings\Mike\My Documents\Visual Studio 2005\Projects\PostBuildOperations\SetupSigning\.\Debug\setup.exe

which means we are off to a good start. Now, how to sign?

We begin by examining Microsoft.Common.targets, located at %windir%\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets. Because we know that the bootstrapper is getting signed in clickOnce projects, I suspect that we should look for "setup.exe" in this file to see if we can find any clues, and eventually we happen onto this:

 <SignFile
    CertificateThumbprint="$(_DeploymentResolvedManifestCertificateThumbprint)"
    TimestampUrl="$(ManifestTimestampUrl)"
    SigningTarget="$(PublishDir)\setup.exe"
    Condition="'$(BootstrapperEnabled)'=='true'"/>

We should be able to use something like this for signing our setup project outputs. We need some way to get the CertificateThumbprint. Scanning Microsoft.Common.targets for _DeploymentResolvedManifestCertificateThumbprint, we find it as an output of the ResolveKeySource task:

         <ResolveKeySource
            KeyFile="$(AssemblyOriginatorKeyFile)"
            CertificateThumbprint="$(ManifestCertificateThumbprint)"
            CertificateFile="$(ManifestKeyFile)"
            SuppressAutoClosePasswordPrompt="$(BuildingInsideVisualStudio)"
            ShowImportDialogDespitePreviousFailures="$(BuildingProject)"
            ContinueOnError="!$(BuildingProject)">
   
            <Output TaskParameter="ResolvedKeyFile" PropertyName="KeyOriginatorFile" Condition=" '$(SignAssembly)' == 'true' "/>
            <Output TaskParameter="ResolvedKeyContainer" PropertyName="KeyContainerName" Condition=" '$(SignAssembly)' == 'true' "/>
            <Output TaskParameter="ResolvedThumbprint" PropertyName="_DeploymentResolvedManifestCertificateThumbprint" Condition=" '$(SignManifests)' == 'true' "/>

        </ResolveKeySource>

It looks like this task is serving double-duty for resolving the key source for both manifest signing as well as assembly signing. In this case, we don't care about assembly signing. Most likely, we only need the CertificateFile input and the ResolvedThumbprint output. So, we will need a key file. I used a C# Windows Application to create a temporary signing key, which I then copied to my project directory. Combining and slightly altering the ResolveKeySource and Signfile tasks above, we come up with:

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003">

    <PropertyGroup>
        <CertificateKeyFile>$(ProjectDir)\SetupSigning_TemporaryKey.pfx</CertificateKeyFile>
    </PropertyGroup>
    
    <Target Name="SignSetup">
        <ResolveKeySource
            CertificateFile="$(CertificateKeyFile)">
            <Output TaskParameter="ResolvedThumbprint" PropertyName="_ResolvedCertificateThumbprint" />
        </ResolveKeySource>


        <SignFile
            CertificateThumbprint="$(_ResolvedCertificateThumbprint)"
            SigningTarget="$(ProjectDir)\$(Configuration)\setup.exe"/>
    </Target>
</Project>

First, we define a property: the CertificateKeyFile, with a value equal to the path to the certificate file created previously. This property is used as CertificateFile value to the ResolveKeySource. The output of this task (ResolvedThumbprint) is stored in the _ResolvedCertificateThumbprint property. This property is then in turn used as the CertificateThumbprint of the SignFile task. Building the project, we see there are no errors. And indeed, when we navigate to the setup.exe and view the properties, we see there is a Digital Signatures item, and all indications are that the setup.exe is now digitally signed.

Presumably, getting the msi signed is as simple as adding a new SignFile task:

         <SignFile
            CertificateThumbprint="$(_ResolvedCertificateThumbprint)"
            SigningTarget="$(BuiltOutputPath)" />

Unfortunately, building this yields an error:

 Target SignSetup:
    E:\Documents and Settings\Mike\My Documents\Visual Studio 2005\Projects\PostBuildOperations\SetupSigning\PostBuild.proj(17,9): error MSB3482: SignTool reported an error 'Invalid character in the given encoding. Line 1, position 1.'.
Done building target "SignSetup" in project "PostBuild.proj" -- FAILED.

Sort of a strange message, isn't it? Well, it turns out that the error ("Invalid character in the given encoding. Line 1, position 1.") is the exact same message you would get if you tried to load the msi as an XML document. It turns out that the SignFile task will first check if the file is a PE file. If it is not, it will attempt to do XML-signing on the file. Unfortunately, the msi is neither a PE file nor an XML file. Hmm, I thought signtool would work for this task. What is going on?

Playing around with the command line parameters of signtool.exe shows that the following command-line does work for this scenario:

 signtool sign /sha1 <cert thumbprint> <path to msi>

So we shouldn't be using the SignFile task, but instead should be using signtool directly. If only there was a task which would execute this command for us. Well, it turns out that there is:

        <Exec Command='signtool.exe sign /sha1 $(_ResolvedCertificateThumbprint) "$(BuiltOutputPath)"' />

Now, building signs the msi just as we expected.

This time around, we learned:

  • How to examine Microsoft.Common.targets for information on how to use some msbuild tasks
  • How to define a property in a project file
  • How to use the output of one task in another task.

[This post written while listening to Pink Floyd]

Comments

  • Anonymous
    October 04, 2006
    Mike Wade, VS2005 Bootstrapper author and all-around nice guy wrote a blog entry about authenticode signing

  • Anonymous
    October 18, 2006
    Last time , I went over how to sign the bootstrapper and MSI via an msbuild-based postbuild step. This

  • Anonymous
    December 20, 2007
    One of the things I've always wanted to do, but never got around to, is to figuring out how to sign my

  • Anonymous
    September 12, 2009
    The ResolveKeySource command given above returns a thumbprint like this: 1a 3c 37 7b c2 1b 7b e3 5f 10 2c 92 51 b8 d7 0d b9 af af c1 Unfortunately, signtool is looking for something like: 1a3c377bc21b7be35f102c9251b8d70db9afafc1 Any thoughts on how to resolve this problem?