Signing files in GitHub Actions

Background

I recently wrote about the changes I had had to make to our Azure DevOps pipelines to address the changes required when code signing with a new DigiCert certificate due to new private key storage requirements for Code Signing certificates

Today I had to do the same for a GitHub Actions pipeline. The process is very similar, but there are a few differences in the syntax and the way the secrets are stored.

The Solution

Step 1: Create a Composite Action

I stored theses steps as a Composite Action for easier reuse, but you could put them within a workflow if you prefer.

The composite action installs the DigiCert tools in the first step, and then finds and signs the files in the second.

Yes, I know I could just have a single step, but I wanted to follow the Azure DevOps flow as closely as possible.

name: 'Sign Code with DigiCert'
description: 'Signs the contents of a folder with DigiCert'
inputs:
  digicert-api-key: 
    description: 'The DigiCert API key'
    required: true
  tools-download-url:
    description: 'The URL for the DigiCert Windows MSI'
    required: false
    default: https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download
  signer_p12_file: 
    description: 'The DigiCert Signer P12 File as base64 encoded string'
    required: true
  cert_crt_file:
    description: 'The DigiCert Certificate CRT File as base64 encoded string'
    required: true
  digicert_host:
    description: 'The DigiCert Host'
    required: false
    default: https://clientauth.one.digicert.com
  keypair_alias:
    description: 'DigiCert certifiate alias'
    required: true
  password:
    description: 'Digicert password'
    required: true
  file-path:
    description: 'Path to scan for files'
    required: true
runs:
  using: "composite"
  steps:
  - name: Install DigiCert Client Tools
    shell: pwsh
    run: | 
      curl -X GET  ${{ inputs.tools-download-url }} -H "x-api-key:${{ inputs.digicert-api-key }} " -o smtools-windows-x64.msi 
      msiexec /i smtools-windows-x64.msi /quiet /qn 

  - name: Code Sign Files
    shell: pwsh
    run: |
      # Define the base path where signtool.exe is located
      $basePath = "C:\Program Files (x86)\Windows Kits\10\bin"
      # Filtering via version and architecture, could use just one of the these, depends on needs
      $preferredVersion = "x64"

      # Get the matching signtool path (pick the last in the list if multiple returned)
      $signtoolPath = (Get-ChildItem -Path $basePath -Recurse -Filter "signtool.exe" -File | Where-Object { $_.FullName -like "*\$preferredVersion\*" })[-1] | Select-Object -ExpandProperty FullName
      write-host "Found signtool at $signtoolPath"
      
      set-content -Path 'signer.p12.base64' -Value '${{ inputs.signer_p12_file }}' 
      certutil -decode -f 'signer.p12.base64' 'DigiCert Signer Certificate_pkcs12.p12'

      set-content -Path 'cert.crt.base64' -Value '${{ inputs.cert_crt_file }}' 
      certutil -decode -f 'cert.crt.base64' 'cert.crt'

      # Find the files that match 
      write-host "Finding files that match'the path ./src/sample/bin/**/**/*.dll"
      Get-ChildItem -Path "${{ inputs.file-path }}" | ForEach-Object {
        $filePath = $_.FullName
        write-host "Signing file $filePath"
        & $signtoolPath sign /v /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 /csp "DigiCert Signing Manager KSP" /kc "${{ inputs.keypair_alias}}" /f "cert.crt" $filePath
      }         
    env:
      SM_HOST: ${{ inputs.digicert_host }} 
      SM_API_KEY: ${{ inputs.digicert-api-key }} 
      SM_CLIENT_CERT_PASSWORD: ${{ inputs.password }} 
      SM_CLIENT_CERT_FILE: DigiCert Signer Certificate_pkcs12.p12
      SM_TLS_SKIP_VERIFY: true

Step 2: Store the Certificates as Secrets

As with Azure DevOps implementation we now have a pair of files, the .CRT certificate and the .P12 signer’s certificate.

GitHub does not have a feature like Azure DevOps Secure Files. So, we have to store the certificates as secrets.

To store these certificates as GitHub Secrets you need to encode the file content into a base64 string and then add it as a secret in your GitHub repository, or Organisation or Enterprise. Use whichever level works best for you, in my case I chose to store them as Organisation secrets.

Here are the steps:

  1. Open Command Prompt or PowerShell.
  2. Use the certutil command to encode the file:
    certutil -encode yourfile.crt yourfile.crt.base64
    
  3. Open the generated yourfile.crt.base64 file in a text editor (e.g., Notepad) and copy its content.
  4. Add the secret to GitHub at the level you chose:
    • Name your secret (e.g. CRT_FILE).
    • Paste the base64-encoded content into the Value field.

Repeat this process for both the .CRT and the .P12 file.

Step 3: Store the other DigiCert settings as secrets and variables

The other DigiCert settings can be stored as a mixture of secrets and variables. Use variables if reading the value in the log is not deemed a security risk.

The secrets:

  • DIGICERT_API_KEY
  • DIGICERT_SIGNER_P12_FILE as base64 encoded string (see above)
  • DIGICERT_CERT_CRT_FILE as base64 encoded string (see above)
  • DIGICERT_CLIENT_CERT_PASSWORD

and as a variable:

  • DIGICERT_KEYPAIR_ALIAS

Step 4: Using the Composite Action in a Workflow

Finally pull it all together in your workflow

 - name: Sign files with Digicert
   uses: blackmarble/Sign-Code-With-DigiCert@v1
   with:
     digicert-api-key: '${{ secrets.DIGICERT_API_KEY }}'
     signer_p12_file: '${{ secrets.DIGICERT_SIGNER_P12_FILE }}'
     cert_crt_file: '${{ secrets.DIGICERT_CERT_CRT_FILE }}'
     keypair_alias: '${{ vars.DIGICERT_KEYPAIR_ALIAS}}'
     password: '${{ secrets.DIGICERT_CLIENT_CERT_PASSWORD }}'
     file-path: './src/Sample/bin/**/**/*.dll'

For the original version of this post see Richard Fennell's personal blog at Signing files in GitHub Actions