As of June 1, 2023, the private keys required to perform Windows code signing must be stored on a FIPS-compliant device. In most cases, a Yubikey is the tool of choice. This solves some problems and creates others.

In the past, when I ordered my certificates from K Software, as so many Xojo developers do, actually getting my private key was a huge challenge. K Software required the use of old browsers, and thanks to browser auto-updating, extracting the private key was problematic to say the least. So using my own private key is an absolute blessing. But the new frustration is that the private key now has to be locked on a Yubikey.

Buying a certificate

For my most recent renewal, I decided to go through ssl.com instead of K Software to get a much better price. At the time of this writing, a 1-year OV certificate from K Software is $313, and only $129 from ssl.com. At K Software's price, you might as well buy the EV certificate for $405 and get that sweet instant SmartScreen reputation. But ssl.com also allows you to buy up to 10 years, which drops the price to just $64.50 per year. This is the option I took, although it should be noted that they are only allowed to issue you one 3-year certificate at a time. When that expires, they will issue you a new one.

K Software will sell you the Yubikey for $90. That's a very fair price, considering that one directly from Yubico costs $80-$90, depending on the model. On the other hand, ssl.com's price for a Yubikey is absolutely insane at $279. There's some value in getting the key from them if you want to avoid the attestation steps. But if you can follow instructions and use a command line, save yourself the $200 and buy directly from Yubico.

Setting up the Yubikey

Ok, so the certificate is purchased, the validation is done, the Yubikey is delivered, and now it's time to actually sign something. You need to perform steps called "attestation", and ssl.com's documentation for this is actually pretty good, so I won't repeat it here. Instead, I'll link to their article on the subject.

HOWEVER there are two pieces of information they left out.

Setup PIV

First, you'll need to set up your private key on your Yubikey before following the other instructions. In Yubikey Manager, go to the PIV section under Applications and press "Configure PINs" to begin the setup. If you've already done this for your Yubikey, skip to the next section. The PIN is something like a six-character code that you will use as a password for signing. It's generally something you'll want to remember. The PUK is an eight character code that can be used to unlock the device if the PIN has been entered incorrectly too many times. It doesn't have to be memorable, but it should be stored somewhere safe. The Management Key is a longer string that you'll need to generate. My advice is to use a quick Xojo app:

Var Board As New Clipboard
Board.Text = EncodeHex(Crypto.GenerateRandomBytes(32))

Both the PUK and Management Key should be stored in a secure location, such as a reputable password manager.

Request a Code Signing Certificate

Second, in step 10 of their guide, they instruct you to go to the "End Entity Certificates" section to download the DER certificate to install on your Yubikey. They skipped a step. When you buy a certificate, they issue you an eSigner certificate so you have the privilege of paying for the overpriced eSigner service. This service would cost me about $175 per month. No one should use it.

Instead, you will need to open a ticket or use their live chat to have them create an actual code signing certificate. In the End Entity Certificates, you should see both an "eSigner Code Signing Certificate" and a "Code Signing Certificate". Use the table in the section for the regular "Code Signing Certificate" section. Also, don't use the look-alike table at the top of your dashboard labeled "Certificate Downloads".

Once you've completed the attestation steps, you're ready to try signing something. Code signing on Windows is most commonly done with signtool, and Yubico has instructions on how to use your Yubikey to sign a file with signtool.

These instructions work fine, and if you already have signtool installed, I recommend following them to get familiar. When you run the command, you'll get a prompt asking for your PIN, and once you enter it, your file will be signed. It's really not much different from how we used to sign before the Yubikey requirement.

Signing without the PIN

The problem is that your PIN is needed for every single file that needs to be signed. For an Inno Setup installer, the absolute minimum number of signatures required is 3: your application, the installer, and the uninstaller. But realistically it will be more. And if you have multiple installers like I do - 32-bit, 64-bit, ARM, and combo - the number of times your PIN is needed can be very annoying.

This is where osslsigncode comes in handy. This is an alternative to signtool that can use the Yubikey via PKCS#11 instead of using it as a smart card. It also allows the PIN to be passed on the command line, so we can now skip the PIN prompt altogether.

