Sigh... more signing

Last time, I went over how to sign the bootstrapper and MSI via an msbuild-based postbuild step. This time, I hope to expand on that a little bit by introducing a few more MSBuild concepts.

We'll start off by creating a new setup project (SetupSignAll), and copy over the post build project file and signing key from SetupSigning. I'm a little bothered by the inconsistencies between signing the bootstrapper and msi. Let's fix that discrepancy by having both of them use signtool directly:

 <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>

        <Exec Command='signtool.exe sign /sha1 $(_ResolvedCertificateThumbprint) "$(BuiltOutputPath)"' />
        <Exec Command='signtool.exe sign /sha1 $(_ResolvedCertificateThumbprint) "$(ProjectDir)\$(Configuration)\setup.exe"' />
    </Target>
</Project>

Building succeeds (that is, after you remember to re-launch devenv from a VS Command prompt), and both the msi file and the bootstrapper are signed.

Now something else bothers me: the exec commands are basically the same except for the last argument, the path to the file to be signed. It would be nice to somehow refactor this to avoid duplication. Lets play around with ItemGroups and see if they are able to help us. Let's create a little test to run in our postbuild step. Lets begin by modifying the PostBuildEvent arguments to only run the "Test" target:

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

Building of course fails because there is no "Test" target. Lets fix that up by defining our Items and the Test target at the same time. We add:

     <ItemGroup>
        <BuiltSetupFile>
            <Path>$(BuiltOutputPath)</Path>
        </BuiltSetupFile>
        <BuiltSetupFile>
            <Path>$(ProjectDir)\$(Configuration)\setup.exe</Path>
        </BuiltSetupFile>
    </ItemGroup>
    
    <Target Name="Test">
        <Message Text="@(BuiltSetupFile)" />
    </Target>

My naive notion here is that while a Property is like a variable with an intrinsic type (int, boolean, etc.), while an Item is more like a struct, which can have multiple fields (all though in this case there is only one). This would gather all of the files we want to sign into a group called "BuiltSetupFile". To access the group, use the special "@" symbol (as opposed to the "$" symbol to access a property). Building now gives:

 E:\MWadeBlog\SetupSignAll\PostBuild.proj(8,9): error MSB4035: The required attribute "Include" is missing from element .

Well, apparently multiple fields will be allowed, but you have to have the "Include" attribute:

     <ItemGroup>
        <BuiltSetupFile Include="$(BuiltOutputPath)" />
        <BuiltSetupFile Include="$(ProjectDir)\$(Configuration)\setup.exe" />
    </ItemGroup>

Now, building gives:

 Target Test:
    E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi;E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe

Build succeeded.

There is some potential here: both the files were added to the item group, and we are able to display them as a group. Let's see how signtool likes this:

     <Target Name="SignSetup">
        <ResolveKeySource
            CertificateFile="$(CertificateKeyFile)">
            <Output TaskParameter="ResolvedThumbprint" PropertyName="_ResolvedCertificateThumbprint" />
        </ResolveKeySource>
        
        <Exec Command='signtool.exe sign /sha1 $(_ResolvedCertificateThumbprint) "@(BuiltSetupFile)"' />
    </Target>

gives:

 Target SignSetup:
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi;E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe"
    SignTool Error: File not found: E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi;E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe

So there is still a problem: we have to somehow the items into individual pieces. This time, let's use the special "%" symbol in our test task:

     <Target Name="Test">
        <Message Text="%(BuiltSetupFile.Identity)" />
    </Target>

This causes MSBuild to process the items individually:

 Target Test:
    E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi
    E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe

Build succeeded.

The ".Identity" corresponds to the "Include" attribute of the item. This looks promising for the signing task:

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

Which appears to work when building:

 Target SignSetup:
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi"
    Successfully signed: E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe"
    Successfully signed: E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe

Build succeeded.

I see 2 problems with this change. First, it seems we have taken 2 steps forward only to take 2 steps back: instead of having duplicate "exec" tasks, we just explicitly created 2 items to sign. Second, what happens if the bootstrapper is turned off from being built? Let's see what happens after changing the bootstrapper to not be built:

 Target SignSetup:
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi"
    Successfully signed: E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi    
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe"
    SignTool Error: File not found: E:\MWadeBlog\SetupSignAll\.\Debug\setup.exe

Signtool complained because it tried to sign a file which did not exist. There are several solutions to this problem. We could simply not list the item in the group, but this doesn't seem robust: what happens when we turn the bootstrapper back on? We will have to remember to re-add this item. Instead, let us condition the item so that it does not get added to the group:

     <ItemGroup>
        <BuiltSetupFile Include="$(BuiltOutputPath)" />
        <BuiltSetupFile Include="$(BootstrapperPath)" 
                        Condition="Exists($(BootstrapperPath))" />
    </ItemGroup>

Note that I had to create a new property for the path to the setup.exe; for whatever reason, it seems like Exists didn't like the combination of multiple properties. This seems to work just fine:

 Target Test:
    E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi

Build succeeded.

I could have added the condition to the Exec task; however, I think this is better because it makes the ItemGroup more re-usable for other tasks. But an even better technique would be to simply not list the bootstrapper item in the group. It seems that items can contain wildcards, including the *. So, let's test this with a new "BuiltOutputDir" (by adding /p:BuiltOutputDir="$(ProjectDir)$(Configuration)" to the PostBuildEvent):

     <ItemGroup>
        <BuiltSetupFile Include="$(BuiltOutputDir)\*.*" />
    </ItemGroup>

Running the Test target yields the expected result:

 Target Test:
    E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi

Build succeeded.

and turning the bootstrapper back on also seems to work:

 Target Test:
    E:\MWadeBlog\SetupSignAll\Debug\setup.exe
    E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi

Build succeeded.

I have high hopes for the SignSetup task as well:

 Target SignSetup:
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\Debug\setup.exe"
    Successfully signed: E:\MWadeBlog\SetupSignAll\Debug\setup.exe
    signtool.exe sign /sha1 963C7D3298349519368D1E36AB2BABBE84060DFA "E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi
    Successfully signed: E:\MWadeBlog\SetupSignAll\Debug\SetupSignAll.msi
    
Build succeeded.

Checking the files shows that they are in fact signed.

In summary, we built upon the prior signing example and learned more about:

  • Items and groups
  • Accessing items within groups
  • Adding conditions to item definitions and tasks

[This post written while listening to Led Zeppelin]

MWadeBlog_06_10_18.zip