Skip to main content

Signing Binaries with Azure Trusted Signing and a Gitlab Runner

· 7 min read
Quentin Faidide

Windows implemented an antivirus scan which can prevent users from using most binaries by marking them as Trojan, unless they are signed with a cerficate.

It used to be necessary to pay a hefty fee and to send legal documents back and forth in order to get a certificate with a USB dongle to sign binaries. Microsoft since release Azure Trusted Signing, which makes that process a little easier.

This article will document the steps that are necessary to automatically sign your binaries with Azure Trusted Signing in Gitlab CI.

Pre-requisites

  • A Gitlab Project you want to sign binaries off.
  • A CI computer or Virtual Machine running Windows 10 or higher
  • Powershell
  • Dotnet Runtime 6.0 such as you can call the dotnet command from your default CI machine Powershell terminal.
  • Windows SDK 10.0.26100 for Windows 10 or 11 on your CI Machine, such as the following binary can be accesed: C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe.
  • On the CI machine, a Gitlab Runner in Powershell mode running as a service, as described here, and of which we share a sample config file here:
concurrent = 1
check_interval = 0
shutdown_timeout = 0

[session_server]
session_timeout = 1800

[[runners]]
name = "windows-local"
url = "https://gitlab.com"
id = 41966
token = "redacted"
token_obtained_at = 2024-05-16T23:17:40Z
token_expires_at = 0001-01-01T00:00:00Z
executor = "shell"
shell = "powershell"
environment = ["AZURE_TENANT_ID=redacted", "AZURE_CLIENT_ID=redacted", "AZURE_CLIENT_SECRET=redacted"]
[runners.custom_build_dir]
[runners.cache]
MaxUploadedArchiveSize = 0
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]

Mind the environment

You might notice that we have three environement variables here that are redacted, these will be used later to log in to Azure Trusted Signing.

Setting Up Azure Trusted Signing

Existing articles did not cover an error I encountered and that others had online, or the specifics of signing with a Gitlab Runner, but they do cover setting up Azure Trusted Signing. Please refer to this article or this one to setup an Azure Trusted Signing account. We provide additional information below so refer to it if you're having an issue.

After following either of the two tutorials you should have :

  • A Trusted Signing Account resource. Make sure that you have the right Account URI displayed to the top right when opening the resource.
  • An App Registration tied to it that is owned by the tenant you saved the id of (for me the default Directory of my account). To check that the app registration belongs to the right tenant, go to Microsoft Entra Id resource and go to Manage/App Registrations to check that it appears under Owned Applications.
  • A secret for the app registration that you have saved the Value field of.
  • Under the IAM section of the Trusted Signing Account, the role assignment tab should show an Owner (you), a Trusted Signing Certificate Profile Signer mapping to your App Registration (not your user), and a Trusted Signing Identity Verifier under your name.
  • Under the Certificate Profile tab, you should have a certificate profile to your name (the name will be necessary to configure the signtool utility).

To validate your identity:

  • For a business, don't use gmail account or any email on a domain you do not own. This gets silently refused in a loop from my experience.
  • It is tremendously easier to validation with Individual instead of the default Organization identity validation type at the top of the identity validation page.

Make sure to have saved the following variables:

  • AZURE_TENANT_ID: the id of your tenant, that you can find in the Entra ID.
  • AZURE_CLIENT_ID: the application id of your App Registration that you can find in its resource landing page.
  • AZURE_CLIENT_SECRET: the value field of a secret created for the app registration. Note that the secret id is not of any use.
  • Endpoint: The trusted signing account URI
  • CodeSigningAccountName: The name of your Trusted Signing Account
  • CertificateProfileName: The name of a Certificate Profile tied to your Trusted Signing Account.

Downloading the Trusted Signing Client

You need to have the following package. Download it by clicking Download package, rename it as zip, and extract it to your preffered location.

After that, in the extracted folder, right next to the metadata.sample.json file, create the following metadata.json file, replacing the variables with what was previously saved:

