Sign every executable with an Authenticode certificate through MSBuild
Asked Answered
F

4

13

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?

Fredenburg answered 28/8, 2009 at 13:44 Comment(3)
A DLL is an assembly, not an executable. an EXE is an executable assembly. So, you really want to know how to sign assemblies, not executables. The keyword difference might help - can't say I know much about signing, though.Pyrogallol
In fairness to the OP - MS use the term "executable (.exe, .dll ...)" in the Windows Logo programs.Rocray
Additionally, native/unmanaged DLLs and EXEs are not assemblies. Calling EXEs and DLLs assemblies is .Net-specific terminology and only applies to CLI. Unmanaged DLLs can't go in GAC and cannot be Strong-Name signed, but they can be authenticode signed. Ref: Assembly CLI, wikipediaByington
D
18

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.

Detinue answered 7/6, 2011 at 15:26 Comment(1)
A way to avoid having the password in plain text in the project/MSBuild file is described in Stack Overflow question How do I securely store a .pfx password to use in MSBuild?. Essentially, it uses the Windows certificate store and the /sha1 option for signtool.exe. The security is then tied to the user account that gets to have a certificate store with the certificate in question.Zilvia
W
3

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.

Warehouseman answered 29/10, 2009 at 23:55 Comment(4)
I found a way of letting signtool iterate over a list of files from within a team build. Since it has to be done at solution level, the only drawback is that you need to speficy a filter for including/excluding files (e.g. you won't want to sign 3rd party libraries and you will only want to include exe and dll files). I you're interested, I can post some hints on our development blog (which is german however...)Fredenburg
I would be interested in your method of iterating signtool over a list of files, and am happy to read it in German.Warehouseman
We just use a batch file that we <Exec> from MSBuild. Two lines of code (a trivial 'for' statement) that recursively iterate over all exes and dlls in the target folder, running signtool on each.Softy
I don't know if things have changed in MSBuild since 2009, but you can definitely loop over a list of files and run a tool for each file in the list. You're looking for task batching.Highhat
N
2

My answer builds on top of ShadowChaser's answer. the difference that my MSBuild target will sign a specified file or if none is specified, it will sign the build output of the project from which the target was invoked.

save the following in a targets file let's say signing.custom.targets. once you save it just include it in your csproj or wixproj and use the target to sign your output. this will also work on local dev machine as the sign.properties (its a hybrid dev env so we didn't want to specify cert properties twice) file does not exist on local dev box.

to sign a file from command line using this, you can

MSBuild.exe signing.custom.targets /t:SignAssembly /p:_MsiFileToSign="..\Builds\Release\Setups\yourFileToSign.msi

signing.custom.targets definition

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <CertPath Condition= "'$(CertPath)' == ''">c:\dev\sign\</CertPath>
    <Config Condition="'$(Config)' == ''">Release</Config>
    <MSIProductVersion Condition ="'$(MSIProductVersion)' ==''">16.3</MSIProductVersion>
    <MSIBuildNumber Condition ="'$(MSIBuildNumber)' ==''">3207</MSIBuildNumber>
  </PropertyGroup>

  <PropertyGroup Condition="'$(OutputType)'=='Library'">
    <_MsiFileToSign Condition="'$(_MsiFileToSign)' ==''" >$(OutputPath)$(AssemblyName).dll</_MsiFileToSign>
  </PropertyGroup>
  <PropertyGroup Condition="'$(OutputType)'=='Exe'">
    <_MsiFileToSign Condition="'$(_MsiFileToSign)' ==''">$(OutputPath)$(AssemblyName).exe</_MsiFileToSign>
  </PropertyGroup>

  <Target Name="ReadProperties">
    <ReadLinesFromFile File="$(CertPath)sign.properties">
      <Output TaskParameter="Lines" PropertyName="PropsInOneLine" />
    </ReadLinesFromFile>
  </Target>
  <Target Name="CreateProperties" DependsOnTargets="ReadProperties">
        <PropertyGroup>
            <SignToolPath>$(CertPath)signtool.exe</SignToolPath>
            <AuthenticodeCertFile Condition="'$(PropsInOneLine)' != ''">$(CertPath)$([System.String]::Copy($(PropsInOneLine)).Split(';')[0].Split('=')[1])</AuthenticodeCertFile>
            <AuthenticodePassword Condition="'$(PropsInOneLine)' != ''">$([System.String]::Copy($(PropsInOneLine)).Split(';')[2].Split('=')[1])</AuthenticodePassword>
            <AuthenticodeTimestamp Condition="'$(PropsInOneLine)' != ''">$([System.String]::Copy($(PropsInOneLine)).Split(';')[4].Split('=')[1])</AuthenticodeTimestamp>
        </PropertyGroup>
    </Target>
  <Target Name="SignAssembly"
          DependsOnTargets="CreateProperties"  >
    <Message Text=" File Name to sign= $(_MsiFileToSign)" />
    <Exec Command="&quot;$(SignToolPath)&quot; sign /f &quot;$(AuthenticodeCertFile)&quot; /p &quot;$(AuthenticodePassword)&quot; /t $(AuthenticodeTimestamp) /v &quot;$(_MsiFileToSign)&quot;"  Condition="'$(PropsInOneLine)' != ''" />
  </Target>
  <Target Name="SignMsi"
          DependsOnTargets="CreateProperties"  >
        <PropertyGroup>

            <_MsiFileToSign>$(TargetPath)</_MsiFileToSign>
        </PropertyGroup>

        <Message Text=" File Name to sign= $(_MsiFileToSign)" />
        <Exec Command="&quot;$(SignToolPath)&quot; sign /f &quot;$(AuthenticodeCertFile)&quot; /p &quot;$(AuthenticodePassword)&quot; /t $(AuthenticodeTimestamp) /v &quot;$(_MsiFileToSign)&quot;"  Condition="'$(PropsInOneLine)' != ''" />
  </Target>
