In a previous post, I discussed the process of code signing a .NET Core assembly with a digital certificate. In it, I mentioned three methods of utilizing SignTool.exe to perform this operation with a caveat. None of the methods were scalable nor could be easily injected into an automated build pipeline.
In this post, I’ll discuss the process of integrating a code signing process into the Azure DevOps build pipeline. We’ll use the Azure Key Vault to securely store a digital certificate and we will add a PowerShell task to invoke a function to sign the assembly.
It is worth mentioning that I will focus on signing a .NET Core assembly for the brevity and succinctness of the build. However, this build process can easily be tailored to sign any file with an Authenticode digital signature, including C++ executables and even PowerShell scripts.
Azure Key Vault
Azure Key Vault is a technology that allows an organization to securely store secrets in the cloud. You can also use it to store digital certificates, encryption keys, and secrets generated by hardware security modules (HSMs). But the real beauty of it is that Azure Key Vault gives your organization a central and singular location to manage all of these items.
You can manage access to these secrets via roles, access policies, and fine-grained privileges. And with Azure Key Vault’s monitoring capabilities, you can view all activities across all of the secrets stored in the vault.
Azure Key Vault vs the Secure File Library
Azure DevOps provides a Secure Files Library to allow you to store sensitive files in a secure manner. Similar to Key Vault, you can store code signing certificates and encryption keys. With two options for storing secrets, you might question which is the better option. The short answer is – it depends.
I personally prefer storing code signing certificates in Azure Key Vault. If you store a certificate in the Secure Files library, you lose the fine-grained access controls, metrics, and manageability that comes with the vault. Worse yet, there’s no visibility into the details of the certificate from the Secure Files library. You can’t easily answer questions such as, “When does it expire,” or “What is its thumbprint?” Your organization can very easily be left in an embarrassing situation when you release a product signed with an expired certificate.
There are positives to storing the certificate in the Secure Files library, however. For starters, it allows you to consolidate all of the artifacts required for building your application. You can still share the certificate across multiple builds if you need to. Also, the Secure File Library is cheaper (at least by a little), as you don’t incur an additional fee for downloading and using the files in your build.
So the technology that you choose to use should fit in line with your organization’s goals and pocketbook. In this post, I will be using Azure Key Vault.
Create a Key Vault Resource
In order to store a certificate in Azure Key Vault, you’ll need a Key Vault resource. If you don’t have one already, I’ll walk you through the process of creating one.
First, login to Azure. Click the “Create a resource” link in the sidebar. In the “New” blade, type “Key Vault” into the “Search the Marketplace” text box. Select “Key Vault” from the drop down list. The Key Vault blade will appear, as shown below:
Click the Create button. This will open the Create Key Vault blade. Enter a name that uniquely identifies this key vault.
Remember this name. You must set the AzureKeyVault task’s keyVaultName parameter to this value in the build. We will perform this operation in a bit.
Next, select the appropriate subscription and select (and create, if necessary) the resource group that will contain this vault. Also, specify a location. Yours should look similar to the following:
Click the Create button to create the new vault. It will take a few moments for the deployment to complete. Once it completes, you’ll be able to find the newly created vault by clicking the All resources link under the Favorites section in the sidebar. Alternatively, you can also click the “Go to resource” button in the Deployment Succeeded toast notification.
Upload a Certificate to the Key Vault
Now that the Key Vault resource has been created, you can now upload a code signing certificate. The certificate that you upload must contain the private key.
Creating a digital certificate is outside the scope of this post. However, I will mention that the digital certificate that you upload must be a valid code signing certificate. When you view the certificate in the Certificates MMC snap-in, it’s intended purpose must include “Code Signing.” Similarly, when you view the Enhanced Key Usage field of the certificate, it must contain the value “Code Signing.”
Open the Key Vault resource that you created in the previous section. In the Key Vault blade, click on the Certificates menu item in the sidebar.
Click the “Generate/Import” button. This will open the Create a Certificate blade. In the Method of Certificate Creation drop down, select “Import.” Give the certificate a unique name.
Remember this name. The AzureKeyVault task will read the contents of the certificate into a build variable with this name during the build.
Upload your code signing certificate and enter the respective password.
Click the Create button. The certificate will be added to your Key Vault resource.
Connect the Azure DevOps Project to the Azure Subscription
In order to use the Azure Key Vault from your build, you’ll need to first connect your Azure DevOps project to your Azure subscription.
This post assumes that you have already created a project in Azure DevOps and that this project contains the source code of a .NET Core Application. I created a project and uploaded the source code from the simple “Hello World” application created in my previous post.
In Azure DevOps, open your project and navigate to the project settings page. If you’ve never been to the project settings, you may have difficulty finding the link. It’s at the bottom left of the screen:
Under the Pipelines section, click the “Service connections” menu item. Click the “New service connection” drop down and select “Azure Resource Manager.” The Add an Azure Resource Manager Service Connection dialog will appear. Specify a unique connection name, your subscription, and the same resource group as you chose above.
Remember this name. You will set the AzureKeyVault task’s azureSubscription value to this name in the build.
You may also want to check the “Allow all pipelines to use this connection” checkbox. Click the OK button and the Azure will create the connection.
Grant the Azure DevOps Project Access to Key Vault Secrets
Now you’ll need to grant the Azure DevOps project access to your Key Vault’s secrets.
You’ll need to head back to your Azure portal to accomplish this. In Azure, navigate to the Key Vault resource that you created above. Click the “Access policies” menu item. The list of access policies will be listed in the right pane. Currently, you’ll probably only see yourself if you created the vault.
Click the “Add new” button. This will open the Add Access Policy blade. Click the “Select Principal” selector. The Principal blade will appear. In the Search by name or email address text box, enter the name of your Azure DevOps project’s Service Principal Name to the filter down the list.
If you don’t know what your Azure DevOps project’s Service Principal Name is, it is trivial to find. Back in your Azure DevOps portal, visit your project settings page. Then click on the “Service Connections” menu item, select your Azure service connection and then click on the “Manage Service Principal” action. This will open the project’s Service Principal page in Azure. The Service Principal Name is listed under the “Display Name” label.
Click the Select button. In the “Secret Permissions” drop down, check “Get” and “List”. Then click the OK button. Your project will now have access to the certificate.
Create a New Build
Next, we will create a new build and code sign the assembly.
In Azure DevOps open the project and navigate to Pipelines | Builds. Click the “New Pipeline” button. This will open the New Pipeline Wizard.
On the Connect tab, select the location of your source code. My source code is stored alongside my project in an Azure Git Repo. On the Select tab, select the repository containing your source code.
A Trimmed Down Build
On the Review tab, you will be granted the option of creating and modifying the YAML for your build. This is where we will customize the build process to pull the certificate out of Azure Key Vault and sign the assembly.
First, strip down your azure-pipelines.yml file to the following:
steps: - script: dotnet publish -o $(Build.ArtifactStagingDirectory) displayName: 'dotnet publish'
This will perform three operations: restore the dependencies required by the project, build the project, and then copy the assemblies to the artifact staging directory. This is perhaps the most that we can simplify our build file for a .NET Core assembly.
Reading In the Digital Certificate from Azure Key Vault
Next, we want to add a task that will read in the certificate from Azure Key Vault. We can do this by invoking the AzureKeyVault task. You will give this task the name of your service connection and the name of your Key Vault resource. Your azure-pipelines.yml file should look similar to:
steps: # ... - task: [email protected] inputs: azureSubscription: 'twelve21-signdotnetcore-connection' keyVaultName: 'twelve21-key-vault'
When invoked, this task will load the base-64 encoded contents of the certificate into a variable. The variable’s name will be the same as the name you provided for the certificate when you uploaded it into Key Vault above.
Signing the Assembly
Next, we’ll execute a PowerShell script to sign the assembly with the certificate. We’ll do this by decoding the base-64 content of the certificate into a byte array. Then, we’ll instantiate a new X509Certificate2 given the binary contents of the certificate. Finally, we’ll provide this certificate instance to the Set-AuthenticodeSignature function.
The Set-AuthenticodeSignature function is part of the Microsoft.PowerShell.Security module and is not known to work on Linux. Because of this, we’ll have to force the build to run on a Windows VM.
pool: vmImage: windows-2019 steps: # ... - powershell: | $filePath = '$(Build.ArtifactStagingDirectory)\Twelve21.SignDotNetCore.dll'; $base64 = '$(twelve21-sample-certificate)'; $buffer = [System.Convert]::FromBase64String($base64); $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($buffer); Set-AuthenticodeSignature -FilePath $filePath -Certificate $certificate;
Publishing the Artifacts
Finally, we’ll use the PublishBuildArtifacts task to publish the signed assembly and supporting files.
# ... steps: # ... - task: [email protected] inputs: pathtoPublish: '$(Build.ArtifactStagingDirectory)' artifactName: build
You can now download the artifacts from the build’s Summary page. Once you download and expand the zipped contents, you will notice that the target assembly has been signed by the digital certificate stored in the Azure Key Vault.
Here is the entirety of the build script for your convenience:
pool: vmImage: windows-2019 steps: - script: dotnet publish -o $(Build.ArtifactStagingDirectory) displayName: 'dotnet publish' - task: [email protected] inputs: azureSubscription: 'twelve21-signdotnetcore-connection' keyVaultName: 'twelve21-key-vault' - powershell: | $filePath = '$(Build.ArtifactStagingDirectory)\Twelve21.SignDotNetCore.dll'; $base64 = '$(twelve21-sample-certificate)'; $buffer = [System.Convert]::FromBase64String($base64); $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($buffer); Set-AuthenticodeSignature -FilePath $filePath -Certificate $certificate; - task: [email protected] inputs: pathtoPublish: '$(Build.ArtifactStagingDirectory)' artifactName: build
This post showed you how you can code sign a .NET Core assembly as part of an Azure DevOps build pipeline using a code signing certificate stored in Azure Key Vault. It is not an overly complicated task, but there are many steps that must be performed.