Code signing on Windows with Azure Trusted Signing

Trash your overpriced third party certs! Set that clumsy dongle on fire! Pop the bottle of champagne. Code signing apps and plugins on Windows in 2024 is finally (much more) sane and (same as Apple) cheap.

Before April 2024, this service from Microsoft was in Private Preview and named Azure Code Signing (ACS). It’s now named Azure Trusted Signing and is currently in Public Preview.

This article walks you through how I set things up. You should visit Microsoft’s official docs where they do something similar and also check out Koala DSP’s guide.

You’ll need a biz with 3 years of history to use Trusted Signing! Microsoft currently saysTrusted Signing at this time can only onboard Legal Business Entities that have verifiable tax history of three or more years.” I’m a sole proprietor in the EU and was verified. If you don’t have a registered business, or if it’s younger than 3 years, you will have to wait until they launch “personal” validation.

Why is code signing needed on Windows?

Installers throw up an evil blue SmartScreen warnings on Windows by default. This frightens users and makes them think there’s a virus.

That’s bad. Installation is the person’s first experience with your product. Adding friction at the start of that experience sucks. Especially for less technical users.

Love pain? Check out my detailed posts on Windows code signing with third party certs here, or code singing and notarization on macOS here.

How Azure Trusted Signing works

I’ve been in the Trusted Signing private preview since late 2023. I’ve spent an hour chatting to the (very nice!) team one-on-one and have participated in a couple meetings. Here’s the scoop:

Instead of buying an overpriced signing certificate from a third party, you’ll pay $9.99 a month for a signing account. When you make a new installer, you’ll use tools such signtool or the official GitHub Action to sign the installer.

Instead of lasting years, certs are generated daily (with a lifespan of 3 days). That allows for time-precise revocation if there’s any need.

Trusted Signing has been used internally for all of Microsoft’s products for years now. This isn’t a “new” service. See this link for detailed compatibility info.

Getting started: Create an Azure account

Do it here.

Step 2: Create a Subscription

In Azure, you add paid services through creating a “Subscription” record.

This is sort of a clunky and pointless bureaucratic thing, but hey, it’s a pre-req to setting up a code signing account. There’s no extra charge for setting up a “subscription.”

Step 3: Create a “Trusted Signing Account”

Probably easiest just to stick signing in the search bar than it is to wade through hundreds of crazy service names.

Select the subscription you just created, pick an arbitrary name and select a region:

You’ll need to specify the region’s endpoint when signing. You’ll see the url on the main trusted signing account page after creation.

Step 4: Create an “App Registration”

This is a bit bulky and uncessary because it’s Azure, but this step creates API credentials for an arbitrary “Application” to use outside of Azure. In other words, this is how Azure will know it’s you when you go to sign your installers.

Search for App Registrations and create a new one.

Give it a name and keep the defaults.

Note the client ID (1) and the tenant ID (2) for later signing. Locally you will later set these as environment variables AZURE_CLIENT_ID and AZURE_TENANT_ID.

Then add a secret (3), setting the expiry date to 24 months.

Also note the secret value of the created secret. You’ll set this as AZURE_CLIENT_SECRET.

Step 5: Assign “Roles”

Before you are allowed to validate your identity or sign stuff, you’ll need to explicitly add these roles.

This is one of those…. very redundant and annoying sort of steps. Azure could grant the identity roles to the user who created the trusted signing account, but for “security” reasons (i.e. enterprise customers with convoluted access paradigms) it doesn’t.

You’ll need to go through this wizard twice — once for Trusted Signing Identity Verifier. This is so your logged in user needs has permission to go through identity validation.

And once for Trusted Signing Certificate Profile Signer which is a role that your “application user” (that you just created) needs for the actual signing.

In the Trusted Signing Account, click Access Control (IAM) and then Add role assignment

Search for “trusted” to bring up the relevant roles you need to add.

Select the role (yes, the light gray background means selected) and then click through the wizard to add a user.

Again, you’ll do this twice: Assign Trusted Signing Identity Verifier to your logged in user and Trusted Signing Certificate Profile Signer to your “App” client/user you created in the previous step.

The UX on all this is a bit rough. To double check you did it right, go to IAM > Role Assignments and double check the two roles are there:

trusted-signing is my “App” user.

Step 6: Identity Validation

Compared to the old, crusty, third-party identity validations that can take weeks, require phone calls and physical letters, Microsoft’s identity validation is fairly chill.

Microsoft uses an in-house, worldwide identity validation service. They claim they can validate in as little as an hour. For me (in the EU, submitted on a Saturday) it took ~12 hours to get the initial request for additional documents, another ~2 days to get back to me, and then things stalled out a bit because of a misunderstanding (more on this later) taking 10 days in total.

You’ll want to select New Identity > Public

Private means “use a certificate chained to an opt-in trust root that your app users have to manually install” — so, yes, you want Public!

