Code-signing WISER on Windows#
Code-signing is a widely-accepted technique for verifying that distributable binaries come from a known source and have not been tampered with since they were built. On Windows, an unsigned installer triggers a Microsoft Defender SmartScreen warning (“Windows protected your PC”), and many browsers and anti-virus tools will flag the download. Code-signing the WISER installer and uninstaller removes these warnings and lets users trust that the application came from us.
This document describes how WISER is code-signed on Windows, what tools you need installed, and how the installer script itself works.
Unlike macOS, Windows code-signing does not require a separate notarization
step. You build the installer and sign it in a single make command, and the
signature is embedded directly into the .exe.
Tools You Need#
To build and code-sign the WISER installer on Windows you need two pieces of software installed locally — NSIS and the Windows SDK — plus the physical code-signing key.
NSIS#
The Nullsoft Scriptable Install System (NSIS)
compiles install-win/win-install.nsi into the WISER installer. Download the
installer from the NSIS download page
and run it; the default options are fine. After installation, makensis.exe is
typically located at C:\Program Files (x86)\NSIS\makensis.exe, which is the
path the WISER Makefile expects (see the NSIS variable near the top of the
Makefile).
Windows SDK (signtool)#
The actual signing is performed by signtool.exe, which ships with the
Windows 10/11 SDK.
Download and run the SDK installer. You only need the “Windows SDK Signing
Tools for Desktop Apps” component, though installing the full SDK is fine.
After installation, signtool.exe lives under a versioned path such as:
C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe
The version number in that path (10.0.26100.0 above) depends on which SDK
version you installed, so it will likely differ on your machine. See
Configuring the Signtool Path below for how to find
your exact path and point the install script at it.
Code-signing certificate (USB token)#
WISER is signed with a certificate stored on a physical USB hardware token (for example a YubiKey or a SafeNet/eToken device). To sign, you must:
Plug the USB token into your machine.
Install the token vendor’s driver/middleware (e.g. SafeNet Authentication Client) so Windows can talk to it. Once installed, the token’s certificate is automatically registered in the Windows Certificate Store.
Know the certificate’s SHA-1 thumbprint (see Finding Your Certificate Thumbprint), which is how the install script selects the right certificate.
Because the script identifies the certificate by thumbprint and resolves it from the Windows Certificate Store at signing time, the only machine-specific secret you need is the thumbprint — there is nothing else to copy off of the original build machine.
Building and Signing: the make dist-win command#
On Windows, building and signing happen together in a single Make target. From
the top of the WISER repository, in the wiser-prod conda environment, run:
make dist-win
This first runs the build-win target (which freezes WISER with PyInstaller
into dist/WISER) and then invokes NSIS. The build-and-sign line in the
Makefile is:
$(NSIS) /NOCD /DWISER_VERSION="$(APP_VERSION)" /DSHA1_THUMBPRINT=$(SHA1_THUMBPRINT) install-win\win-install.nsi
Here is what each part does:
$(NSIS)— the path to the NSIS compiler,makensis.exe. It is defined once near the top of theMakefileasNSIS="C:\Program Files (x86)\NSIS\makensis.exe". This is the program that reads the.nsiscript and produces the installer.exe./NOCD— “no change directory.” By defaultmakensischanges its working directory to the folder containing the script./NOCDtells it to stay in the directory wheremakewas invoked (the repository root) so that the relative paths inside the script — likedist\WISER\*.*andinstall-win\license.rtf— resolve correctly./DWISER_VERSION="$(APP_VERSION)"— defines theWISER_VERSIONsymbol inside the script. The/Dflag is NSIS’s “define” option, analogous to a compiler’s-D.$(APP_VERSION)is computed by theMakefileby runningpython src/wiser/version.py, so the version is never hard-coded. The script usesWISER_VERSIONto name the install directory, the registry keys, and the output file (e.g.Install-WISER-1.4b1.exe). The script deliberately!errors out if this symbol is not provided./DSHA1_THUMBPRINT=$(SHA1_THUMBPRINT)— defines theSHA1_THUMBPRINTsymbol: the SHA-1 thumbprint of your code-signing certificate.$(SHA1_THUMBPRINT)comes from yourSecret.mkfile — a copy ofSecret-template.mkthat is not checked into git.signtooluses this thumbprint to pick the correct certificate out of the Windows Certificate Store. The script also!errors out if this symbol is not provided.install-win\win-install.nsi— the NSIS script to compile. This is the last argument, and is the script described in How the Install Script Works below.
The related quick-sign-win target runs the exact same makensis command
without first rebuilding with PyInstaller. It is handy when dist/WISER is
already built and you only want to (re)build and sign the installer.
Configuring the Signtool Path#
The win-install.nsi script needs to know where signtool.exe lives on your
machine. This is set near the top of the script with a single define:
!define SIGN_TOOL "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool"
Because the SDK version number in that path differs between machines, you will
likely need to change it. To find your own signtool.exe:
Open File Explorer and navigate to
C:\Program Files (x86)\Windows Kits\10\bin.You will see one or more version folders (e.g.
10.0.22621.0,10.0.26100.0). Open the newest one.Open the
x64subfolder.signtool.exeshould be inside.Copy that folder path (without the trailing
.exe) into theSIGN_TOOLdefine.
Alternatively, from a PowerShell prompt you can search for it directly:
Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
Where-Object { $_.FullName -like "*x64*" }
Use the path to the x64 build for 64-bit Windows.
Finding Your Certificate Thumbprint#
The SHA1_THUMBPRINT value comes from your code-signing certificate. With the
USB token plugged in and its middleware installed, list the certificates in
your personal store from PowerShell:
Get-ChildItem Cert:\CurrentUser\My | Format-List Subject, Thumbprint
Find the WISER code-signing certificate in the output and copy its
Thumbprint value (a 40-character hex string). Put it in your Secret.mk:
SHA1_THUMBPRINT=your.sha1.thumbprint.here
The thumbprint is a property of the certificate itself, so it is the same on any machine the token is plugged into. It is the only signing secret you need to carry between machines.
How the Install Script Works#
install-win/win-install.nsi does two jobs that have to be carefully ordered
so that both the installer and the uninstaller end up code-signed. NSIS
normally embeds the uninstaller into the installer at compile time, but an
uninstaller created that way cannot itself be signed. To work around this, the
script uses the
two-pass technique from the NSIS wiki:
Inner pass. The script invokes
makensison itself a second time with theINNERsymbol defined (see the!makensiscall in the outer block). WhenINNERis defined, the script’s only job is to write the uninstaller binary out to%TEMP%and quit. The outer pass then runs that temporary installer, which drops a standaloneUninstall WISER-<ver>.exe.Sign the uninstaller. Back in the outer pass, the script calls
${SIGN_TOOL}to code-sign the just-created uninstaller:"${SIGN_TOOL}" sign /sha1 "${SHA1_THUMBPRINT}" /fd SHA256 /t http://timestamp.sectigo.com "%TEMP%\Uninstall WISER-<ver>.exe"
Build the real installer. The outer pass continues, packaging the frozen
dist\WISERtree, the icon, and the now-signed uninstaller into the finalInstall-WISER-<ver>.exe.Sign the installer. The
!finalizedirective runs after the installer is written and signs it the same way (%1is the installer filename that NSIS just produced).
The signing commands themselves are straightforward:
sign /sha1 "${SHA1_THUMBPRINT}"selects the certificate by thumbprint from the Windows Certificate Store (populated by the USB token’s middleware)./fd SHA256sets the file digest algorithm to SHA-256./t http://timestamp.sectigo.comadds a trusted timestamp so the signature stays valid even after the signing certificate eventually expires.
Both signing commands reference the single SIGN_TOOL define, so updating that
one line (see Configuring the Signtool Path) is enough
to point the whole script at your signtool.exe.