首页 > 解决方案 > Azure 构建管道:是否可以使用存储在 Key Vault 中的代码签名证书在 VSBuild 任务中对 MSIX 进行签名?

问题描述

当使用代码签名证书 (*.PFX) 时,我可以在 VSBuild 任务中对 MSIX 文件进行签名,该证书存储为 Build Pipeline 库部分中的安全文件,使用以下设置(为简洁起见截断):

注意:关键是我们如何分配p:PackageCertificateKeyFile参数。

- task: AzureKeyVault@2
  inputs:
    azureSubscription: 'Dev (SomeGuid)'
    KeyVaultName: 'SomeKeyVault'
    SecretsFilter: 'SomeCertPassword'
    RunAsPreJob: false
    
- task: DownloadSecureFile@1
  name: signingCert
  inputs:
    secureFile: 'SomeCertName.pfx'  
    
- task: VSBuild@1
  inputs:
    platform: '$(buildPlatform)'
    solution: '$(solution)'
    configuration: '$(buildConfiguration)'
    msbuildArgs: '
      /p:AppInstallerUri=$(msixInstallUrl)
      /p:AppxBundle=Never 
      /p:AppxBundlePlatforms="$(buildPlatform)" 
      /p:AppxPackageDir="$(Build.ArtifactStagingDirectory)/" 
      /p:AppxPackageSigningEnabled=true
      /p:GenerateAppInstallerFile=true      
      /p:PackageCertificateThumbprint="" 
      /p:PackageCertificateKeyFile="$(signingCert.secureFilePath)"
      /p:PackageCertificatePassword="$(SomeCertPassword)"
      /p:UapAppxPackageBuildMode=SideLoadOnly 
      ' 

但是,作为将代码签名证书存储在管道库的安全文件部分中的替代方法,我想将其存储在 Key Vault 的证书部分中,然后在AzureKeyVault任务中检索它。因此,YAML 看起来像这样(为简洁起见被截断):

- task: AzureKeyVault@2
  inputs:
    azureSubscription: 'Dev (SomeGuid)'
    KeyVaultName: 'SomeKeyVault'
    SecretsFilter: 'SomeCertName,SomeCertPassword'
    RunAsPreJob: false
        
- task: VSBuild@1
  inputs:
    platform: '$(buildPlatform)'
    solution: '$(solution)'
    configuration: '$(buildConfiguration)'
    msbuildArgs: '
      /p:AppInstallerUri=$(msixInstallUrl)
      /p:AppxBundle=Never 
      /p:AppxBundlePlatforms="$(buildPlatform)" 
      /p:AppxPackageDir="$(Build.ArtifactStagingDirectory)/" 
      /p:AppxPackageSigningEnabled=true
      /p:GenerateAppInstallerFile=true      
      /p:PackageCertificateThumbprint="" 
      /p:PackageCertificateKeyFile="$(SomeCertName)"
      /p:PackageCertificatePassword="$(SomeCertPassword)"
      /p:UapAppxPackageBuildMode=SideLoadOnly 
      ' 

想要这样做的原因是因为我cannot find *.appinstaller在尝试运行单独的MSIX Code Signing任务时得到了文件。在同一个构建任务中对包进行签名似乎更简单、更容易。

但是,我收到以下错误:

错误 APPX0104:找不到证书文件“***”。C:\Program 文件 (x86)\Microsoft Visual

据我所知,当从AzureKeyVault任务变量中检索证书文件时,它被存储为字符串而不是文件。但是,MSBuild 任务需要一个文件。我尝试在网上搜索并尝试了一些 Powershell 脚本将导入的AzureKeyVault任务变量从字符串转换为 base64,但我没有运气(请参阅下面的链接以供参考)。我实际上已经尝试了大约 40 种不同的方法来做到这一点,我担心列出它们只会混淆问题。结果,我觉得最好展示我正在尝试做的事情并询问它是否可能,如果是,如何?

标签: azureazure-pipelinescode-signingazure-pipelines-build-taskazure-pipelines-yaml

解决方案


I was able to solve this by installing the MSIX Packaging extension to my Azure subscription, as referenced here: https://docs.microsoft.com/en-us/windows/msix/desktop/msix-packaging-extension?tabs=yaml

This extension is extremely useful because, much like the Azure Sign Tool (AST), which seems to be the only alternative to this method, it handles the converting of the code signing certificate string to base64 and then signing the package. Obviously, there is a lot going on under the hood. You will need to configure your build pipeline to have access to the key vault. However, unlike the AST, this extension is far easier to use because it does not require all of the configurations that the AST requires. This is because the AST handles the task of retrieving the code signing certificate from the key vault whereas in my implementation I just use the Azure Key Vault task to pull the file into my build pipeline, which is also fairly easy to setup and implement. From my perspective, AST is too complicated to be useful.

Below is the full implementation that I use, which also parameterizes the Key Vaults that store the various code signing certificates by our subscriptions/environments.

