Time stamping AppX packages

Signing basics

Visual Studio makes it fairly easy to sign your Universal Windows Platform AppX packages using a certificate so the author and publisher can be verified.

When you create a new UWP application project, you’ll find a file named ProjectName_TemporaryKey.pfx in it with a self-signed certificate and private key that will sign your development builds. If and when you associate the project with an app in the Windows Store, you also get a store certificate issued by Microsoft (ProjectName_StoreKey.pfx) for signing the packages that you upload to the store. If you distribute apps for sideloading, to testers or within the enterprise, you will need a proper Authenticode code signing certificate from a vendor such as Symantec (formerly VeriSign).

In the project I’m currently working on, we build our app both for sideloading (for internal testers) and for the store. To enable that, we have different build configurations on our CI server and a project file that contains the following properties (thumbprints excluded for readability):

<PackageCertificateKeyFile Condition="'$(JaywaySigningCertificate)' == 'enterprise'">Jayway.WindowsApp_EnterpriseKey.pfx</PackageCertificateKeyFile>
<PackageCertificateThumbprint Condition="'$(JaywaySigningCertificate)' == 'enterprise'">...</PackageCertificateThumbprint>
<PackageCertificateKeyFile Condition="'$(JaywaySigningCertificate)' == 'store'">Jayway.WindowsApp_StoreKey.pfx</PackageCertificateKeyFile>
<PackageCertificateThumbprint Condition="'$(JaywaySigningCertificate)' == 'store'">...</PackageCertificateThumbprint>

Each build configuration overrides the JaywaySigningCertificate property, which in turn sets all the other properties related to signing according to what kind of build is being produced. So far so good. If we look closely at an .appx or .appxbundle  file produced by Visual Studio or our build server, we’ll find that the package is indeed signed but not time stamped, as indicated by the Timestamp column on the Digital Signatures tab in the file’s properties dialog.

AppX without time stamp

Why time stamping?

So why would you want to time stamp a signed binary?

The problem with not doing so is that as soon as the certificate’s validity period expires, the package is no longer considered valid and trying to install it will fail. Code signing certificates typically expire and have to be renewed every other year or so. For example, if you had a certificate that was valid until December 31st 2016 and signed a package with it on December 29th, that package would be invalid the week after. In my experience, it’s easy to forget about certificate expiration dates and when they expire it always comes as an unpleasant surprise at a bad time.

When you verify the validity of a binary without a time stamp, the system will check if the signing certificate is still valid today and fail if it isn’t. But if the binary was time stamped, the validation check will verify that the certificate was valid at the point in time when the signing was done and the time stamp was written. That makes a time stamped package useful essentially forever, or at least until something else makes the validation fail, such as revoked root certificates or insecure algorithms (bye bye SHA-1).

Let’s simulate what happens with a signed package without a time stamp when the certificate used to sign it expires, by moving the system clock forward. In this case, I used a certificate that expires in the fall of 2017, so by moving the clock to the end of the year we’ll see what happens. I use SignTool from the Windows SDK and its verify command to check if the system thinks my binary is any good.


Here we can clearly see that after the certificate has expired, the package is no longer valid and any packages signed with it will become useless for our test users.

So by time stamping we extend the time period that our binaries are useful and we need to worry a bit less about certificate expiration dates. And there’s really no reason not to do it.

Signing and time stamping AppX

Unfortunately, Microsoft makes it much more complicated than you’d expect to time stamp your UWP package. (Hence the need for this blog post.) The regular AppX build pipeline doesn’t support it. Other types of binaries can be time stamped even after signing by using SignTool with its timestamp command, but that doesn’t work for AppX packages. Probably because they are archived bundles that contain much more than plain PE executables. The documentation explicitly says that “You can’t use the SignTool time stamp operation on a signed app package; the operation isn’t supported”.

So if we can’t add the timestamp after building, we need to modify the build and signing process itself. The AppX build process is defined in an MSBuild targets file called Microsoft.AppxPackage.targets, located (for Visual Studio 2015) under %ProgramFiles(x86)%\MSBuild\Microsoft\VisualStudio\v14.0\AppxPackage. I encourage you to browse through it if you’re familiar with MSBuild syntax and interested in the inner workings of building AppX files. The build task used for signing the packages is called SignAppxPackage, and is implemented in an assembly named Microsoft.Build.AppxPackage.dll in the same directory.

