9
votes

I'm trying to do CI/CD in Azure DevOps with a ClickOnce application. How can I securely make my code signing certificate available during the build when using a hosted agent?

Note I'm aware you can use a script as suggested at Visual studio team services deploymen/buildt certificate error. However this approach is not secure. The certificate would be loaded into the certificate store of the account the hosted agent is running under. This would allow the agent, and hence other Azure DevOps accounts, to potentially access and use the certificate.

1
Did you ever figure this out Jacob? Would love the solution if possible.SimonGoldstone
I'm in the same situation. I have tried to find a solution but without success. Any updates from you @Jacob?Karl-Petter Åkesson
There are two tasks that allow you to sign your files, go to marketplace and search for "sign". One uses SecureFiles to store your certificate and password, the other expects the certificate to be checked into your repo, and you provide your password.Greg

1 Answers

3
votes

The solution to the issue is to override the built in task SignFile. Interestingly enough the task SignFile uses a built in function in Microsoft.Build.Tasks.Deployment.ManifestUtilities.SecurityUtilities.SignFile which has two overloads, one that takes a thumbprint, and one that takes a file and password.

The solution is then to create a new Task that can reference the other overload. Since we cannot change the calling SignFile we need to maintain the same signature, and place the appropriate variables in the environment variables. In this case "CertificateFile" and "CertificatePassword".

Then reference those two in the overwritten SignFile. What I did was to create a new targets file (filesign.targets) and place the code there. Checked that in to my repository and referenced it from the main project file(s). <Import Project="filesign.targets" />

This way we can also hold our key files in an Azure Key Vault, load them at built and give them a unique password just for that build.

The targets file holds the new FileSign task:

<?xml version="1.0" encoding="Windows-1252"?>
<!--
***********************************************************************************************
Microsoft.VisualStudio.Tools.Office.targets

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
created a backup copy.  Incorrect changes to this file will make it
impossible to load or build your projects from the command-line or the IDE.

This file defines the steps in the standard build process specific for Visual Studio Tools for 
Office projects.

Copyright (C) Microsoft Corporation. All rights reserved.
***********************************************************************************************
-->

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <UsingTask TaskName="SignFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">

    <ParameterGroup>
      <SigningTarget Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem" />
      <CertificateThumbprint ParameterType="System.String" />
      <TargetFrameworkVersion ParameterType="System.String" />
      <TimestampUrl ParameterType="System.String" />
      <CertificateFile ParameterType="System.String" />
      <CertificatePassword ParameterType="System.String" />
    </ParameterGroup>
    <Task>
      <Reference Include="mscorlib" />
      <Reference Include="Microsoft.Build.Tasks.Core" />
      <Using Namespace="System" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
                var EnvCertFile = System.Environment.GetEnvironmentVariable("CertificateFile");

                Log.LogMessage("CertFile:!!" + EnvCertFile);

                if (string.IsNullOrWhiteSpace(CertificateFile) && string.IsNullOrWhiteSpace(EnvCertFile)) {
                    var signFile = new Microsoft.Build.Tasks.SignFile();
                    signFile.CertificateThumbprint = CertificateThumbprint;
                    signFile.SigningTarget = SigningTarget;
                    signFile.TargetFrameworkVersion = TargetFrameworkVersion;
                    signFile.TimestampUrl = TimestampUrl;
                    return signFile.Execute();
                } else {

                    var certificate = string.IsNullOrWhiteSpace(CertificateFile) ? EnvCertFile : CertificateFile;
                    var EnvCertPassword = System.Environment.GetEnvironmentVariable("CertificatePassword");
                    var certificatePassword = string.IsNullOrWhiteSpace(CertificatePassword) ? EnvCertPassword : CertificatePassword;
                    var testString = new System.Security.SecureString();
                    // Use the AppendChar method to add each char value to the secure string.
                    if (!string.IsNullOrWhiteSpace(certificatePassword))
                        foreach (char ch in certificatePassword)
                            testString.AppendChar(ch);
                    Microsoft.Build.Tasks.Deployment.ManifestUtilities.SecurityUtilities.SignFile(certificate, testString,
                        TimestampUrl == null ? null : new Uri(TimestampUrl),
                        SigningTarget.ItemSpec);
                    return true;
                }

]]>
      </Code>
    </Task>
  </UsingTask>
</Project>

Code based on: https://gist.github.com/KirillOsenkov/4cd32c40bffd3045f77e

References: https://github.com/Microsoft/msbuild/blob/fc10ea8ce260b764bb9fa5033b327af9fefcaabe/src/Tasks/ManifestUtil/SecurityUtil.cs https://github.com/Microsoft/msbuild/blob/master/src/Tasks/SignFile.cs