My instructions are based on this StackOverflow answer. I have modified the instructions in some cases.

  1. Download and install the yubico-piv-tool from here.
  2. Add C:\Program Files\Yubico\Yubico PIV Tool\bin to your PATH variable so the libykcs11.dll library can find its siblings. Instructions for editing your PATH variable can be found here. If you have any command prompts open, close them so your new ones will have the new PATH variable.
  3. Create a directory to put all your signing stuff. I chose a "Signing" folder in my "Documents" folder.
  4. Download both osslsigncode and pksc11.dll from GitHub and extract them to your signing folder. I chose to rename my osslsigncode folder to remove the version information to make it easier to update in the future.
  5. Use Yubikey Manager to export your public certificate to your signing folder. I just named mine certificate.crt.

Your folder structure should look something like this:

Signing
  osslsigncode
    bin
      libcrypto-3-x64.dll
      libcurl.dll
      osslsigncode.exe
      zlib1.dll
    share
      bash-completion
        completions
          osslsigncode.bash
  certificate.crt
  pkcs11.dll

Now you're ready to sign a file. Get the path to a file and sign it with:

& "C:\Users\Thom\Documents\Signing\osslsigncode\bin\osslsigncode.exe" sign -pkcs11module "C:\Program Files\Yubico\Yubico PIV Tool\bin\libykcs11.dll" -pkcs11engine "C:\Users\Thom\Documents\Signing\pkcs11.dll" -pass <PIN> -ts http://timestamp.sectigo.com -key "pkcs11:id=%01" -certs "C:\Users\Thom\Documents\Signing\certificate.crt" -nolegacy -in <UnsignedFile.exe> -out <SignedFile.exe>

Great, now we don't have to enter a PIN to sign a file.

The Inno Setup problem

Inno Setup requires that the file be signed in-place, i.e. the file being asked to be signed is replaced by the signed version. osslsigncode will absolutely not do this. Instead, we'll use a batch script as in intermediate. The script will rename the original file, sign it in place of the original file, and then delete the renamed file.

Now create a file innosign.bat in your signing folder. It should look like this:

@echo off
set mypath=%~dp0
set script_path=%mypath:~0,-1%
set source_path=%~1
set pass=%~2
set name=%~3
set url=%~4
for %%i in ("%source_path%") do (
    set file_drive=%%~di
    set file_name=%%~ni
    set file_ext=%%~xi
    set file_path=%%~pi
)
set renamed_file=%file_name%-original%file_ext%
set renamed_path=%file_drive%%file_path%%renamed_file%
ren "%source_path%" "%renamed_file%" || (
    echo Failed to rename file
    exit /b 1
)
"%script_path%\osslsigncode\bin\osslsigncode.exe" sign -pkcs11module "C:\Program Files\Yubico\Yubico PIV Tool\bin\libykcs11.dll" -pkcs11engine "%script_path%\pkcs11.dll" -pass "%pass%" -ts "http://timestamp.sectigo.com" -key "pkcs11:id=%%01" -certs "%script_path%\certificate.crt" -n "%name%" -i "%url%" -in "%renamed_path%" -out "%source_path%" -nolegacy || (
    ren "%renamed_path%" "%file_name%%file_ext%"
    echo Failed to sign file
    exit /b 1
)
del "%renamed_path%"

This can be called as innosign.bat <file> <pin> <app name> <website>.

In Inno Setup, use "Configure Sign Tools" and create a new tool. I named mine "Yubi" and the command looks like this:

C:\Users\Thom\Documents\Signing\innosign.bat $f <pin> $p

Then for my SignTool command in my Inno Setup script, I used

SignTool=Yubi $qBeacon$q $qhttps://usebeacon.app$q

Those annoying $q symbols are replaced by quotes when executed. Everything after the tool name will be passed instead of the $p parameter, and $f is the file path, so the command that Inno Setup will execute will look something like this:

C:\Users\Thom\Documents\Signing\innosign.bat <file> <pin> "Beacon" "https://usebeacon.app"

And that's it. You should be able to run your Inno Setup script to sign your applications and installers with your Yubikey without having to enter your PIN for every file... or at all.