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]
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 toManage/App Registrations
to check that it appears underOwned Applications
. - A secret for the app registration that you have saved the
Value
field of. - Under the
IAM
section of theTrusted 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 defaultOrganization
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.
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