Back to Blog
Windows Code Signing Azure CI/CD

Windows Code Signing with Azure Trusted Signing: End-to-End Guide

A complete guide to signing Windows desktop apps with Azure Trusted Signing — covering SmartScreen reputation, the dotnet sign CLI, Tauri signCommand integration, and CI/CD automation with GitHub Actions.

Windows Code Signing with Azure Trusted Signing: End-to-End Guide

This is a deep-dive companion to our cross-platform code signing guide. If you need the full picture including macOS, start there.

Shipping unsigned Windows software is a non-starter. SmartScreen warnings scare users away before they even try your app. Traditional code signing certificates are expensive, require identity verification, and involve managing private keys. Azure Trusted Signing changes the economics entirely — the certificate lives in Azure’s HSM, signing happens via API calls, and you never handle private keys.

We have shipped multiple signed desktop apps using this approach. This guide covers every step, including the pitfalls that cost us hours.

Why Azure Trusted Signing

Traditional code signing requires purchasing a certificate ($200-500/year for OV, $400-700/year for EV), completing identity verification with a certificate authority, storing the private key securely (often on a USB hardware token), and managing certificate renewals.

Azure Trusted Signing costs $9.99/month, handles identity verification through Azure’s existing identity platform, stores keys in a FIPS 140-2 Level 3 HSM, and issues short-lived certificates automatically. The signing itself is fast — under two seconds per file.

The main limitation: SmartScreen reputation starts from zero with a new Trusted Signing certificate, just like any new OV certificate. EV certificates historically received immediate SmartScreen trust, though Microsoft has been moving away from that distinction. In practice, reputation builds within a few days of real user installs.

Prerequisites

You need:

  1. An Azure account with an active subscription
  2. An Azure Trusted Signing account (create in Azure Portal → search “Trusted Signing”)
  3. A certificate profile configured in your Trusted Signing account
  4. An App Registration (service principal) with the Trusted Signing Certificate Profile Signer role
  5. The service principal’s tenant ID, client ID, and client secret

The dotnet sign Tool

The official CLI for signing with Azure Trusted Signing is the dotnet sign tool. As of early 2026, it requires the --prerelease flag:

dotnet tool install -g --prerelease sign

Basic usage:

sign code artifact-signing \
  --timestamp-url http://timestamp.acs.microsoft.com \
  --artifact-signing-endpoint https://eus.codesigning.azure.net/ \
  --artifact-signing-account MySigningAccount \
  --artifact-signing-certificate-profile MyProfile \
  MyApp.exe

Authentication uses the DefaultAzureCredential chain. In CI, set these environment variables:

  • AZURE_TENANT_ID
  • AZURE_CLIENT_ID
  • AZURE_CLIENT_SECRET

Warning: The NuGet package is called sign, not Azure.CodeSigning. The Azure.CodeSigning package was deprecated and removed from NuGet feeds. If you see azure.codesigning is not found in NuGet feeds, you are using the old package name.

Signing Tauri Apps with signCommand

Tauri v2 supports a signCommand configuration that runs a command to sign each binary during the build process. This is the recommended approach because Tauri signs the .exe binary before packaging it into the NSIS installer, then signs the installer itself — both get signed in a single build step.

Here is the complete GitHub Actions workflow for a Tauri app:

- name: Install dotnet sign tool
  shell: pwsh
  run: |
    dotnet tool install -g --prerelease sign
    $toolDir = Join-Path $env:USERPROFILE '.dotnet\tools'
    echo $toolDir >> $env:GITHUB_PATH

- name: Prepare signing config
  shell: pwsh
  env:
    AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
    AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
    AZURE_SIGNING_PROFILE: ${{ secrets.AZURE_SIGNING_PROFILE }}
  run: |
    $signExe = (Get-Command sign).Source -replace '\\', '/'
    $signCmd = "$signExe code artifact-signing --timestamp-url http://timestamp.acs.microsoft.com --artifact-signing-endpoint $env:AZURE_SIGNING_ENDPOINT --artifact-signing-account $env:AZURE_SIGNING_ACCOUNT --artifact-signing-certificate-profile $env:AZURE_SIGNING_PROFILE %1"
    $config = @{ bundle = @{ windows = @{ signCommand = $signCmd } } } | ConvertTo-Json -Depth 3 -Compress
    Set-Content "app/signing-config.json" $config

- name: Build Tauri app (Windows, signed)
  working-directory: app
  env:
    AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
    AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
  run: npx tauri build --target x86_64-pc-windows-msvc --config signing-config.json

The %1 placeholder is replaced by Tauri with the path of each file to sign. The --config flag merges the signing configuration with your tauri.conf.json at build time, so your main config stays clean.

Pitfalls We Encountered

Forward slashes in signCommand paths. Tauri executes signCommand as a raw process, not through a shell. Backslashes in Windows paths cause parsing issues. Converting all paths to forward slashes (-replace '\\', '/') resolves this. Windows accepts forward slashes in file paths.

Resolving the sign tool path. After dotnet tool install -g, the sign executable is in ~/.dotnet/tools/. Even after adding it to GITHUB_PATH, Tauri’s signCommand subprocess may not inherit the updated PATH. Using (Get-Command sign).Source to resolve the absolute path ensures it works.

PowerShell JSON generation. Generating the signCommand JSON config via bash heredocs or echo statements led to quoting nightmares on Windows. Using PowerShell’s ConvertTo-Json produces clean, properly escaped JSON every time.

The deprecated Azure.CodeSigning package. Earlier documentation and blog posts reference dotnet tool install --global Azure.CodeSigning. This package has been removed from NuGet. The correct package is sign with the --prerelease flag.

Timestamp server reliability. The Microsoft timestamp server (http://timestamp.acs.microsoft.com) is generally reliable. If you experience intermittent failures, the DigiCert timestamp server (http://timestamp.digicert.com) is a solid alternative.

Verifying Signatures

After building, verify the signature:

Using PowerShell:

Get-AuthenticodeSignature "KeyQ Tempo_1.0.0_x64-setup.exe" | Format-List

Using Properties dialog: Right-click the .exe → Properties → Digital Signatures tab. You should see your organization name with a valid timestamp.

Both the installer and the installed executable should show digital signatures when using the signCommand approach. If you only use the azure/trusted-signing-action post-build, only the installer is signed — the binary inside it is not.

Cost

Azure Trusted Signing costs $9.99/month for the Basic tier, which includes unlimited signing operations. There are no per-signature fees. Compare this to traditional OV certificates ($200-500/year) or EV certificates ($400-700/year) from providers like DigiCert or Sectigo.

Secrets Reference

Store these as GitHub Actions secrets:

SecretDescription
AZURE_TENANT_IDAzure AD tenant ID
AZURE_CLIENT_IDApp Registration (service principal) client ID
AZURE_CLIENT_SECRETApp Registration client secret
AZURE_SIGNING_ENDPOINTRegional endpoint (e.g., https://eus.codesigning.azure.net/)
AZURE_SIGNING_ACCOUNTTrusted Signing account name
AZURE_SIGNING_PROFILECertificate profile name

Endpoint Regions

Use the endpoint that matches where you created your Trusted Signing account:

RegionEndpoint
East UShttps://eus.codesigning.azure.net
West US 2https://wus2.codesigning.azure.net
North Europehttps://neu.codesigning.azure.net
West Europehttps://weu.codesigning.azure.net

A region mismatch causes a 403 Forbidden error during signing.


Setting up code signing for the first time can be surprisingly frustrating. If you want to skip the trial-and-error, we have done this across multiple apps and can help you get a working pipeline in hours, not days. Get in touch.