trigger:
  branches:
    include:
    - refs/heads/main
    - refs/heads/demo
    - refs/heads/develop
# The following is for testing purposes only. For now we disable automatically building for commits to feature branches in develop.
#    - refs/heads/feature*

pool:
  vmImage: 'windows-latest'

# Variable Declaration(s)
# Note: We must use the name/value combination because we are (dynamically) mixing in variable groups.
variables:
- name: buildPlatform
  value: 'x64'
- name: buildConfiguration
  value: 'Release'
- name: major
  value: 1
- name: minor
  value: 0
- name: build
  value: 0
- name: revision
  value: $[counter('rev', 0)]
# Set conditional variable group based off of the trigger branch (see trigger section above).
# Note: If we use SourceBranch then it will append `refs/heads/` to the SourceBranchName.
- ${{ if eq(variables['build.SourceBranch'], 'refs/heads/main') }}:
  - group: PROD
- ${{ if eq(variables['build.SourceBranch'], 'refs/heads/qa') }}:
  - group: QA
- ${{ if eq(variables['build.SourceBranch'], 'refs/heads/develop') }}:
  - group: DEV
# The following is for testing purposes only.
# Note: If you want to run for a feature branch in develop then you must uncomment this line. Otherwise, you will get an error that the pipeline is not valid.
#       "Step AzureKeyVault input ConnectedServiceName references service connection $(azureSubscription) which could not be found."
- ${{ if contains(variables['Build.SourceBranch'], 'refs/heads/feature') }}:
  - group: DEV

steps: 
# Update the AppXManifest file's major, minor, build, and revision parameters with the current values.
# Note: We use the day's build counter to determine the revision number.
#       By rule, this counter is limited to 255 values per day and will start over at 1!
- powershell: |
   [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")
   $path = "SomeAppName.MsixInstaller/Package.appxmanifest";
   $doc = [System.Xml.Linq.XDocument]::Load($path);
   $xName = [System.Xml.Linq.XName]::Get("{http://schemas.microsoft.com/appx/manifest/foundation/windows10}Identity");
   $doc.Root.Element($xName).Attribute("Version").Value = "$(major).$(minor).$(build).$(revision)";
   $doc.Save($path);
  displayName: 'Version the Package Manifest'
  
# Retrieve the code signing certificate from the Key Vault.
# Note: 20211008 - Ignore the errors because the values are correct.
- task: AzureKeyVault@2
  displayName: 'Azure Key Vault: Retrieve Variable(s)'
  inputs:
    azureSubscription: '$(azureSubscription)'
    KeyVaultName: '$(keyVaultName)'
    SecretsFilter: 'SomeCertName'
    RunAsPreJob: false

# Specify the minimum version of NuGet that we want to use to restore the solution's NuGet packages.
- task: NuGetToolInstaller@1
  displayName: 'NuGet: Use v5.11.0'
  inputs:
    versionSpec: 5.11.0
    checkLatest: true

# Run NuGet restore to download the NuGet packages before building the solution.
- task: NuGetCommand@2
  displayName: 'NuGet: Run restore'
  inputs:
    command: 'restore'
    restoreSolution: '**/*.sln'

# Build the MSIX package.
# Note: Set AppxPackageSigningEnabled=false to avoid a build error (i.e., missing thumbprint). We will sign the package in a subsequent step.
- task: VSBuild@1
  displayName: 'VSBuild: Build the MSIX package'
  inputs:
    clean: true
    configuration: '$(buildConfiguration)'
    platform: '$(buildPlatform)'
    restoreNugetPackages: false  #adding this because we're doing an explicit restore above, lets skip the implicit restore.
    solution: '**/*.sln'
    msbuildArgs: '
      /p:AppInstallerUri="$(appServiceUri)"
      /p:AppxPackageDir="$(Build.ArtifactStagingDirectory)/" 
      /p:AppxBundle=Never 
      /p:AppxBundlePlatforms="$(buildPlatform)" 
      /p:UapAppxPackageBuildMode=SideLoadOnly 
      /p:GenerateAppInstallerFile=true
      /p:AppxPackageSigningEnabled=false
      '

# Sign the MSIX package with the code signing certificate from the Key Vault.
- task: MsixSigning@1
  displayName: 'Code Sign MSIX Package'
  inputs:
    package: '$(Build.ArtifactStagingDirectory)\**\*.msix'
    certificateType: 'base64'
    encodedCertificate: '$(DeveloperCodeSigningCertFile)'
    continueOnError: true

# Publish the MSIX package in preparation for deployment. This will be consumed by the Release Pipeline.
- task: PublishBuildArtifacts@1
  displayName: 'Publish ArtifactName: drop'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'
    
# Run tests (if any).
# Note: This tests sources that are found matching the given filter '**\*test*.dll,!**\*TestAdapter.dll,!**\obj\**
- task: VSTest@2
  displayName: 'VSTest: Run Tests (if any)'
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

推荐阅读