Now fill out the form. Use a DUNS Number if you are a US biz and have one. Otherwise a Tax ID, for example if you are in the EU with a business (like I am).

Make sure you can provide proof of ownership of the domain you are submitting as your Primary Email (in other words, it can’t be @gmail.com or whatever).

Azure form validation sucks. It took me a few tries before pressing Create was possible. Some fields seem to wants numbers only, otherwise it would say things like “This is not a valid tax id.”

When I finally could press Create, I got hit with this great popup, despite having valid primary and secondary email addresses:

The problem was (randomly) that the secondary email address has to be on the same domain as the primary!

Providing identity documents

You’ll probably have to provide additional documents. I got an email 12 hours after I submitted the request. They wanted proof of business and (for some reason I didn’t initially understand) proof of domain name ownership?

Logging back into Azure and navigating to Trusted Signing > Identity Validation, you’ll see the status is Action Required. Clicking on the record brings up the document uploader:

It took 2 days for my documents to be processed. At which point I got another automated request for Domain purchase invoices or registry confirmation records.

This was a bit strange. I provided them with a renewal receipt of my domain melatonin.dev, so I wasn’t sure what else they wanted. I provided them the original purchase receipt and then 2 hours later got another request for the same document.

It felt a bit dystopian as there were no comments from a human as to what the problem was — but I then figured out I was being dumb. They probably wanted proof of ownership of my Primary Email domain (which in my case was different than my marketing domain).

About an hour later, I then got an email validation request on my Primary Email:

Then things stalled out for me for a few more days. Because I had the luxury of being in the private alpha, I pinged the team to ask what’s up (Thanks Meha!). Apparently the internal validation team was confused if I was applying as a business or as an individual. They will have a workflow for non-business individuals, but it’s not live yet. I told them I’m a business, a sole proprietorship, at which point they asked me again for my business license and then approved me. The process took a total of 10 days

Let me know in the comments how long it took for your identity validation, would be nice to know if I’m an outlier.

Interestingly, the identity validation record expires 2 years after the request was made, so keep that in mind!

Step 7: Create a Certificate Profile

The actual certs on Azure Trusted Signing are created and rotated daily. But you’ll need to create a “profile” to access them. Create a Public Trust profile:

Pick a name.

Yes, you got it. You’ll need this name later when signing…

Under Verified CN and O select your verified identity (from the last step).

Step 8: Signing locally

I initially skipped this step and just got things building on GitHub. I usually only create signed builds via CI (I prefer to keep that boundary hygienic, helps with debugging, etc).

To get going locally, follow Microsoft’s docs about how to get started with signtool.exe. They are solid except they conveniently don’t really mention authentication at all. Whups. For that, you’ll probably just want to export AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and AZURE_TENANT_ID as environment variables to get started.

You can also crib Koala DSP’s guide for this part.

I don’t have much to add, except:

  • You aren’t crazy — yes, you need to download a dlib and pass its downloaded location as a command line argument to signtool. Not awkward at all. If you don’t want to use nuget to grab the dlib, you can click “Download Package” in the sidebar, rename the downloaded file to a .zip and bob’s your uncle.
  • You also need to lovingly handcraft some json to feed signtool — because we’re having fun, ya know? Make sure that endpoint url is right, I totally fucked that up at first…
  • signtool credentials give priority to the azure environment variables for “service principles” (aka your application user), but there are many methods including ManagedIdentity. You can also use az login. I recommend just setting AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and AZURE_TENANT_ID as environment variables.
  • If you need to double check what dotnet runtime you have you can do it with dotnet --list-runtimes. I think anything over 6 and you should be happy?
  • If you need a new version of signtool, you can download it from within Visual Studio and it’ll show up in C:\Program Files (x86)\Windows Kits\10\bin.

Step 9: Trusted Signing in CI (GitHub)

Azure publishes a trusted signing action for GitHub Actions which basically scripts inputs to the Powershell integration.

You’ll need 6 pieces of information that we’ll add as GitHub secrets.

The first 3 are the application client info (as in local signing): AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET.

In addition you’ll need the AZURE_ENDPOINT — this the url for the region you selected. You can find this labelled Account URI on the main Trusted Signing Account page in Azure. For me, in the EU, it’s https://weu.codesigning.azure.net/

While you are there, note the name of your trusted signing account. You’ll store that as a secret called AZURE_CODE_SIGNING_NAME.

Lastly, you’ll need the AZURE_CERT_PROFILE_NAME from step 7.

You might notice a lot of the GitHub and API stuff is still called “code signing” and not “trusted signing.” It’s because Azure renamed the service but didn’t want to break existing usage. 🤷‍♂️

In total, you should have 6 GitHub secrets. You could argue some of this stuff doesn’t actually need to actually be a secret (can just be in the workflow yaml) but I have public repositories, so this is nicer.