{
"Endpoint": "https://weu.codesigning.azure.net/",
"CodeSigningAccountName": "app-signing",
"CertificateProfileName": "QuentinFaidide"
}

After that, make sure to save the following variables:

  • ACS_JSON: The full path to the metadata.json file
  • ACS_DLIB: The full path to this DLL: C:\Users\tester\Microsoft.Trusted.Signing.Client.1.0.60\bin\x64\Azure.CodeSigning.Dlib.dll, that lives in a subfolder of the folder you just extracted.

Trying to sign a binary

I strongly advise trying to sign a binary yourself before moving on to integrating this process in the CI in order to identify any issues.

Let's do the following in powershell:

# we first set the following environement variables
$env:AZURE_TENANT_ID = "the value saved earlier"
$env:AZURE_CLIENT_ID = "the value saved earlier"
$env:AZURE_CLIENT_SECRET = "the value saved earlier"
$ACS_DLIB = "C:\Users\tester\Microsoft.Trusted.Signing.Client.1.0.60\bin\x64\Azure.CodeSigning.Dlib.dll"
$ACS_JSON = "C:\Users\tester\Microsoft.Trusted.Signing.Client.1.0.60\metadata.json"
cd "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64"
.\signtool.exe sign /debug /v /fd SHA256 /tr "http://timestamp.acs.microsoft.com" /td SHA256 /dlib $ACS_DLIB /dmdf $ACS_JSON "C:\Users/tester/my_binary.exe"

If you are given an error, you might want to take a look at the comment section of the melatonin.dev article.

info

Initally, I encountered this error while following the KoalaDSP article:

Error information: “Error: SignerSign() failed.” (-2147024846/0x80070032)

This was due to variable substitution syntax incompatible with Powershell, which created a situation in which the the paths to the ACS_JSON and ACS_DLIB were invalid.

Signing Binaries in Gitlab CI

The first things you might want to ensure are that:

  • You have changed the Gitlab Runner config to insert the AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET variables.
  • Your gitlab runner can be selected with a tag (optional but might be convenient)
  • THe job preceding the one we are interested in produce a binary that it has access to

Now let's move on to the Gitlab stage description. I personally use the following cmake windows YAML anchor to configure my Windows jobs:

.windows-cmake-template: &windows-cmake-config
cache:
- key: "windows-${CI_PROJECT_NAME}-build-5" #increment the number when you wanna clear the cmake cache
paths:
- build
- libs
before_script:
- Import-Module "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
- Enter-VsDevShell -SkipAutomaticLocation -SetDefaultWindowTitle -InstallPath "C:\Program Files\Microsoft Visual Studio\2022\Community\" -DevCmdArguments "-arch=x64 -no_logo"
tags:
- windows-local

This ensures that I start inside the visual studio shell and that the right path are cached for my project. I also make use of a key id for the cache in order to easilly clear the cmake cache, and tag my windows runner so that it's selected.

After that, it's a matter of calling the signtool command from a job stage defined in the stage list:

windows-signing:
stage: release-package
dependencies: []
<<: *windows-cmake-config
script:
- $BASE_BUILD_PATH = $PWD.Path
- $BINARY_PATH = "build\src\MyModule\MyApp\Debug\MyBinary.exe"
- $ACS_DLIB = "C:\Users\tester\Microsoft.Trusted.Signing.Client.1.0.60\bin\x64\Azure.CodeSigning.Dlib.dll"
- $ACS_JSON = "C:\Users\tester\Microsoft.Trusted.Signing.Client.1.0.60\metadata.json"
- cd "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64"
- .\signtool.exe sign /debug /v /fd SHA256 /tr "http://timestamp.acs.microsoft.com" /td SHA256 /dlib $ACS_DLIB /dmdf $ACS_JSON ${BASE_BUILD_PATH}\${BINARY_PATH}
artifacts:
paths:
- build\src\MyModule\MyApp_artefacts\Debug\MyBinary.exe
only: # specific for me to have this job only triggered when a version tag is pushed
refs:
- tags