Skip to the article. I want to share some backstory, but this is not a recipe page, so if you just want a guide, click here. This guide is tailored to setups using Electron Builder, but it should also work using the Azure GitHub Actions app, or even by just running it locally on your (Windows) computer.
About ten years ago, I worked as a sysadmin at my research department. Trying to impress people wasn’t difficult, because you can imagine the state of decay that academic IT is usually in. So a few organizational improvements later, I was an indispensable asset to the institute. But then, I got cocky. I thought: “We have so many computers around here, wouldn’t it be great to implement Active Directory to make managing users easier?” And so a journey began that I never fully recovered from. Microsoft Active Directory is a pain in the butt to work with, and it is almost incomprehensible to understand it. So, after many failed attempts, I had to cede and own my failure.
“What does any of this have to do with code signing?” you may ask now. And, in a perfect world, you would be right. Code signing is a completely different task from managing some users at an institution. But we don’t live in a perfect world, and so trying to implement code signing using Microsoft’s Azure service is equally a pain as trying to set up Active Directory. Why? Because, in its infinite wisdom, Microsoft decided to build its Azure computing service entirely on top of the existing Active Directory infrastructure. And because Active Directory is difficult, so is Code Signing. And, if you look around the internet, you will see that you’re not alone if you can’t wrap your head around it. There are many threads, questions, and guides that try to tell you what you have to do, but none of them work completely. There is always something missing.
The main source of confusion appears to be that Microsoft really wants distinctive names for everything, even though, technically, many things are equivalent. Sometimes, things are called “Application ID,” sometimes “Client ID,” and sometimes “Object ID.” And then, you may come across the question of what the difference between a “Managed Account” and an “App Registration” is, and what the difference between an “App Registration” and a “Service Principal” is. It turns out, once you have actually understood how Microsoft thinks, it is straight forward. But you will be clutching your hand and yell.
With this article, I want to spare you the despair that I had to go through, the valley of tears, the days of wrenching my head around documentation over documentation over documentation. I want you to be able to set up code signing without the fuzz. Simple, straight forward, and with fewer headaches than I had. So, no need to memorize the MSDN, no need to install the Azure CLI, no need for anything but an app you would like to code sign.
High-Level Overview
Before we get into the details, a very short high-level overview over what this guide will cover. This guide will tell you everything you need to know to set up Azure Trusted Signing as an alternative to existing code signing alternatives. The reason I wrote this guide is that, after Microsoft’s decision to require code signing certificates to be stored on Hardware Security Modules complying with FIPS standards, code signing became twice as expensive before. But, thankfully, Microsoft offers a relatively cheap way to code signing directly now. It will set you back about $120 each year, which is cheaper than most alternatives, and it at least promises to be less of a pain to maintain. I believe that, if you create an operating system and demand code signing, you should be the one to offer people a way to do so. Apple has done this for years at this point. Microsoft really needed to pull even. And thankfully, they did.
This guide assumes the average case: an individual who wants to do compliant code signing without selling half a liver to a certificate authority. It assumes you have no Azure subscription yet, no interest in using any of the Machine Learning AI fancy-dandy stuff, and you just want to code-sign an application that you develop because that’s what you are required to do if you don’t want to scare your own users. So, it really starts from zero.
These are the required steps:
- Create a Microsoft account
- Sign up for an Azure subscription and register the code signing service
- Verify your identity
- Set up an “app registration” to be able to use the code signing certificate
- Implement this information into Electron Builder (or into whichever other workflow you have)
- Enumerate all the housekeeping steps that will come up from time to time and which you have to keep track of
Now, let’s get into it.
Guide: Code Signing Electron Apps (and other apps) with Azure Trusted Signing on GitHub Actions (or otherwise)
The biggest hurdle to understanding Azure for newbies such as myself and, probably, you, is the confusing naming of Microsoft. I will be using these blockquotes throughout the guide to explain unnecessary double-naming schemes and other traps that Microsoft has laid out for us naïve users.
0. Preparing Documents and Information
You will need quite an amount of info ready at hand. The process will take about an hour if everything goes according to plan.
- A name and email address for a Microsoft Account (must be a person)
- Identity verification documents. For an individual, these are:
- A government-issued photo ID
- A utility bill (I used a credit card bill which also worked) that has the same address (!) printed on it as on your photo ID
- For a business, different requirements exist, and since I don’t have one, I didn’t double-check these. There are rumors that for a business validation, it needs to be in existence for at least three years.
- A Chromium-based browser or Safari, because the partner service Microsoft works with for identity verification doesn’t like Firefox (Microsoft Azure works fine on Firefox; this is only required for the identity verification step)
- A smartphone
- The Microsoft Authenticator app installed on said smartphone (you don’t have to log in)
1. Create a Microsoft Account
First, you’ll need a Microsoft account. I used my personal one, but depending on your use-case, you may want to set up a second one. If you’re a solo-dev or working on an open source project, your personal account should be fine. You can transfer the whole Azure shenanigans to another account later on.
After you have that, log in, navigate to azure.microsoft.com, click “Get Started with Azure,” and, if they still offer this option, “Try Azure for free.” This will give you $200 of free credit. I never used that, because I only needed code signing, and that’s a monthly subscription that you can’t pay in advance with this credit. But hey, it saves you one month of code signing!
Now you should have a subscription. And that was the easiest step. It’s going to go downhill from here. Now, make sure you head over to portal.azure.com and log in there, because that’s where almost everything in the coming steps will happen.
2. Set up Trusted Signing
The next step is immediately to set up trusted signing. I followed this guide, but I repeat the steps here without any of the confusing naming. I recommend you first read this section of the article, and then follow the guide. The guide is pretty accurate; but it wants you to make some decisions without explaining them.
To give you an understanding of what you are about to do. Azure is a collection of hundreds of services, which ranges from Active Directory through credential management systems to AI shenanigans. One of these services is Trusted Signing. Each service that you want to use you have to “register” beforehand. In the process of that, you will need to provide payment information. Free services are already registered (because you won’t need to pay), paid services you have to consciously activate, which you do via this registration.
First, activate Trusted Signing by entering “subscriptions” into the search bar and going into the Subscriptions section. Then, in the left-hand-side navigation, go to “Settings” → “Resource Providers,” filter by starting to type Microsoft.CodeSigning
, click the three dots next to the entry, and register. This will then prompt you to actually select the stuff you need to pay money for: The code signing subscription. For almost all projects, I recommend the basic one for $10 a month. If you need the premium that costs $100, you probably already know that you’ll need it.
You then also need to provide a name for the associated resource group and the actual signing account name. I recommend the following:
-
Resource Group Name
: Choose as a name something you would also name a Linux user group, i.e., based on what the group’s users can do. If you have only a single app, use, e.g.,zettlr_code_signing
, but if you have multiple ones (or plan to), use something more generic, like justcode_signing
. -
Account Name
: Choose as a name something descriptive for just a single certificate. In my case,zettlr
. If you have multiple apps, you probably want multiple accounts, each named for the app they are used to sign. (Note that in the “basic” subscription you can only have a single account; ten in the premium.) Note down this name for the last step!
You will have to make two decisions here that Microsoft does not fully explain. You need to create a resource group and choose an account name. In my notes, I actually wrote down my choices followed by the words “Is this a good choice? I have no idea.” So, to give you an idea: A resource group is basically like a user group on Linux. It is a group whose members can do certain things, in this case: use a resource. Each of the services you subscribe to (again, “resources” in Microsoft-lingo) requires one single resource group. You then assign users to that group instead of to the resource, but a resource basically just hangs off of such a group like an appendage. Why do you need both a resource and a group for that? Go ask Seattle HQ. The second issue is what an “account name” refers to. That is basically a container for one single code signing certificate. If you have, e.g., multiple apps you want to sign with different certificates, you would create different “accounts,” but the average code signing apprentice likely only needs one. My choices made sense, after all, but it was sheer luck because I have only understood the meaning of this after already having set up everything. Note also that calling the trusted signing container “account” is confusing. It is merely a container that holds your certificates. There is also an account that you will create to authenticate for the code signing step. But that “account name” is actually just the name of the “app registration,” which you won’t need, as opposed to the account name for the trusted signing account, which you do need. If you don’t pay attention to these semantic nuances, you’re in for a treat.
3. Enabling Yourself to Verify Yourself
Now a somewhat weird situation (at least for a single developer). Before you are able to verify that you are who you are, you need to grant yourself the role to be able to verify yourself.
Sounds confusing? Well, if you think about it, many large companies use Azure, so it makes sense that only, say, HR should be able to verify a company there, so it makes sense to make it a separate role. For a single developer, however, this may be confusing because there’s only a single person.
To do so, go into your trusted signing account that you just set up, click on “Access control (IAM)” → “Add” → “Add Role Assignment.” In the list that appears, search for the role “Trusted Signing Identity Verifier” and add yourself as a member. This is important, because only actual Microsoft accounts will be able to verify an identity (see below). Click “Review + assign” and you should then be able to proceed to the next step.
4. Going Through Identity Verification
During the identity verification step, you will actually prove to Microsoft that you are who you say you are. This is the step where you’ll need your government-issued ID document, the utility bill, and your smartphone.
Verification runs in two three steps:
- “Request” a verification
- Run through the verification
- Wait until Microsoft has approved your verification
To verify your identity, first navigate to the “Trusted Signing Account” you created in the previous step. Then, in the navigation, under “Objects” click “Identity validations.” Then, select “Individual” (it defaults to “Organization,” which you need if you want to verify an organization), and then click on “New identity” → “Public.”
You can also validate yourself for a private certificate. That is, as far as I understood it, intended to be used for software that is developed internally by companies and distributed internally. It still needs to be code signed to avoid users downloading malware, but it doesn’t have to be a publicly accessible code signing certificate.
Now, you have to provide your complete address and name, because that is what will go into the certificate. Take note of the name you provide, as this will be the CommonName
or CN
of the resulting certificate, which you need in the last step. If you are now worried and don’t want your address everywhere on the internet: Don’t worry. Neither your email nor your street address are required inside the certificate. Only your name is.
It is absolutely important that you enter the very same primary email address that your Microsoft Account uses, because that is the “Entra ID Account Email,” and since Microsoft binds these validation requests to Microsoft accounts, this is absolutely crucial. I, for example, have a pending validation request that I can’t delete in that list, because I didn’t know that, and I chose the wrong email 🙂🔫.
Microsoft-Terminology alert! What the f*** is an “Entra” ID? You don’t know? Don’t worry. There is no such thing as “Entra.” Microsoft made that up because … honestly, I am running out of explanations at this point. Basically, Microsoft has various “things” that can authenticate themselves; including regular Microsoft Accounts, “App Registrations” (will become important later), and “Managed Users” (really irrelevant for our purposes here). They chose to call the section in Azure that you can manage those authentication objects “Entra,” but that’s really it. Technically, “Entra” is the core of the Active Directory part in Azure, and some parts of the documentation still refer to Entra as “AD.” The only vital part of this is the “Tenant ID” which you will only find in the Entra section, and nowhere else. Anyway, the reason you have to provide the same email address as your Microsoft Account is that they need to hand off your identity validation request to some third party; in my case it was “au10tix.” They basically use that email address to correlate your validation to the correct Microsoft account. However, they never explicitly say that, and that’s why one might be tempted to use a more official sounding email-address here (after all, it appears as if the entire form is entirely about the certificate, not you).
Next, click on the just-created identity validation request, and in the top, click on the link to complete your verification. Clicking that link will bring you to another page. This will try to log you in to an account that is associated with the email address you inserted at the previous step, which is why you will know now if you accidentally messed up — if the system is unable to log you in. On this page you can select an identity verifier (at the time of writing, they only have one), and you will see a QR code. That is important later. Follow the instructions on this page.
At this point, you leave Microsoft’s premises and end up on the ones of their service partner. If you’ve already used code signing in the past, this process will be familiar. Scan the required documents (ID front and back, a bill to the same address, finally a selfie). After that has been done, it may take a while for them to check the authenticity of your documents, but then, the website will redirect you. You should now see a notification that they want to store some token in the Microsoft Authenticator app, which you should grant. This is basically a “we verify that the person is who they say.” Now, tap the QR code scanner icon in the Authenticator app on your smartphone, and use that to scan the QR code on the verification page. This will then prompt Microsoft to request to read this identity verification information, which we obviously want to grant.
At this point, the page should say “Verification successful!”
It may now take any time between 20 minutes and 7 business days until the ID verification status also updates in Azure itself. You will receive an email once that happens. For me, it took about 20 minutes. YMMV.
5. Create a Certificate
Are you exhausted yet? I certainly was at this point. Hopefully, you aren’t yet! Because we’re still just at 50 % of the process.
Now you can return back to the guide I linked above to create a certificate. You do so using the “Certificate profiles” tab in the navigation. When creating a certificate, you now have to select the verified information to base the certificate off of. Make sure not to click “Include street address” or postal code if you want this information to remain confidential.
Here it will ask you to select an “endpoint.” Microsoft never really explains what it is or why you need to select it. There is the implication that you choose one that’s geographically close to you, but I don’t think that’s a requirement. Rather, what I believe, is that each of these endpoints actually resolves to a special server that has these FIPS hardware modules installed, so with this selection you choose where your actual code signing certificates will be stored. So, I’d say choose one that sounds sensible. The only important part is that you provide whichever endpoint you chose in the last step.
Then you’re done — you have a certificate! Yay!
A fact on these certificates that I found useful to know, coming from the days when you would actually get your certificate as a file. Microsoft creates certificates that are valid for only 72 hours, and renews them every day. This means that, whenever you sign an app, you likely use a different certificate (except you release two updates in a day). This is why you will have to provide the name of the certificate in the last step, and not an exact file. What will happen on your side is that your workflow will provide the certificate name, which Microsoft will internally resolve to whichever the current certificate is. This means, even the “certificate” you create, is really just a container for, and not an actual certificate.
6. Create an “App Registration” to Access The Certificate
Now to the part that took me the longest to understand. Because Azure is built on top of Active Directory, it is aimed at any type of organization from individual to multinational corporation. Inside the largest corporations, you usually have various types of users, internal applications that communicate with Azure, and many more things. This is why Azure offers a mind-boggling plethora of authentication options.
If you orient yourself at this blog post that has been circulating in the community, it will recommend an app registration. If you look at the official Azure resource, it will recommend a managed user. And then, there are various terms being thrown around from “Service Principal” to “self-signed certificates” to “client secrets.” So let’s explain what all of that is, and then create what you actually need.
This is really the part where Microsoft went bonkers with naming things opaquely. So, you have three “objects” that can be used to authenticate: Microsoft accounts, users, and apps. And then you have — separate from that — authentication methods that you can create for each of them. I am still not completely sure what the difference between a managed user account and an app registration is, but I know the following. Both managed users and app registrations are roughly the same in terms of functionality for code signing. Both can log in using a secret (basically an “Application password,” or what web developers know under the name “Authentication Bearer”). Your Microsoft Account is different, however, because that is bound to an existing physical person. To keep things simple, you will be creating an “App registration.” Microsoft will often talk about apps in a way of you writing an app and then allowing users to log in through your app into Microsoft services. This is certainly a part of the functionality of “App registrations,” but not necessary for our case. Lastly, Microsoft will differentiate between an “App registration” and a “Service principal,” often implying that both can only be set in the Microsoft “Entra ID” portal, but not only are App registration and Service principal the same, but you don’t even have to go to the Entra ID portal to do any of this. In fact, a “service principal,” for Microsoft, is simply an app registration that additionally has an authorization token set. Furthermore, Microsoft documentation will often talk about that you have to create “roles” for the app you just created to assign users to, but, again, this is not necessary for our use case. But since we do have to assign a role to the app (not for), this adds to the confusion. So, in simple terms: You simply need to create an app, add a secret, grant this app the correct role, and then you can start signing. The Microsoft-lingo has caused me an entire week of delay in getting everything set up because it is so convoluted and ill-explained. But now, let’s continue with the guide.
With your newly created certificate ready to go, search for “App registrations” in the search field and go to the corresponding page. Click on “New registration,” give the app some name (I opted for zettlr-code-sign-app
). Note this name down for the paragraph below. Then keep the supported account types setting on “Single tenant,” and ignore the “Redirect URI” setting.
This confused me so much, because every single Microsoft doc on this that I read really insisted that I insert a value into the Redirect URI field, but it turns out that it is not needed for our purposes.
Then, click “Register.”
Now you need to give this app the permission to use the code signing certificate. Head back into your Trusted Signing Account that you created earlier, and click on “Access control (IAM).” Click on “Add” → “Add role assignment.” Start searching for the role named “Trusted Signing Certificate Profile Signer.” This role allows anything that is assigned to it to actually use code signing certificates. Microsoft recommends keeping this separate from the “Trusted Signing Identity Verifier” role that you assigned to yourself above, and I concur.
Click on the corresponding role, which will select it (there is no checkbox or any visual indication that this will select the role). Then, click on Members, select “User, group, or service principal.” This also includes apps, but as outlined earlier, Microsoft decided to sometimes call apps “service principals.” Click on “Select members” and then search for the name of your app registration. This is important, because of reasons unbeknownst to me (and ill-described), the provided selection only shows users. Apps only show up once you actually search for them by name.
Then, save this role assignment. Now your app is capable of accessing the code signing resource.
Nota bene: During this entire ordeal I read that app registrations also need the “Reader” role for the resource, allegedly because they otherwise are not able to access it. In the end it turned out I had a different error in my code, but it worked with my app registration also having this Reader role. I do not believe it to be required, but if you get authentication errors, maybe it is? Let me know what your experience in this is. I would recommend not assigning this role to keep it as simple as possible, I’m just being transparent here.
So, you have an app registration, but you won’t be able to authenticate with this app. Why is this? Well, because you are lacking an authentication token for it! So let’s create one now.
Go back into the App Registrations, then click the app you just created, then in the navigation go to “Manage” → “Certificates & secrets.” Click on “Client secrets” → “New client secret.” Give it a description (a.k.a. “Where will it be used?”), e.g., GitHub Actions Secret
, and select an expiration. This is one of the household chores: Microsoft does not allow you to generate never-expiring application passwords. Save your changes by clicking on “Add.” Now, it is very important that you note down the “Value” of this secret—Azure will never show this to you again, and this is the password that your code signing workflow can use to authenticate as this app and be able to use the signing workflow. Note that you should not copy the “Secret ID”—it only has one, because everything in Azure has an ID.
Now we’re almost done.
7. Set Up the Code Signing Workflow
Now we can finally get to the meat of the thing. First, I will describe the way this works with Electron Builder, and then add some remarks as to how to make this work with other flows (including the GitHub Azure Action).
To configure Electron Builder, you will first need to adapt its configuration. I have this stored as a YAML file, since this separates the builder config from the package.json
, which I prefer. But it works regardless of where your configuration is. In your Electron Builder config, add the following values:
win:
# ... other values under the `win` key ...
azureSignOptions:
# This is equivalent to the CommonName (CN) value of your certificate.
# This is essentially your full name (or of your company) as you
# entered it into the Identity Validation Request form above.
publisherName: "Hendrik Erz"
# This endpoint is the server room where the certificate
# is stored, as explained in Microsoft's guide.
endpoint: "https://neu.codesigning.azure.net/"
# This is the name of the actual certificate profile (not the
# "Trusted Signing Account"!).
certificateProfileName: "zettlr-code-sign-cert"
# This is the "Trusted Signing Account". It is not, as I have
# falsly believed for many days, the account's display name that
# you will use to log in. This is where inconsistent naming will
# lead. So, enter the Trusted Signing Account name, not the name
# of your app registration. From the app registration, you only
# need the client ID (see below).
codeSigningAccountName: "zettlr"
Next, you will have to provide the appropriate environment variables to Electron Builder. So, in your GitHub Actions workflow, add the following environment variables at the corresponding build step:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
These values come from these places:
- The “Tenant ID” is the ID of your Azure Active Directory. This is not the subscription ID of your Azure subscription, but rather the ID of the directory. Remember I mentioned Azure is built on top of AD? The “Tenant” is the AD ID. You can find it in “Microsoft Entra ID” (which, as we have learned, is the new name for Active Directory). This is used so that Azure knows what tree the client (the app you registered) can be found under.
- The “Client ID” is the “Application” ID of the app registration, which you can find in the overview for your app registration. Note that this is NOT the “Object” ID.
- The “Client Secret,” finally, is the password you created in the last step (again, not the secret’s ID, but the value that Microsoft won’t show you again).
At this point, your GitHub Actions CI workflow should run through flawlessly, and you should be able to successfully sign your apps. And it only took 4,000 words (almost 10 pages) to get here!
Now, as promised a few words on how this will work in other workflows, such as Electron Forge, or if you use the Azure Trusted Signing Action on GitHub. First, what we have set up is what Microsoft calls an “Environment Credential” login, that is: we log in by setting some variables that the SignTool will grab from its environment. Opposed to that is if we would pass those values directly to the script, or use a standard username/password login combination.
Electron Forge uses the same approach as Electron Builder, but the setup will be a bit different. (It’s not yet finalized so not in their documentation.) The whole process up until the integration will be exactly the same, only where to put these values is different.
Lastly, if you would like to use the Azure Trusted Signing Action directly (for example because you don’t use Electron, or due to other reasons), you can simply follow their advice and stick with their first example in their README and use the App Registration authentication. You then simply pass the same environment variables to the script, but use a different naming scheme:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
In addition, the Azure Trusted Signing Action takes everything as environment variables, so you don’t have two separate configuration files that you have to touch, as is the case with Electron Builder.
The reason Electron Builder requires you to change configuration in two places is because it also needs to support existing code signing certificate approaches. By setting the
azureSignOptions
property in its configuration, you signal to Electron Builder that it needs to invoke the corresponding module to code sign your app. Then, the environment credentials will be used by that module; Electron Builder just calls the tool, and the tool then looks up the credentials in the environment.
8. Housekeeping
Once your workflow works, there is a bit of housekeeping to take care of.
First, you will have to repeat the identity validation step every year. I believe that, while it is a bit cumbersome, I can see Microsoft’s reasoning here, and unless something bad happens, it should always take about 20 minutes and be fully automated. I assume Azure will send out an email reminder beforehand, but I could be wrong. So set a reminder!
Second, you will have to refresh the client secret variable once in a while. This is as simple as creating a new secret for the app registration and updating your repository secret on GitHub.
Lastly, it has been recommended to set up “budget alerts.” This is really where the hell-hole of late-stage capitalism rears its ugly head, since no cloud provider—not AWS, not Azure—allows you to just cut off anything if you run over a certain budget. And there are plenty of reports of single devs suddenly facing tens of thousands of Dollars in bills because some function ran erroneously in an infinite loop and Azure happily kept counting up the bill. In the basic plan, you have 5,000 code signings free each month, which should be plenty, but you can just overshoot this limit. If you do, each additional signing will cost. And if your CI pipeline has an infinite loop, you absolutely want to know before you have to declare bankruptcy.
Bonus Round
Because it was so fun, here’s a final, simplified (!) flow chart outlining the entire process of setting up Azure Trusted Signing in its entire, beautiful, gore. Note the three “starting points” Microsoft Account, App Registration, and GitHub Actions that all lead to the final, signed application.
flowchart LR
acc([Microsoft Account]) -->|subscribes to| Azure
Azure -->|has one| signAcc[Trusted Signing Account]
acc -->|has Identity Validation Role| signAcc
acc -->|requests| ident[Identity Verification]
signAcc -->|contains| ident
ident -->|creates| cert[Certificate]
app([App Registration]) -->|has Signer Role| signAcc
signAcc -->|contains| cert
app -->|has one| secret[Secret]
gh([GitHub Actions]) -->|uses| secret -->|to access| cert
cert -->|signs| yourApp[Your Application]