HodorNV / ALOps

ALOps
59 stars 24 forks source link

Feature Suggestion: Sign App with HSM (Azure Key Vault) #717

Open DanielGoehler opened 9 months ago

DanielGoehler commented 9 months ago

Is your feature request related to a problem? Please describe. It would be nice if ALOps supported signing via HSM (Azure Key Vault), which is supported by GlobalSign and DigiCert [1]. The hardware token signing option also looks promising #614, but is limited to hardware tokens attached to the VM. We currently have 8 different build agent VMs on a VMware cluster with three host machines. I'm not sure this is possible. If we need 8 tokens and the VMs can't be moved to a different host machine.

Describe the solution you'd like As suggested in #614, I will try to get Freddy's AL Go for Github action to work (https://github.com/microsoft/AL-Go/tree/main/Actions/Sign). Perhaps this could also be integrated into ALOpsAppSign.

DanielGoehler commented 9 months ago

We have a GlobalSign Standard Code Signing with HSM (Hardware Secure Module). DigiCert Code Signing with HSM is also available. Both are supported by Azure Key Vault [1]. Note:

We order through our current certificate provider for all certificates, PSW Group, and we had to contact support after ordering so they could get us the HSM option from GlobalSign. Verification as usual. You will also get your private key signed as before, the only difference is that you will create your private key directly in the Azure Key Vault [2]. To meet the FIPS 140-2 Level 2 HSM requirement, you will need to create an Azure Key Vault Premium [3]. I had to sign a document confirming this and also that I have the qualifications in information security and risk management, network security and physical security. Currently the costs for Azure Key Vault Standard and Premium are the same as for our location Germany West Central [4], but HSM-protected keys RSA 4096-bit cost €4.503 per key per month + €0.136/10,000 transactions. (The difference to Standard is only the €4.503 per key per month).

I then created an app registration with a secret [5] and enabled access to the Azure Key Vault [6].

I used Freddy's AL Go for Github action to sign our AppSource apps for now (https://github.com/microsoft/AL-Go/tree/main/Actions/Sign).

Our release pipeline looks now like this:

Release.yaml

[...]
- task: ALOpsAppCompiler@2
  displayName: 'App Compiler'
  inputs:
    artifactversion: '$(artifactversion)'
    artifacttype: '$(artifacttype)'
    artifactcountry: '$(artifactcountry)'
    alsourcepath: '$(System.DefaultWorkingDirectory)/MainApp'
    appversiontemplate: '$(appversionno)'
    appfilenametemplate: '%APP_PUBLISHER%_%APP_NAME%_%APP_VERSION%.app' # Template for App filename.
    alcodeanalyzer: 'AppSourceCop,CodeCop,UICop'
    publishartifact: false

#- task: ALOpsAppSign@1
#  displayName: 'App Sign'
#  inputs:
#    usedocker: false
#    artifact_path: '$(ALOPS_COMPILE_ARTIFACT)'
#    pfx_path: '\\files\DevOps\Common\CodeSigning\CodeSigning.pfx'
#    pfx_password: '$(CodeSigningPasscode)'
#    timestamp_uri: 'http://timestamp.digicert.com/'

- template: templates/SignApp.yml
  parameters:
    artifact_path: '$(ALOPS_COMPILE_ARTIFACT)'
    client_id: '$(CodeSigningClientId)'
    tenant_id: '$(TenantID)'
    client_secret: '$(CodeSigningSecret)'
    vault_name: '$(CodeSigningVaultName)'
    certificate: '$(CodeSigningCertificate)'
    timestamp_uri: 'http://rfc3161timestamp.globalsign.com/advanced'

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(ALOPS_COMPILE_ARTIFACT)'
    ArtifactName: 'Dynamics 365'
[...]

Instead of the ALOpsAppSign task, I use templates/SignApp.yml and PublishPipelineArtifact.

templates\SignApp.yml

parameters:
  artifact_path: ''
  client_id: ''
  tenant_id: ''
  client_secret: ''
  vault_name: ''
  certificate: ''
  file_digest: 'sha256'
  timestamp_uri: 'http://timestamp.digicert.com'

steps:
  - task: PowerShell@2
    displayName: 'Install AzureSignTool'
    inputs:
      targetType: 'inline'
      script: |
        dotnet nuget add source -n nuget.org https://api.nuget.org/v3/index.json
        dotnet tool install AzureSignTool --global --version 4.0.1
      ignoreLASTEXITCODE: true

  - task: PowerShell@2
    displayName: 'Sign App'
    inputs:
      targetType: 'inline'
      script: |
        cd (Join-Path $env:userprofile .dotnet/tools)
        ./AzureSignTool sign --file-digest ${{parameters.file_digest}} `
            --azure-key-vault-url "https://${{parameters.vault_name}}.vault.azure.net/" `
            --azure-key-vault-client-id ${{parameters.client_id}} `
            --azure-key-vault-tenant-id ${{parameters.tenant_id}} `
            --azure-key-vault-client-secret ${{parameters.client_secret}} `
            --azure-key-vault-certificate ${{parameters.certificate}} `
            --timestamp-rfc3161 "${{parameters.timestamp_uri}}" `
            --timestamp-digest ${{parameters.file_digest}} `
           "${{parameters.artifact_path}}"

To get AzureSignTool up and running, I installed the .NET 6.0 SDK on each build agent.

Ether manual

$tempFile = Join-Path $env:TEMP "dotnet-win6.exe"
Invoke-WebRequest -Uri "https://download.visualstudio.microsoft.com/download/pr/9b8baa92-04f4-4b1a-8ccd-aa6bf31592bc/3a25c73326e060e04c119264ba58d0d5/dotnet-sdk-6.0.418-win-x64.exe" `
                      -UseBasicParsing `
                      -OutFile "$tempFile"
Start-Process -Wait -FilePath "$tempFile" -ArgumentList /quiet
Remove-Item "$tempFile" -Confirm:$false -Force:$true

Or via PowerShell Remoting

$Hosts = @("BCBUILD1", "BCBUILD2", "BCBUILD3", "BCBUILD4", "BCBUILD5", "BCBUILD6", "BCBUILD7", "BCBUILD8")
$Hosts | ForEach-Object {; 
    Write-Host $PSItem
    Invoke-Command -ComputerName $PSItem -Authentication NegotiateWithImplicitCredential -ScriptBlock {
      Write-Host $PSItem
      $tempFile = Join-Path $env:TEMP "dotnet-win6.exe"
      Invoke-WebRequest -Uri "https://download.visualstudio.microsoft.com/download/pr/9b8baa92-04f4-4b1a-8ccd-aa6bf31592bc/3a25c73326e060e04c119264ba58d0d5/dotnet-sdk-6.0.418-win-x64.exe" `
                        -UseBasicParsing `
                        -OutFile "$tempFile"
      Start-Process -Wait -FilePath "$tempFile" -ArgumentList /quiet
      Remove-Item "$tempFile" -Confirm:$false -Force:$true
    }    
}

Optimisations would also be possible. If you check to see if the .NET 6.0 SDK is installed, it could be part of the pipeline. dotnet nuget add source -n nuget.org https://api.nuget.org/v3/index.json is only needed once. dotnet tool install AzureSignTool --global --version 4.0.1 is also only needed if %userprofile%.dotnet\tools\AzureSignTool.exe is missing. I haven't looked into why --global doesn't work.

I haven't had this problem with our existing Build Agents, but if you get the error

The file cannot be signed because it is not a recognized file type for signing or it is corrupt.

the SignTool.exe under the hood needs navsip.dll installed on the system. [7]

You can do this by copying the functions from https://github.com/microsoft/AL-Go/blob/main/Actions/Sign/Sign.psm1 to a PowerShell console and running Register-NavSip.

Sign.psm1

function GetNavSipFromArtifacts
(
    [string] $NavSipDestination
)
{
    $artifactTempFolder = Join-Path $([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())

    try {
        Download-Artifacts -artifactUrl (Get-BCArtifactUrl -type Sandbox -country core) -basePath $artifactTempFolder | Out-Null
        Write-Host "Downloaded artifacts to $artifactTempFolder"
        $navsip = Get-ChildItem -Path $artifactTempFolder -Filter "navsip.dll" -Recurse
        Write-Host "Found navsip at $($navsip.FullName)"
        Copy-Item -Path $navsip.FullName -Destination $NavSipDestination -Force | Out-Null
        Write-Host "Copied navsip to $NavSipDestination"
    }
    finally {
        Remove-Item -Path $artifactTempFolder -Recurse -Force
    }
}

function Register-NavSip() {
    $navSipDestination = "C:\Windows\System32"
    $navSipDllPath = Join-Path $navSipDestination "navsip.dll"
    try {
        if (-not (Test-Path $navSipDllPath)) {
            GetNavSipFromArtifacts -NavSipDestination $navSipDllPath
        }

        Write-Host "Unregistering dll $navSipDllPath"
        RegSvr32 /u /s $navSipDllPath
        Write-Host "Registering dll $navSipDllPath"
        RegSvr32 /s $navSipDllPath
    }
    catch {
        Write-Host "Failed to copy navsip to $navSipDestination"
    }

}

Register-NavSip

Update 2024-01-29: Change PublishPipelineArtifact to PublishBuildArtifacts so that it works as before.

waldo1001 commented 9 months ago

thanks so much for this procedure, and taking your time to document it.

Our problem is the lack of certificates to test the procedure.

Though - you very well documented the steps, so we'll try to create the step accordingly. we'll also try to documented the procedure to get to a pfx, since this still seems possible.

DanielGoehler commented 9 months ago

@waldo1001 If you want to try Azure Key Vault signing, you can import your current PFX into an Azure Key Vault Standard. It is software encrypted.

To meet your new CA's HSM requirements (e.g. FIPS 140-2 Level 2), you will need Premium.

DanielGoehler commented 9 months ago

There is also a new blog from Dmitry Katson about Code Signing with AL Go for GitHub and GlobalSign: https://katson.com/code-signing-in-2024/

dawe-sievers commented 1 week ago

Anything new about whether or when this is coming to AL-Ops?

waldo1001 commented 6 days ago

We set this feature to "future". The "problem" is that our provider doesn't give us an HSM, but still a simple PFX... . That process took us 6 months .. . We asked a new one now, so let's see.. .

waldo1001 commented 6 days ago

Question - would you want to test it if we blindly added it?

DanielGoehler commented 6 days ago

Absolutely, I’d be happy to test and provide feedback. Currently, we have the working PowerShell script referenced above.

To test, you can import your self-created .pfx file into the Azure Key Vault. Premium is only needed to meet your CA’s HSM requirements, not for general testing.