Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sign every executable with an Authenticode certificate through MSBuild

I have an Authenticode certificate (.pfx) which I use to sign executables.

How can I configure Team Build so that it signs every single executable (.exe, .dll, ...) automatically while building the project?

like image 545
Mephisztoe Avatar asked Aug 28 '09 13:08

Mephisztoe


2 Answers

Here's the method we use:

  1. Unload the WiX project and select Edit

  2. Scroll to the bottom, where you can find <Import Project="$(WixTargetsPath)" />

  3. Add a new line immediately above it: <Import Project="ProjectName.custom.targets" /> We use the naming convention "ProjectName.custom.targets", but the file can be named anything you want.

  4. Create a new XML file named ProjectName.custom.Targets and place the following code into it:

    <?xml version="1.0" encoding="utf-8"?>
    <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        <!-- replace the contents of this with your private test authenticode certificate -->
        <AuthenticodeCertFile Condition="'$(AuthenticodeCertFile)' == ''">$(MSBuildProjectDirectory)\AuthenticodeTest.pfx</AuthenticodeCertFile>
      </PropertyGroup>
    
      <!-- this gets the path to signtool.exe and places it in the _SignToolSdkPath property -->
      <Target Name="_GetSignToolPath">
        <GetFrameworkSdkPath>
          <Output TaskParameter="Path" PropertyName="_SignToolSdkPath" />
        </GetFrameworkSdkPath>
        <PropertyGroup>
          <_SignToolPath>$(_SignToolSdkPath)bin\signtool.exe</_SignToolPath>
        </PropertyGroup>
      </Target>
    
      <!-- This gets a list of all of the "referenced" assembies used by the installer project --> 
      <!-- Unfortunately, I cheated and used an "internal" item list - you could replace this with each specific assembly but it gets complicated if your build output is redirected -->
      <Target Name="_GetSourceAssembliesToSign" DependsOnTargets="ResolveReferences">
        <!-- Kludge - not supposed to target internal items, but there are no other options -->
        <CreateItem Include="@(_ResolvedProjectReferencePaths)">
          <Output ItemName="_SourceAssemblyToSign" TaskParameter="Include" />
        </CreateItem>
      </Target>
    
      <!-- This signs the assemblies in the @(_SourceAssemblyToSign) item group -->
      <!-- Note that it only executes when build output is redirected ie/ on TFS Build or when OutDir is changed -->  
      <!-- Authenticode timestamp is optional - doesn't make sense to timestamp the test certificate -->
      <Target Name="_AuthenticodeSignSourceAssemblies" AfterTargets="BeforeBuild" DependsOnTargets="_GetSignToolPath;_GetSourceAssembliesToSign" Condition="'$(AuthenticodeCertFile)' != '' and '$(OutDir)' != '$(OutputPath)'">
        <Exec Command="&quot;$(_SignToolPath)&quot; sign /f &quot;$(AuthenticodeCertFile)&quot; /p &quot;$(AuthenticodePassword)&quot; /t $(AuthenticodeTimestamp) /v &quot;%(_SourceAssemblyToSign.Identity)&quot;" Condition="'$(AuthenticodeTimestamp)' != ''" />
        <Exec Command="&quot;$(_SignToolPath)&quot; sign /f &quot;$(AuthenticodeCertFile)&quot; /p &quot;$(AuthenticodePassword)&quot; /v &quot;%(_SourceAssemblyToSign.Identity)&quot;" Condition="'$(AuthenticodeTimestamp)' == ''" />
      </Target>
    
      <!-- This signs the MSI file itself -->
      <!-- Note that additional changes may be needed if your CAB files are separate - those would need to be signed as well -->
      <!-- Note that it only executes when build output is redirected ie/ on TFS Build or when OutDir is changed -->  
      <Target Name="_AuthenticodeSignMsi" AfterTargets="SignMsi" DependsOnTargets="_GetSignToolPath" Condition="'$(AuthenticodeCertFile)' != '' and '$(OutDir)' != '$(OutputPath)'">
        <PropertyGroup>
          <_MsiFileToSign>$(TargetDir)%(CultureGroup.OutputFolder)$(TargetName)$(TargetExt)        </_MsiFileToSign>
        </PropertyGroup>
    
        <Exec Command="&quot;$(_SignToolPath)&quot; sign /f &quot;$(AuthenticodeCertFile)&quot; /p &quot;$(AuthenticodePassword)&quot; /t $(AuthenticodeTimestamp) /v &quot;$(_MsiFileToSign)&quot;" Condition="'$(AuthenticodeTimestamp)' != ''" />
        <Exec Command="&quot;$(_SignToolPath)&quot; sign /f &quot;$(AuthenticodeCertFile)&quot; /p &quot;$(AuthenticodePassword)&quot; /v &quot;$(_MsiFileToSign)&quot;" Condition="'$(AuthenticodeTimestamp)' == ''" />
      </Target>
    </Project>
    

Create a test authenticode certificate (we named ours AuthenticodeTest.pfx) and place it in source control - the path to it is set in the AuthenticodeCertFile property. To test it out, run msbuild at command line and change the OutDir property - ie/ msbuild Test.sln /p:OutDir=C:\Test

Some customizations will be needed if:

  • If you don't want to use the "private" item group (I cheated)
  • If you don't use WiX project references
  • If your cab files are separate from the MSI they will need to be signed as well

To run your final build select "Queue New Build" in TFS. Click "Parameters" and expand "Advanced". Under "MSBuild Arguments" add /p:AuthenticodeCertFile=ProductionCertFile.pfx /p:AuthenticodePassword=Secret. Note that this may not be entirely secure - it could be tricky to have the build agent find the PFX file without checking it in and the password could be logged in the build output. Alternately you could create a special locked down build agent for this, or run the build locally at command line - but obviously that wouldn't be a "clean room" environment. It may be worth creating a special locked down "clean" server specifically for that purpose.

like image 94
ShadowChaser Avatar answered Oct 30 '22 05:10

ShadowChaser


Since the code signing certificate must be installed on the build computer in order to perform signing, why not sign everything that is built on that computer every time it is built? The computer is "at risk" because it has the code signing certificate installed, so it will need to be protected in some fashion (physical security and system security). If it is protected, why not let it do the work it was intended to do, prepare the files for delivery, consistently, repeatably, every time?

Unfortunately, the answer "don't" also seems to be the standard Microsoft answer, since they seem to provide almost no support in MSBuild to loop over a list of file names, calling a program once for each file name in the list. I've found ways to pass a wildcard generated list of files to the Signtool.exe program, but it can only handle one file at a time.

I fear (for me) that it is back to writing a batch file which loops over its arguments and calls signtool for each argument. Writing batch files for the common task of signing a build output makes me think MSBuild really isn't as mature a build system as it should be. Either that, or signtool has the wrong interface. In either case, signing multiple files without enumerating the name of every file to sign appears to be a "no go" with MSBuild.

like image 29
Mark Waite Avatar answered Oct 30 '22 06:10

Mark Waite