The entire action will look something like this:

    - name: Azure Trusted Signing
      uses: azure/trusted-signing-action@v0.3.16
      with:
        azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
        azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
        azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
        endpoint: ${{ secrets.AZURE_ENDPOINT }}
        code-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_NAME }}
        certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
        
        # Sign all exes inside the folder
        files-folder: ${{ env.ARTIFACTS_PATH }}
        files-folder-filter: exe
        file-digest: SHA256

This signs all exe files in the named directory. I opened an issue so we can just specify a single filename. Specifying file-digest is unfortunately also required at the moment.

Success looks like this:

Submitting digest for signing...
OperationId 9823489-2398492348-2134234: InProgress
Signing completed with status 'Succeeded' in 2.9607421s
Successfully signed: D:\a\pamplejuce\pamplejuce\Builds\Pamplejuce_artefacts\Release\Pamplejuce Demo-0.0.1-Windows.exe
Number of files successfully Signed: 1
Number of warnings: 0
Number of errors: 0
Trusted Signing completed successfully

Debugging

No certificates were found

The following certificates were considered:
Issued to: localhost
Issued by: localhost
Expires: Fri Apr 25 16:54:32 2025
SHA1 hash: SOMEHASH

After EKU filter, 0 certs were left.
After expiry filter, 0 certs were left.
SignTool Error: No certificates were found that met all the given criteria.

An error like this means the dlib that calls out to Azure wasn’t invoked. There could be a couple reasons for this, double check the following:

  • The path to the dlib — you should be specifying the full path including filename to Azure.CodeSigning.Dlib.dll (note, not Azure.CodeSigning.dll, I made that mistake!)
  • Make sure the x64 and x86 situation is aligned. Both the dll and the signtool executable need to be using the same version.
  • Make sure you are on a recent enough version of signtool. As of April 2024, Microsoft support recommended 10.0.2261.755 or later.

Number of Errors: 1

Surely an award winning error message, we have the following work of art. Note this is with /v and /debug settings on, lol:

Number of files successfully Signed: 0
Number of warnings: 0
Number of errors: 1

I tried everything to resolve this one. In the end, the issue was the path to the thing I was signing was wrong! I was an idiot and trying to sign a folder (that for some reason was called MyPlugin.exe.)

GitHub Action 403s:

Azure.RequestFailedException: Service request failed.
Status: 403 (Forbidden)
...
Error information: "Error: SignerSign() failed." (-2147467259/0x80004005)
SignTool Error: An unexpected internal error has occurred.

This probably means your application user (client id/secret) doesn’t have the Trusted Signing Certificate Profile Signer role, see Step 5.

FAQ

Check out Microsoft’s Trusted Signing FAQ too…

Do I still need to buy a cert and put it into the account?

No. It’s all managed for you, by Azure. You just interact with their API via their tools. No more buying and juggling certs.

Do I keep a cert in “Azure Key Vault” or something?

Nope. Azure Key Vault is a different service, for the old school manual certs.

Can I use AzureSignTool?

Again, no. That’s for the old Azure Key Vault / manual certs.

Is this basically the modern equivalent to signing with an EV cert?

Yes.

How will smartscreen reputation work? Will I get instant reputation?

Your Azure trusted signing account will start off with a base level of reputation. Reputation now belongs to the code signing identity, not individual certs (as the actual certs are rotated daily).

Can I give my devs access to the account?

Yes, there’s full RBAC control. As far as I’m aware there’s no additional charge for additional accounts, etc.

Do I need to pay some sort of Azure “subscription”?

No, an azure subscription is a record/resource you need to setup (see step 2) so that Azure can bill you. It’s just bureaucratic b.s. needed for the big enterprise bois. Just a hoop to jump through, no additional cost.

Why does my business need to be 3 years old?!

Apparently this has something to do with “Code Signing Baseline Requirements.

Microsoft is working on allowing anyone (“personal” or businesses with less years of tax history) to do signing by doing extra identity proofing. They hoped to have it out by the Public Preview. As of April 2024, it’s not ready yet.

Additional Resources


Response

  1. Jaxel Avatar
    Jaxel

    I came here to say, kudos, this covers a lot of territory. Wanted to add for anyone who reaches here:

    – 403s can sometimes be seen when customers have multiple identities on their configuration, so ensure you are using the proper one.
    – Also, for customers outside of an Azure environment there’s a nagging message that might pop up (https://github.com/Azure/azure-sdk-for-net/issues/29471), you can easily avoid it by excluding managed identity credentials on your config:

    {
    “Endpoint”: “”,
    “CodeSigningAccountName”: “”,
    “CertificateProfileName”: “”,
    “ExcludeCredentials”: [
    “ManagedIdentityCredential”
    ]
    }

    Or the appropiate action from the CI actions.

Leave a Reply

Your email address will not be published. Required fields are marked *