</Project>

including it in your csproj

<Import Project="$(SolutionDir)signing.custom.targets" />
<Target Name="AfterBuild" DependsOnTargets="SignAssembly">
</Target>

to include it in your wixproj

<Import Project="$(SolutionDir)signing.custom.targets" />
<Target Name="AfterBuild" DependsOnTargets="SignAssembly">
</Target>
Nattie answered 20/11, 2015 at 16:50 Comment(0)
F
-5

Don't.

You do not want to automatically sign builds. Most builds don't need signing anyway; they're only used for automating tests. Some builds may be handed to your in-house testers. But only builds that you actually release outside your organization need Authenticode signatures.

In that case, you should have a manual verification step after signing anyway. So, signing manually doesn't insert an extra manual step in the release process, and automating it saves very little time. In exchange, there will be far fewer signed files floating around in your organization, and you can make much stronger guarantees about the files that are.

Fomentation answered 28/8, 2009 at 14:57 Comment(6)
Sounds reasonable not to sign everything everytime. However, we need to establish an automated process to sign the release version of the software. Also for verification through a platform test for Microsoft, every .exe, .dll and .msi needs to be signed. We have an automated process that gets the current sources, auto-version them, builds them, creates licenses and a directory structure, packages everything in MSIs, puts docs into the folders and finally zips everything for distribution. We would simply add another build script (MS Build -> Team Build) for the signing part. But how?Fredenburg
Oh, I'm agreeing that you should sign the release version. And I'd agree with a mostly-automated process. Just put a pause prompt in the release process, before packing the binaries into the MSI. At that point you can easily sign the binaries you want signed. They are all created at this point, so it's easy to refer to them using a FOR loop or possibly even just a wildcard.Fomentation
I disagree about "not bothering" to sign. We ship over 100 signed assemblies in our product, and they are packaged into an installer that is also signed. Not automating this would be madness. It's much better to test your actual release product than something "similar" to it, and it's safer if all your installers are obfuscated and signed so that an unprotected copy can't leak into the wild. We also run an Agile shop, so any build (that is good) is potentially a release. (We don't sign our CI build though, but that doesn't produce an exe that is actually used for anything)Softy
Obviously you would tool your release process such that signing all 100 assemblies is a single manual step. But that's part of the release process, not the build process.Fomentation
I disagree. Code should be signed at every step, including non-release builds. Create a test certificate, otherwise you can't verify the behavior of Authenticode on your systems. Consider .NET, where authenticode assemblies behave differently. We had one of our applications perform extremely badly for modem users due to revocation list lookup during "evidence gathering". You also state that the assemblies should be signed during the "release process". In the case of an installer, that would require the installer to be disassembled, signed, then reassembled. Insane.Detinue
Sorry, but your response doesn't make sense. Why would you need to disassemble an installer? Like the executable, it's build as part of the release process. The executable is signed, added to the installer, and then the installer is built. (The release process necessarily includes building the executable from a tagged state of source control, to enable reproducability. So any steps afterwards also are part of the release process.)Fomentation

© 2022 - 2024 — McMap. All rights reserved.