2020-01-31

Building strong named assemblies in Azure Pipelines (or solving the MSB3325 error)

If you, like me, are using a certificate to sign your assemblies, making them strong named, you will get in trouble when trying to build the project on another machine (or as in this case, using an azure pipeline). MSBuild will throw an error (MSB3325) that looks like below.

Error MSB3325: Cannot import the following key file: mycertificate.pfx. 
The key file may be password protected. 
To correct this, try to import the certificate again 
or manually install the certificate to the Strong Name CSP 
with the following key container name: VS_KEY_F287A1878DD02205

Problem background

Certificates (in pfx containers) are password protected and for MSBuild to be able to use them, they must be installed in a key container. (MSBuild uses the "Microsoft Strong Cryptographic Provider", MSC)

Normally, when selecting a certificate in Visual Studio, you are prompted for the password and Visual studio creates the container for you. When the container is created, you do not need the password as long as the container exists. If you would like to build the project on another machine you will get the error above since the container is not present.

Manual solutions

If you go to another machine, one simple way to create the container is to deselect and select the certificate again (in visual studio, in the properties/signing window) which will prompt you for the password.
Another way is to use sn.exe and pass the certificate and key container name as parameters

Sn.exe -i mycertificate.pfx VS_KEY_F287A1878DD02205

This command will then prompt you for the password and create the container
https://docs.microsoft.com/en-us/dotnet/framework/tools/sn-exe-strong-name-tool

Running on Azure pipelines

To be able to build this on Azure Pipelines you need to create the container beforehand.
One way is to create a self-hosted agent that you install the container on.
https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/v2-windows?view=azure-devops

If you would like to use a Microsoft-hosted agent, then you must include the creation of the container in the pipeline itself. Using sn.exe seems like a promising way.

Automated solutions

If we would like to automate the process we have two problems that need solving
1. We need to calculate the name of the container
2. We need to pass the password to the sn.exe tool somehow.

The container name is determined by a class in Microsoft.Build.Tasks.v4.0.dll named ResolveKeySource. It will base the name upon the contents of the certificate, plus the user's domain and username that are running the MSBuild command. kdrapel posted an answer regarding this on StackOverflow

The other issue, passing the password to the sn.exe tool seemed a bit harder. There were some examples where piping it to the exe or writing a wrapper around stdin but none of them worked for me.

SnInstallPfx

Thanks to honzajscz, who also had this issue, the GitHub project SnInstallPfx solved both issues in a nice way. Both calculating the container name and allowing us to enter the password ended us up with a simple command line

SnInstallPfx.exe mycertificate.pfx mypassword

Final solution in Azure Pipelines

To include it in the pipeline I used the library function
https://docs.microsoft.com/en-us/azure/devops/pipelines/library/?view=azure-devops

In the library, I uploaded both the certificate and the sninstallpfx.exe tool.
The password I added in a variable group.

In the YML I first added the variable group

variables:
- group: 'My password'

Then the files needed to be copied to the target host using DownloadSecureFile task

- task: DownloadSecureFile@1
  displayName: Download Pfx
  name: myCertificatePfx
  inputs:
    secureFile: MyCertificate.pfx

- task: DownloadSecureFile@1
  displayName: Download sni
  name: snInstallPfx
  inputs:
    secureFile: SnInstallPfx.exe

Then added a PowerShell task where I imported the file paths and password as an environment variable and just invoked the exe

- task: PowerShell@2
  env:
    SN_INSTALL_PFX: $(snInstallPfx.secureFilePath)
    MYCERTIFICATE_PFX: $(myCertificatePfx.secureFilePath)
    MYCERTIFICATE_PFX_PASSWORD: $(myCertificatePfxPassword)
  inputs:
    targetType: 'inline'
    script: '&"$($ENV:SN_INSTALL_PFX)" "$($ENV:MYCERTIFICATE_PFX)" "$($ENV:MYCERTIFICATE_PFX_PASSWORD)"'

After a long research, I was finally able to build the project on a Microsoft hosted pipeline.