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

According to a commenter, new Azure accounts now come with a Subscription record setup, but you’ll have to update it to “pay-as-you-go.”

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. I called it trusted-signing but that’s arbitrary.

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.

You’ll go through this wizard twice.

First, setup the Trusted Signing Identity Verifier. This is so your logged in user gets permission to go through identity validation. This feels a bit silly and redundant, because we’re the admin on our own Azure account? But it’s a necessary step.

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:

Yes, the light gray background means selected!

Select the role and then click through the wizard to add your main Azure user.

Next, you’ll start the wizard over again. You want to add a role assignment for Trusted Signing Certificate Profile Signer. This is a role that we’re adding to the “App Registration” user that you created in Step 4. This is what lets it actually do the signing.

Instead of assigning the role to your main Azure user, you’ll instead do this funny dance of searching for the App Registration user by name! In my case, the App Registration name from Step 4 was trusted-signing. So I typed in trusted to bring up the user:

The UX on all of 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:

Again, trusted-signing is just my poorly named “App Registration” user created in the previous step!

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. This was true for at least one commenter so far!

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 private individuals and open source, 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.

If you end up waiting for more than a few days, one commenter suggests re-submitting identity validation with a different document, for example the EIN (EU tax ID) document instead of a DUNS.

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 do need at least the .NET 6.0 runtime installed. Double check what runtime you have with dotnet --list-runtimes.
  • 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 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 so that Things Just Work.
  • 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.
  • Make sure you are using the 64bit version of signtool and the Dlib and not the 32bit version.

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.

Other silent signtool failures

Unfortunately there can be various silent failures from signtool. Check the following:

  • You have at least .NET 6.0 installed, check with dotnet --list-runtimes.
  • Make sure you are using a 64bit version of signtool unless you are a 32 bit system
  • If you run into anything else, please check out Event Viewer and comment below…

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


Responses

  1. Adam Avatar

    Thank you Sudara for such a comprehensive and easy to follow tutorial.

    I wanted to report that I got verified within about two hours, using my UTR code for my UK limited company.

    With the help of Koala DSP’s signtool steps I am now able to sign Windows installers with no Smartscreen popups! Yay 🙂

    1. sudara Avatar

      Nice, Adam! Thanks for reporting back!

  2. claes Avatar
    claes

    Thanks alot for this, got it running fairly quickly. At one point it failed silently (crash) with info only available in the Event Viewer, but it turned out the .NET 6 runtime was not installed and after that it worked.

    1. sudara Avatar

      Ah good to know re: .NET. I wasn’t sure how “real” that dependency was despite it being mentioned some docs. I’ll update the article.

  3. Matthew Avatar
    Matthew

    After several weeks of frustration and lack of errors visibly being emitted by signtool, it turned out that the 32-bit version of signtool was crashing, and a fault was logged in Event Viewer:

    Faulting application name: signtool.exe, version: 10.0.22621.3233, time stamp: 0xeef8b361
    Faulting module name: msvcrt.dll, version: 7.0.19041.3636, time stamp: 0x7e27562f
    Exception code: 0x40000015
    Fault offset: 0x0003a65b
    Faulting process id: 0x1bec
    Faulting application start time: 0x01daa8d5d610c055
    Faulting application path: C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\signtool.exe
    Faulting module path: C:\WINDOWS\System32\msvcrt.dll

    Changing to the 64-bit version of signtool (and Azure.CodeSigning.Dlib.dll) makes code signing work correctly.

    1. sudara Avatar

      Yikes! Glad you got it working. When you say “lack of errors” did you not get the “No certificates were found” error? It just didn’t output anything? I updated the “Debugging” section to remind people to check 32 vs. 64 bit.

  4. Rich Avatar

    Thanks for this, helped me quite a bit. I got validated within an hour using my DUNS number.

    Couple of things to note:

    if you’re starting with a fresh Azure account you’ll automatically have a subscription, so no need to create one, but you do have to upgrade to pay-as-you-go before anything will be possible.

    It wasn’t so immediately clear to me from these instructions that you need to create the “Trusted Signing Certificate Profile Signer” role using the name of the App Registration from the previous step. Also the Azure UI won’t show you the App Registration as on option to assign the role to without first searching for the name you gave it, that caught me out for a little while.

    So glad this service exists, my traditional certs expired 3 days ago, I was dreading having to go through the process again and even more now that you can’t just get a file a be done with it AND worse the price is just astronomical now! And even better I tested my first successfully signed installer and didn’t see that damned blocked by SmartScreen nonsense!

    1. sudara Avatar

      Thanks for the detail here, Rich! I’ll do my best to clarify those items.

      Also congrats! I also didn’t see SmartScreen on the first sign, was good vibes!

  5. Brian Fritton Avatar

    Thank you very much. Lots of head banging to get this working and your write up helped a lot.

    One thing I encountered not found here I’d recommend adding: On our first Identity Validation attempt, I waited 10 full days with no update past the “In Progress” status. We did that with our DUNS #. I did verify the email as well. Since MS provides no unpaid support, I just submitted another Identity Validation request, this time with our company EIN (tax ID) and that one succeeded within an hour. So… if you don’t get a status past in progress in a day or two, just try another one with a different ID method.

    1. sudara Avatar

      This is great advice, thanks for sharing it. I’ll pop it in the article too.

  6. 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.

    1. sudara Avatar

      Thanks Jaxel!!

Leave a Reply

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