Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Azure Trusted Signing Revisited with Dotnet Sign


:P
On this page:

Trusted Signing Banner

I wrote about Azure Trusted Signing a while back and talked about how frustrating that process was. Well, since then, I'm happy to say that some additional tooling has shown up that makes this process a lot easier.

The documentation for all of this is still non-existent or, at minimum, non-discoverable, and LLMs absolutely give the wrong advice because of it. The change of terminology in the tools isn't helping either, along with the outdated documentation. Why in the heck would you name something as obscure and non-relevant as artifact signing instead of the obvious trusted-signing that already existed? But alas...

IAC, I've had several LLMs take me down all sorts of crazy paths that seem like they should work but don't. In the end, providing LLMs the actual current command lines available eventually yielded the correct syntax to use (Codex eventually got it right for me).

So, here's the skinny on what works with a heck of a lot less effort than what I described in my last post using SignTool—using dotnet sign instead.

Setting up Trusted Signing

You still need to set up your Azure Trusted Signing, so that part hasn't changed, although by now hopefully the process to set this up might be a little simpler than what I described in my previous post:

Once you have your Trusted Signing account set up and ready to go, what has changed for the better is the process of how you sign your binaries, which is quite a bit easier and removes much of the hunt and peck for various support tools.

Enter dotnet sign

dotnet sign is a dotnet tool that you install via the .NET SDK. You need to have the .NET SDK installed to install dotnet sign, as well as the Azure CLI so you can authenticate your Azure account.

Prerequisites

Install .NET SDK

The .NET SDK is required in order to install the dotnet tool infrastructure required to install and run the dotnet sign tool.

You can install the SDK from here if you don't already have it on your dev machine

Install Azure CLI

The Azure CLI is required so that you can authenticate using Azure's oAuth Authentication flow.

dotnet tool and the .NET SDK

dotnet tool deployed applications use the locally installed .NET SDK to take advantage of the cross platform features of .NET without having to install any additional framework dependencies or having to create separate installers for each platform. A Dotnet tool will run on any of .NET's supported platforms assuming you use cross-platform compatible features only. It does this by automatically creating a platform specific executable for the tool application that dotnet tool install is run on. For this reason it's a convenient way to deploy tools across platforms and if you're in the .NET development eco-system in any way it's likely the .NET SDK is already installed.

You can install the Azure CLI in a number of ways but the easiest is probably via WinGet.

Logging into Azure CLI

Once the Azure CLI is installed you can then log in and set the subscription that your Certificate runs under:

az config set core.enable_broker_on_windows=false
az login
az account set --subscription "Pay-As-You-Go"

I've had lots of issues with the default Windows oAuth authentication using a Web Browser, the first configuration line:

az config set core.enable_broker_on_windows=false

provides more reliable authentication directly in the CLI. For me this was the only way I get it to work correctly. Your mileage may vary and you may not even need this. One thing to look out for is to make sure you choose the right subscription!

Install Dotnet Sign

Dotnet Sign is a dotnet tool installed executable, and in order to install it you need use the dotnet tool command that's part of the .NET SDK.

To install:

dotnet tool install -g --prerelease sign

The tool is currently in pre-release so the --prerelease switch is not optional or the package won't be found. You can also install locally into your project by omitting the -g switch which creates a fixed local copy instead of a global instance installed in the shared tools location.

Using Sign Code Artifact Signing

With all this in place you can now use the Sign command to sign your binaries. Specifically you'll use:

sign code artifact-signing

Note that although there's a trusted-signing option, it's now deprecated and you should use artifact-signing instead.

A full signing command for a binary file looks like this:

sign code artifact-signing  `
   --verbosity warning `
   --timestamp-url http://timestamp.digicert.com `
   --artifact-signing-endpoint https://eus.codesigning.azure.net/ `
   --artifact-signing-account MySigningAccount `
   --artifact-signing-certificate-profile MySignongCertificateProfile `  .\Distribution\MarkdownMonster.exe

You can specify multiple files and they will be batch sent together.

Signing is considerably faster than what I saw with my old SignTool based workflow, with signing times under a second for most files. Based on this speed, it looks like SignTool signs based on locally created hashes rather than uploading the entire file to the server for processing.

Unfortunately, this workflow does not support a metadata file like SignTool supports, so you have to specify all the Azure parameters explicitly. However, we'll fix that with the script provided below.

Putting it all together into a Signing Script

The basic syntax for signing is now simple enough. However, I need signing in a lot of different projects, so I need to reuse the functionality across these various projects and provide the script as part of the local build infrastructure.

The reasons to create a script for all this are:

  • Using metadata configuration files (like SignTool used to use)
  • Handling login optionally
  • Handling parameter errors and signing errors
  • Documentation for prerequisites and configuration in the script

Here's the Powershell script:

# File name: Signfile.ps1
# Prerequisites:  
# dotnet tool install -g --prerelease sign
# Azure CLI required for logging in optionally
# Support metadata: SignfileMetaData.json