If you’re really curious and peek at that assembly with a .NET decompiler, you can find traces of unfinished support for time stamping that’s currently not exposed for us to use. So maybe this will get easier in the future. As of now though, the SignAppxPackage build task is not extensible or easily configurable in the way we want it. However, internally the task launches SignTool to do its job, and it provides a property named SignAppxPackageExeFullPath that we can use to point to the executable we want to use. It verifies that we specify an .exe file, so a batch or script file will not work.

So what we have to do is to create our own small executable that we tell the SignAppxPackage task to launch instead of the default Signtool.exe included in the Windows SDK. Our program will just act as a thin wrapper that will modify the command line and add what’s needed for time stamping before finally launching the real SignTool.

The command line change we need is specifically to insert the /tr option, specifying that we want the output to be time stamped using the specified RFC 3161 time stamp server. We only want to do this when SignTool is executed with the sign command. Your certificate issuer should be able to provide you with the time stamp server to use.

Since we also want to preserve any output from SignTool, we need the wrapper to forward any stdout or stderr output produced by SignTool back to the MSBuild task.

Below is the source code for such a wrapper executable along with its config file. I chose to put the time stamp server URL and the path to the real SignTool in an app.config file. Another option could be to pass them as environment variables. The path to Signtool.exe could be left hardcoded, but as you’ll see below I chose to overwrite it at build time, supplying the actual path that the SignAppxPackage build task is using.

// signtool_timestamp_wrapper.cs
using System;
using System.Configuration;
using System.Diagnostics;

class SignToolTimeStampWrapper
  public static void Main(string[] args)
    string commandLine = Environment.CommandLine;
    if (args.Length > 0) {

      // Strip off this executable
      commandLine = commandLine.Substring(commandLine.IndexOf(commandLine.StartsWith("\"") ? '"' : ' ', 1) + 1).TrimStart();

      const string SignCommand = "sign";
      if (commandLine.StartsWith(SignCommand + " ", StringComparison.InvariantCultureIgnoreCase)) {
        commandLine = commandLine.Insert(SignCommand.Length,  $" /tr {ConfigurationManager.AppSettings["TimeStampServerUrl"]}");
    else {
      commandLine = "";
    Process signTool = new Process() {
      EnableRaisingEvents = true,
      StartInfo = new ProcessStartInfo() {
        FileName = ConfigurationManager.AppSettings["SignToolPath"],
        Arguments = commandLine,
        UseShellExecute = false,
        RedirectStandardOutput = true,
        RedirectStandardError = true

    signTool.ErrorDataReceived += (sender, e) => Console.Error.WriteLine(e.Data);
    signTool.OutputDataReceived += (sender, e) => Console.WriteLine(e.Data);
<!-- signtool_timestamp_wrapper.exe.config -->
    <add key="SignToolPath" value="c:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe" />
    <add key="TimeStampServerUrl" value="http://sha256timestamp.ws.symantec.com/sha256/timestamp" />

Compile and add this little wrapper program to your code repository. The final step is to ensure that it gets called when we build our app. We do that with the following addition at the end of our project file.

<Target Name="ReplaceSignToolWithTimeStampWrapper" AfterTargets="_GetSdkToolPaths" Condition="'$(JaywaySigningCertificate)' == 'enterprise'">
    <SignToolTimeStampWrapperConfigFile Include="$(SignAppxPackageExeFullPath).config" />
  <XmlPoke XmlInputPath="@(SignToolTimeStampWrapperConfigFile)"
    Value="$(OriginalSignAppxPackageExeFullPath)" />

This additional build target is inserted into the AppX build process after the  _GetSdkToolsPaths target, which as the name implies resolves the paths to a number of SDK tools and stores in their respective properties. The one we care about is of course the path to Signtool.exe, which is stored in the property SignAppxPackageExeFullPath. It is defined as an overridable property in Microsoft.AppxPackage.targets. In the PropertyGroup we store the original path in our own property called OriginalSignAppxPackageExeFullPath, and then override SignAppxPackageExeFullPath to point to our time stamping wrapper. Finally we use XmlPoke to write the OriginalSignAppxPackageExeFullPath property value with the original tool path to the wrapper’s config file.

That’s it! Not exactly as straight forward as it should be, but definitely doable. Lets build a new package and see the end result.


Our build output now has a time stamp. If we redo the test with moving the system clock forward, we see that our package remains valid even after our certificate has expired.


This Post Has 2 Comments

  1. Alberto Silva

    Excelent article, thank you for sharing!

  2. Michal Náprstek

    Hi Mattias, thank you very much for this awesome tutorial! It works like a charm. Only one small note for all copy-pasters like me – remember to remove the condition from the target ;-)

Leave a Reply