param(
    [string]$file = "",
    [string]$file1 = "",
    [string]$file2 = "",
    [string]$file3 = "",
    [string]$file4 = "",
    [string]$file5 = "",
    [string]$file6 = "",
    [string]$file7 = "",
    [string]$file8 = "",
    [boolean]$login = $false
)
if (-not $file) {
    Write-Host "Usage: SignFile.ps1 -file <path to file to sign>"
    exit 1
}

if ($login) {
    az config set core.enable_broker_on_windows=false
    az login
    az account set --subscription "Pay-As-You-Go"
}

# SignfileMetadata.json is not checked in. Format:
# {
#   "Endpoint": "https://eus.codesigning.azure.net/",
#   "CodeSigningAccountName": "MySigningAccount",
#   "CertificateProfileName": "MySigningCertificateProfile"
# }

$metadata = Get-Content -Path "SignfileMetadata.json" -Raw | ConvertFrom-Json
$tsEndpoint = $metadata.Endpoint
$tsAccount = $metadata.CodeSigningAccountName
$tsCertProfile = $metadata.CertificateProfileName
$timeServer = "http://timestamp.digicert.com"

$signArgs = @(
    "--verbosity", "warning",
    "--timestamp-url", $timeServer,
    "--artifact-signing-endpoint", $tsEndpoint,
    "--artifact-signing-account", $tsAccount,
    "--artifact-signing-certificate-profile", $tsCertProfile
)

# Add file arguments at the end
foreach ($f in @($file, $file1, $file2, $file3, $file4, $file5, $file6, $file7, $file8)) {
    if (![string]::IsNullOrWhiteSpace($f)) {
        $signArgs += $f
    }
}

# Run signtool and capture the exit code
sign code artifact-signing $signArgs
$exitCode = $LASTEXITCODE

if ($exitCode -eq 0) {
    Write-Host "File(s) signed successfully." -ForegroundColor Green
    exit 0
} else {
    Write-Host "Signing failed with exit code: $exitCode" -ForegroundColor Red
    exit $exitCode
}

This script uses a separate, external configuration file for the Azure values required for code signing to work. I tend to check in the signing script into Git, while the metadata is locally created and not checked in which separates the two. Alternately you can use some other approach to keep the private data out of your repo - environment variables would also do the trick, but I prefer this explicit approach because it makes it easy to copy the data and script across multiple projects while still keeping the private data out of Git.

Here's what the metadata file looks like:

// SignfileMetadata.json
{
  "Endpoint": "https://eus.codesigning.azure.net/",
  "CodeSigningAccountName": "MySigningAccount",
  "CertificateProfileName": "MyCodeSignCertificateProfile"
}

So now can drop SignFile.ps and SignfileMetadata.json file into a folder and use it for signing.

In my build script I then have something like this to invoke the signing operation after my binaries have been built:

if ($nosign -eq $false) {    
    "Signing binaries..."
    .\signfile-dotnetsign.ps1 -file ".\Distribution\MarkdownMonster.exe" `
                    -file1 ".\Distribution\MarkdownMonsterArm64.exe" `
                    -file2 ".\Distribution\MarkdownMonster.dll" `
                    -file3 ".\Distribution\mm.exe" `
                    -file4 ".\Distribution\mmcli.exe" `
                    -login $false                   

    if ($LASTEXITCODE -ne 0) {
        Write-Host "Signing failed, exiting build script."
        exit $LASTEXITCODE
    }
}

and then again at the very end to sign the final setup distributable:

.\signfile-dotnetsign.ps1 `
	-file ".\Builds\CurrentRelease\MarkdownMonsterSetup.exe" `
    -login $false

The meta data file can be created in the output folder and should not be checked into Git repo. Alternately you could also change the code to use environment variables if that suits your workflow better.

Dotnet Sign is Much Better

I've been using this new workflow for a couple of weeks now, and it has made signing a lot faster than Signtool. It appears that binaries are locally hashed and only the hash data is sent to the server result in much faster processing times. Average processing with Signtool was around 5 seconds per file, it's now down under 1 second per file - a huge improvement in packaging performance.

Microsoft's own documentation now also uses the Digicert timestamp server instead of the Microsoft one that has been failing for me on a regular basis. Using the DigitCert timestamp server I haven't had any signing failures in the last two weeks.

Summary

So all of this is good news, as it's taken away some of the early growing pains of using Azure Trusted Signing and makes it much more usable and predictable to run on the client without having to jump through all sorts of hoops.

It still isn't as simple as it could be, especially if developers are not already using the .NET ecosystem, but if you're doing code signing for Authenticode, you're likely a Windows developer.

The last hurdle that I'd like to get going now is NuGet signing. But I'll leave that for another time... Onwards.

this post created and published with the Markdown Monster Editor

Posted in Windows  Security  WPF  


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2026