electron-builder icon indicating copy to clipboard operation
electron-builder copied to clipboard

June 2023 Code Signing with HSM APIs

Open petervanderwalt opened this issue 1 year ago • 39 comments

Hi

Is electron-builder able to use services like Digicert Keylocker

https://knowledge.digicert.com/generalinformation/new-private-key-storage-requirement-for-standard-code-signing-certificates-november-2022.html says I now need a hardware token (mine or cloud) but as I use Github Actions CI to build - I can't use a USB token so https://knowledge.digicert.com/solution/digicert-keylocker.html sounds more likely

Refer https://github.com/electron/windows-installer/issues/473#issuecomment-1581441658

Would I be able to use https://docs.digicert.com/en/digicert-one/digicert-keylocker/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html

petervanderwalt avatar Jun 07 '23 20:06 petervanderwalt

https://docs.digicert.com/en/digicert-one/digicert-keylocker/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html looks like it might do

petervanderwalt avatar Jun 07 '23 20:06 petervanderwalt

https://docs.digicert.com/en/digicert-one/digicert-keylocker/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html looks like it might do

Did this work for you? I'm in the same boat, and the path forward is not clear to me.

chscott avatar Jun 13 '23 16:06 chscott

I am awaiting our procurement dept - they have to assist with activating Keylocker subscription / reissuing certificate - before I can give it a go.
If you get to work on it before I can, let me know if you managed - otherwise I'll make sure to update this ticket with more details.
Sounds like its going to be painful so would be good to document for others' sake for sure!

petervanderwalt avatar Jun 13 '23 18:06 petervanderwalt

Hey I bought a cert from GlobalSign, I have created it using Key Vault, but now unsure how to connect to the electron builder process to sign the app for the in-built auto updating. Can anyone help me please?

jcharnley avatar Jun 16 '23 10:06 jcharnley

Hey I bought a cert from GlobalSign, I have created it using Key Vault, but now unsure how to connect to the electron builder process to sign the app for the in-built auto updating. Can anyone help me please?

Digicert has a workflow https://docs.digicert.com/en/digicert-one/digicert-keylocker/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html perhaps contact Global sign, give them that link and ask if they have similar?

petervanderwalt avatar Jun 19 '23 07:06 petervanderwalt

I was finally able to get this to work with a lot of trial and error. My scenario isn't exactly the same as @petervanderwalt because we use AppVeyor for CI, but the concepts should be similar, I think.

package.json

This script sets up the build system to perform code signing and is called from packaging script at the appropriate time.

"setup-keylocker": "pwsh -NoProfile -ExecutionPolicy Unrestricted -Command ./script/setup-keylocker.ps1"

setup-keylocker.ps1

This is PowerShell, but it should be easy enough to understand and port to whatever else you may be using.

try {
  $whoami = $MyInvocation.MyCommand

  # Verify that all required environment variables are set
  $required = @(
    'SM_API_KEY',
    'SM_CERTIFICATE_FINGERPRINT',
    'SM_CLIENT_CERT_FILE',
    'SM_CLIENT_CERT_PASSWORD',
    'SM_HOST',
    'SM_TOOLS_URI',
    'SM_INSTALL_DIR',
    'SIGNTOOL_32_BIT',
    'SIGNTOOL_64_BIT'
  )
  foreach ($variable in $required) {
    if (!$(Test-Path "env:$variable")) {
      throw "Unable to sign files because $variable is not set in the environment."
    }
  }

  # Download SM Tools
  Write-Host "[$whoami] Downloading SM Tools..."
  $params = @{
    Method  = 'Get'
    Headers = @{
      'x-api-key' = $env:SM_API_KEY
    }
    Uri     = $env:SM_TOOLS_URI
    OutFile = 'smtools.msi'
  }
  Invoke-WebRequest @params

  # Install SM Tools
  Write-Host "[$whoami] Installing SM Tools..."
  msiexec.exe /i smtools.msi /quiet /qn | Wait-Process

  Write-Host "[$whoami] Verifying SM Tools install..."
  & "${env:SM_INSTALL_DIR}\smctl.exe" healthcheck --all
} catch {
  throw $PSItem
}

electron-builder.yml (snippet)

win:
  target: nsis
  forceCodeSigning: true
  icon: 'app/static/logos/icon-logo.ico'
  requestedExecutionLevel: requireAdministrator
  rfc3161TimeStampServer: 'http://timestamp.digicert.com'
  sign: 'script/customSign.js'
  signDlls: true
  signingHashAlgorithms: ['sha256']

customSign.js

Note here that signtool.exe embeds errors in stdout, so you have to manually check it for errors.

/* eslint-disable no-useless-escape */
'use strict'

exports.default = async function (configuration) {
  const { execSync } = require('child_process')

  const whoami = 'customSign.js'

  if (!process.env.SM_INSTALL_DIR) {
    throw `Unable to sign files because the path to smctl.exe is not set in the environment.`
  }
  if (!process.env.SIGNTOOL_32_BIT || !process.env.SIGNTOOL_64_BIT) {
    throw `Unable to sign files because the path to signtool.exe is not set in the environment.`
  }

  // Common
  const filePath = `"${configuration.path.replace(/\\/g, '/')}"`
  const smctlDir = `"${process.env.SM_INSTALL_DIR}"`
  const signToolVersionDir =
    process.env.SIGNTOOL_64_BIT || process.env.SIGNTOOL_32_BIT
  const signToolDir = `"${signToolVersionDir}"`

  try {
    const signCommand = `./script/sign.ps1`
    const keyPairAlias = `"Key1"`
    const sign = [
      `pwsh`,
      `-NoProfile`,
      `-ExecutionPolicy Unrestricted`,
      `-Command \"$Input | ${signCommand}`,
      `-FilePath '${filePath}'`,
      `-KeyPairAlias '${keyPairAlias}'`,
      `-SmctlDir '${smctlDir}'`,
      `-SignToolDir '${signToolDir}'\"`,
    ]
    const signStdout = execSync(sign.join(' ')).toString()
    if (signStdout.match(/FAILED/)) {
      console.error(
        `[${whoami}] Error detected in ${signCommand}: [${signStdout}]`
      )
      throw `Error detected in ${signCommand}: [${signStdout}]`
    }
  } catch (e) {
    throw `Exception thrown during code signing: ${e.message}`
  }

  // Verify the signature
  try {
    const verifyCommand = `./script/verify.ps1`
    const fingerprint = `"${process.env.SM_CERTIFICATE_FINGERPRINT}"`
    const verify = [
      `pwsh`,
      `-NoProfile`,
      `-ExecutionPolicy Unrestricted`,
      `-Command \"$Input | ${verifyCommand}`,
      `-FilePath '${filePath}'`,
      `-Fingerprint '${fingerprint}'`,
      `-SmctlDir '${smctlDir}'`,
      `-SignToolDir '${signToolDir}'\"`,
    ]
    const verifyStdout = execSync(verify.join(' ')).toString()
    if (verifyStdout.match(/FAILED/)) {
      console.error(
        `[${whoami}] Error detected in ${verifyCommand}: [${verifyStdout}]`
      )
      throw `Error detected in ${verifyCommand}: [${verifyStdout}]`
    }
  } catch (e) {
    throw `Exception thrown during signature verification: ${e.message}`
  }
}

sign.ps1

[OutputType([Void])]
Param(
  [Parameter(Mandatory)]
  [String]
  $FilePath,

  [Parameter(Mandatory)]
  [String]
  $KeyPairAlias,

  [Parameter(Mandatory)]
  [String]
  $SmctlDir,

  [Parameter(Mandatory)]
  [String]
  $SignToolDir
)

# Set the path
$env:Path = @(
  [System.Environment]::GetEnvironmentVariable('Path', 'Machine'),
  [System.Environment]::GetEnvironmentVariable('Path', 'User'),
  $SignToolDir
) -join ';'

# Get the smctl.exe executable
$smctl = "$SmctlDir/smctl.exe"

& "$smctl" sign --input="$FilePath" --keypair-alias="$KeyPairAlias" --verbose

verify.ps1

[OutputType([Void])]
Param(
  [Parameter(Mandatory)]
  [String]
  $FilePath,

  [Parameter(Mandatory)]
  [String]
  $Fingerprint,

  [Parameter(Mandatory)]
  [String]
  $SmctlDir,

  [Parameter(Mandatory)]
  [String]
  $SignToolDir
)

# Set the path
$env:Path = @(
  [System.Environment]::GetEnvironmentVariable('Path', 'Machine'),
  [System.Environment]::GetEnvironmentVariable('Path', 'User'),
  $SignToolDir
) -join ';'

# Get the smctl.exe executable
$smctl = "$SmctlDir/smctl.exe"

& "$smctl" sign verify --input="$FilePath" --fingerprint="$Fingerprint"

chscott avatar Jun 22 '23 11:06 chscott

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.

github-actions[bot] avatar Aug 22 '23 00:08 github-actions[bot]

Still waiting on feedback from @electron-userland team on this, seems the documentation has not been updated to reflect the newer requirements that all CAs are forcing

petervanderwalt avatar Aug 22 '23 07:08 petervanderwalt

I think with Azure Key Vault Premium you can just host your cert and use a URL to point to it with a password. well that is what am hoping, I haven't hooked it up yet. I'll report back

jcharnley avatar Aug 22 '23 09:08 jcharnley

@petervanderwalt can you clarify what needs to be added to the documentation? Happy to accept a PR adding a page to the documentation? Seems it would be very windows-specific and could be inserted here https://github.com/electron-userland/electron-builder/blob/master/docs/code-signing.md

Side note, it seems that github handle/tag doesn't ping me. I'm the only maintainer but not officially on the team list I guess 🤔

mmaietta avatar Aug 23 '23 16:08 mmaietta

Thanks for checking in

Have a read through https://knowledge.digicert.com/generalinformation/new-private-key-storage-requirement-for-standard-code-signing-certificates-november-2022.html - theres a new standard around where one can no longer store your key locally, has to be on a token, or hosted in an online.

Different vendors handles each differently, so its painful, but in essence the https://github.com/electron-userland/electron-builder/blob/master/docs/code-signing.md?plain=1#L14 - password to decrypt the key, that was exported along with the certificates as a pfx, as it used to be, no longer works. In particular for CI toolchains (signing on Github actions for example)

I still haven't completed ours - ordering the Keylocker subscription is still stuck with our procurement - so have not gone down the rabbit hole myself yet - but just overall the world changed and theres a new way to do it now and its overly complex (:

petervanderwalt avatar Aug 23 '23 18:08 petervanderwalt

We're with the same issue, but we actually use Squirrel to updates and it also used to work with PFX files.

Any release will be launch to help us building the project easier?

HenriqueOtsuka avatar Aug 28 '23 18:08 HenriqueOtsuka

I got this working on CircleCI with electron-builder with the following changes, where I use a Windows executor with the bash.exe shell as my workflow shell.

Added to .circleci/config.yml:

      # Need to setup the signing tools from DigiCert to allow us to access our certificate
      # See https://docs.digicert.com/nl/digicert-keylocker/ci-cd-integrations/script-integrations/circleci-integration-ksp.html
      # for more info.
      - run:
          name: Setup signing tools
          shell: powershell.exe
          command: |
            cd C:\
            curl.exe -X GET  https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
            msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
            New-Item C:\Certificate.p12.b64
            Set-Content -Path C:\Certificate.p12.b64 -Value $env:SM_CLIENT_CERT_FILE_B64
            certutil -decode Certificate.p12.b64 Certificate.p12
      - run:
          name: Set bash path for signing tools
          # first export is for KSP stuff for DigiCert
          # the second export is for signtool.exe that smctl internally calls
          command: |
            echo 'export PATH=/c/Program\ Files/DigiCert/DigiCert\ One\ Signing\ Manager\ Tools:$PATH' >> $BASH_ENV
            echo 'export PATH=/c/Program\ Files\ \(x86\)/Windows\ Kits/10/App\ Certification\ Kit:$PATH' >> $BASH_ENV
      - run:
          name: Sync certificates
          command: |
            sync_output=$(smksp_cert_sync)
            echo ${sync_output}
            if [[ ${sync_output} != *"${KEYPAIR_ALIAS}"* ]]; then
              echo 'Could not sync certificate matching $KEYPAIR_ALIAS env var'
              exit 1
            fi

My winSign.js script (with sign key in my package.json under win pointing at this file):

// Custom Sign hook for use with electron-builder + DigiCert KSP
// Adapted from https://docs.digicert.com/nl/digicert-keylocker/signing-tools/sign-authenticode-with-electron-builder-using-ksp-integration.html

const { execSync } = require('child_process');

exports.default = async (config) => {
  const keypairAlias = process.env.KEYPAIR_ALIAS;
  const path = config.path ? String(config.path) : '';

  if (process.platform !== 'win32' || !keypairAlias || !path) {
    return;
  }

  const output = execSync(
    `smctl sign --keypair-alias=${keypairAlias} --input="${path}" --verbose`,
  )
    .toString()
    .trim();

  if (!output.includes('Done Adding Additional Store')) {
    throw new Error(`Failed to sign executable: ${output}`);
  }
};

My SMCLIENT_CERT_FILE variable was /c/Certificate.p12 and I had an additional env var KEYPAIR_ALIAS to match what I'd sync with smksp_cert_sync.

I did make CSC_LINK the same value as SMCLIENT_CERT_FILE (with password env var also the same) as that was necessary to trigger my custom script, but I don't think the values are actually read/used so they could probably be anything.

MasterOdin avatar Sep 13 '23 21:09 MasterOdin

We use Azure Key Vault to store our customer's keys on an HSM. We have used code signing certs from both Digicert and GlobalSign, they both work perfectly on Azure Key Vault.

The process is a little complicated because you need to generate a CSR on Azure Key Vault and then use that during the provisioning step on Digicert/GlobalSign dashboard. But once it's set up, it works perfectly. I would strongly recommend, it's what we use for storing our customers cert with our Electron service.

davej avatar Sep 20 '23 13:09 davej

I've got AzureSignTool working from Actions i think am on the last error and will setup to publish straight to GH releases..

jcharnley avatar Sep 20 '23 13:09 jcharnley

Ok I got the customSign file to run using AzureSignTool via GitHub actions. I publish the releases to S3 and the application is notified there is an updated version available

One question tho, how do you ignore the customSign.js and process while packaging locally ?

jcharnley avatar Oct 06 '23 12:10 jcharnley

We have our customSign.js script read in values from process.env to determine whether or not to sign the app. This was a similar strategy as we used back when we had a similar script to run @electron/notarize for macos builds.

MasterOdin avatar Oct 06 '23 13:10 MasterOdin

awesome thanks ill do that too

jcharnley avatar Oct 06 '23 13:10 jcharnley

Document https://docs.digicert.com/en/digicert-keylocker/ci-cd-integrations/script-integrations/github-integration-ksp.html really helps.

Here's how is our application signed by KeyLocker: https://github.com/nervosnetwork/neuron/pull/2913

There are mainly two steps:

  1. Setup signing runtime: https://github.com/nervosnetwork/neuron/pull/2913/files#diff-170ebc8e4dc40acf23cbe0ecce5f3e2aef1652511f59860db704106b197e1d52R54-R85
  2. Sign application: https://github.com/nervosnetwork/neuron/pull/2913/files#diff-f1a2ada293a9fd7da045908348b61a30018539ff94b2cf54461bd122f03736ccR13-R15

Keith-CY avatar Oct 26 '23 18:10 Keith-CY

I got this working on CircleCI with electron-builder with the following changes, where I use a Windows executor with the bash.exe shell as my workflow shell.

Added to .circleci/config.yml:

      # Need to setup the signing tools from DigiCert to allow us to access our certificate
      # See https://docs.digicert.com/nl/digicert-keylocker/ci-cd-integrations/script-integrations/circleci-integration-ksp.html
      # for more info.
      - run:
          name: Setup signing tools
          shell: powershell.exe
          command: |
            cd C:\
            curl.exe -X GET  https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
            msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
            New-Item C:\Certificate.p12.b64
            Set-Content -Path C:\Certificate.p12.b64 -Value $env:SM_CLIENT_CERT_FILE_B64
            certutil -decode Certificate.p12.b64 Certificate.p12
      - run:
          name: Set bash path for signing tools
          # first export is for KSP stuff for DigiCert
          # the second export is for signtool.exe that smctl internally calls
          command: |
            echo 'export PATH=/c/Program\ Files/DigiCert/DigiCert\ One\ Signing\ Manager\ Tools:$PATH' >> $BASH_ENV
            echo 'export PATH=/c/Program\ Files\ \(x86\)/Windows\ Kits/10/App\ Certification\ Kit:$PATH' >> $BASH_ENV
      - run:
          name: Sync certificates
          command: |
            sync_output=$(smksp_cert_sync)
            echo ${sync_output}
            if [[ ${sync_output} != *"${KEYPAIR_ALIAS}"* ]]; then
              echo 'Could not sync certificate matching $KEYPAIR_ALIAS env var'
              exit 1
            fi

My winSign.js script (with sign key in my package.json under win pointing at this file):

// Custom Sign hook for use with electron-builder + DigiCert KSP
// Adapted from https://docs.digicert.com/nl/digicert-keylocker/signing-tools/sign-authenticode-with-electron-builder-using-ksp-integration.html

const { execSync } = require('child_process');

exports.default = async (config) => {
  const keypairAlias = process.env.KEYPAIR_ALIAS;
  const path = config.path ? String(config.path) : '';

  if (process.platform !== 'win32' || !keypairAlias || !path) {
    return;
  }

  const output = execSync(
    `smctl sign --keypair-alias=${keypairAlias} --input="${path}" --verbose`,
  )
    .toString()
    .trim();

  if (!output.includes('Done Adding Additional Store')) {
    throw new Error(`Failed to sign executable: ${output}`);
  }
};

My SMCLIENT_CERT_FILE variable was /c/Certificate.p12 and I had an additional env var KEYPAIR_ALIAS to match what I'd sync with smksp_cert_sync.

I did make CSC_LINK the same value as SMCLIENT_CERT_FILE (with password env var also the same) as that was necessary to trigger my custom script, but I don't think the values are actually read/used so they could probably be anything.

@MasterOdin Are you using same windows executor for preparing build ? because I followed the same step. used executor: win/default but build not generated in dist. any thoughts please ?

RamK777-stack avatar Dec 29 '23 15:12 RamK777-stack

@RamK777-stack yeah, we use win/default:

version: 2.1

orbs:
  win: circleci/[email protected]

jobs:
  win:
    executor:
      name: win/default
      shell: bash.exe

and our build config has this:

    "win": {
      "publisherName": [
        "PopSQL, Inc."
      ],
      "icon": "resources/icon.ico",
      "sign": "./build/winSign.js",
      "target": {
        "target": "nsis",
        "arch": [
          "x64",
          "ia32"
        ]
      }
    },
    "nsis": {
      "artifactName": "${productName}-Setup-${version}.${ext}"
    },

Where the JS script I posted above is ./build/winSign.js and the steps are done as part our job before we do yarn prepackage && yarn electron-builder build --win.

MasterOdin avatar Dec 29 '23 15:12 MasterOdin

@MasterOdin Thanks for your helpful response!. yarn prepackage && yarn electron-builder build --win this command also run through executor: name: win/default right ?

this is my code 

` - run:
      name: build
      shell: bash.exe
      command: yarn electron-pack-win-publish`
      
      But .exe and latest.yml is not generated in dist folder.

RamK777-stack avatar Dec 29 '23 16:12 RamK777-stack

Yes, that's run on that same win/default executor job.

MasterOdin avatar Dec 29 '23 17:12 MasterOdin

Thanks @MasterOdin

RamK777-stack avatar Jan 01 '24 02:01 RamK777-stack

I managed to sign my Electron app for Windows under Linux with the following in the most cost-effective way possible:

  • A GlobalSign EV signing certificate (HSM-type)
    • You can use alternative services too; you do NOT need something like DigiCert KeyLocker, which is expensive as heck and limits how many signatures you can do for the duration of your subscription
  • Azure Key Vault (This would be akin to DigiCert's KeyLocker. It's just the normal key vault, NOT the managed HSM version; you do NOT need a managed HSM pool either)
  • jsign (so Windows wouldn't be required to sign)

This would be the most cost-effective way to sign applications without a limit (excluding the EV cert cost, it'd be around < $5-10 a month for signing an app 10k times using Azure Key Vault).

I mainly used these articles to figure it out:

Creating a certificate:

  • https://signmycode.com/resources/how-to-create-private-keys-csr-and-import-code-signing-certificate-in-azure-keyvault-hsm
    • When creating the key vault, you want the premium pricing tier (which gives you access to RSA-HSM key storage).
    • When creating a certificate in the key vault, you want to use Certificate issued by a non-integrated CA (GlobalSign does have direct integration with Azure Key Vault, but it's only for generating TLS certs, not code signing ones)
    • The subject line looks like this (copy and paste into the input box directly after replacing the values):
      • DC=<domain name>,CN=<domain name>,OU="<Company name>",O="<Company name>",L=<Company city>,ST=<Company 2-letter state>,C=<2-letter country code>
    • Under Advanced Policy Configuration, the following needs to be added:
      • Add 1.3.6.1.5.5.7.3.3 to the EKU list (this enables the cert as a signing cert)
      • Exportable private key: No (this exposes the RSA-HSM option)
      • Select the RSA-HSM option
      • You do not need to define the Certificate Type at all.
    • Follow the article instructions to generate the CSR, which you would use with GlobalSign (or any alternative cert provider)
    • Once you get the certificate from GlobalSign, you'll perform the merge request as stated in the article with it in the key vault

Using the Azure Key Vault API (follow it to get an access token for use with jsign)

  • https://www.c-sharpcorner.com/article/how-to-access-azure-key-vault-secrets-through-rest-api-using-postman/

  • Follow the article instructions to get the access token for use with the Key Vault API.

  • For the Azure Key Vault access policy, you only need the following items checked:

    • Cryptographic Operations: Verify, Sign
    • Certificate Management Operations: Get
  • Build your Electron Windows application

  • Once you have jsign installed, you can use the following command to sign the app

jsign --storetype AZUREKEYVAULT \
       --keystore <name of the key vault> \
       --storepass <access token> \
       --tsaurl http://timestamp.digicert.com \
       --replace \
       --alias <certificate name from Certificates> "<your electron application>.exe"

(--tsaurl is a rfc3161 timestamp server, and digitcert's is free to use)

You can get the access token via:

WINDOWS_SIGNING_ACCESS_TOKEN=$(curl -X POST "https://login.microsoftonline.com/${WINDOWS_SIGNING_TENANT_ID}/oauth2/v2.0/token" \
   -H "Content-Type: application/x-www-form-urlencoded" \
   -H "Accept: application/json" \
   -d "grant_type=client_credentials&client_id=${WINDOWS_SIGNING_CLIENT_ID}&client_secret=${WINDOWS_SIGNING_CLIENT_SECRET}&scope=https://vault.azure.net/.default") || { echo "Curl command failed"; exit 1; }
WINDOWS_SIGNING_ACCESS_TOKEN=$(echo "$WINDOWS_SIGNING_ACCESS_TOKEN" | jq -r '.access_token') || { echo "jq command failed"; exit 1; }

If successful, you'll get the message: Adding Authenticode Signature to <application>.exe

For verifying the signature on the application in Linux, I used osslsigncode which is avail in Ubuntu via apt.

osslsigncode verify -in <file name>

A thing to note: You may get a PCKS7 error when running the check. PCKS7 is different than MS's Authenticode cert / check, so you can ignore the error related to it. As long as it says Number of verified signatures: 1, then you're good to go. I also did check my signed binary using the Windows SDK signtool in Windows to make sure that the signature was valid, and it was.

signtool verify /pa <exe file>

image

Hope that helps!

Note:

You'll want to also sign the application binary itself if you're using electron-builder as it will only sign the installer.

You can do this using an afterSign script:

module.exports = async (context) => {
  const { appOutDir } = context;
  const appName = context.packager.appInfo.productFilename;
  
  // call jsign using "path.join(appOutDir, `${appName}.exe`)"
}

Note: If you use the electron-builder auto updater, if you're signing the installer after the electron-builder process, then you need to update the latest.yml file to a new hash. You can use this link as the basis for the hashing, then update all the sha values in latest.yml accordingly:

https://stackoverflow.com/questions/46407362/checksum-mismatch-after-code-sign-electron-builder-updater

theogravity avatar Jan 25 '24 22:01 theogravity

our Electron service

@jcharnley We are trying to sign our windows electron app the same way. We already have EV certificate hosted in Azure key vault as HSM. Can you kindly provide the steps you used to sign the app using azuresigntool sign electron builder in Github action. stuck at this for some time.

Any help would be greatfull

Thanks.

darkangel081195 avatar Jan 31 '24 06:01 darkangel081195

my YML file. think u will need the Install azuresigntool and azure sign in and dotnet

  "win": {
    "target": "nsis",
    "icon": "relative\\path\\to\\app_icon.ico",
    "signingHashAlgorithms": ["sha256"],
    "certificateSubjectName": "https://domainofCert",
    "sign": "customSign.js"
  },
name: build and publish windows desktop dev

on:
  push:
    branches:
      - develop

jobs:
  publish:
    # To enable auto publishing to github, update your electron publisher
    # config in package.json > "build" and remove the conditional below

    runs-on: ${{ matrix.os }}
    # if: github.ref == 'refs/heads/develop'
    strategy:
      matrix:
        os: [windows-latest]

    steps:
      - name: Checkout git repo
        uses: actions/checkout@v3

      - name: Install dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '6.0.x'

      - name: Install azuresigntool
        run: 'dotnet tool install --global AzureSignTool'

      - name: Install Node and NPM
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm

      - name: Install
        run: |
          npm install
      - name: Azure Sign in
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - name: Publish releases
        env:
          # These values are used for auto updates signing
          KEYVAULT_AUTH: '${{secrets.AZURE_CREDENTIALS}}'
          # This is used for uploading release assets to github
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # This is used for uploading release assets to s3 bucket that is used for auto updates
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          npx nx run shell-desktop:make:production --publishPolicy=always

this is my customSign.js file. you can tell electron-builder to use this file instead of the default thing they do

const cp = require('child_process');

function isEmpty(value) {
  return !value || !value.length;
}

// uses configuration.path to sign the file, passes in the keyvault auth as an env variable
exports.default = async function (configuration) {
  const timeserver = 'http://timestamp.digicert.com';
  const azureURL = 'https://azureURLofTheCert/';
  const certificateName = 'nameofcert';
  const authRaw = process.env.KEYVAULT_AUTH;
  const keyVault = JSON.parse(authRaw);

  if (isEmpty(configuration.path)) {
    throw new Error('Path to file is required');
  }
  // AzureSignTool command with all the required parameters
  const command = [
    'azuresigntool.exe sign -fd sha384',
    '-kvu',
    azureURL,
    '-kvi',
    keyVault.clientId,
    '-kvt',
    keyVault.tenantId,
    '-kvs',
    keyVault.clientSecret,
    '-kvc',
    certificateName,
    '-tr',
    timeserver,
    '-td',
    'sha384',
    '-v',
  ];

  // throws an error if non-0 exit code, that's what we want.
  cp.execSync(`${command.join(' ')} "${configuration.path}"`, {
    stdio: 'inherit',
  });
};

jcharnley avatar Jan 31 '24 11:01 jcharnley

my YML file. think u will need the Install azuresigntool and azure sign in and dotnet

  "win": {
    "target": "nsis",
    "icon": "relative\\path\\to\\app_icon.ico",
    "signingHashAlgorithms": ["sha256"],
    "certificateSubjectName": "https://domainofCert",
    "sign": "customSign.js"
  },
name: build and publish windows desktop dev

on:
  push:
    branches:
      - develop

jobs:
  publish:
    # To enable auto publishing to github, update your electron publisher
    # config in package.json > "build" and remove the conditional below

    runs-on: ${{ matrix.os }}
    # if: github.ref == 'refs/heads/develop'
    strategy:
      matrix:
        os: [windows-latest]

    steps:
      - name: Checkout git repo
        uses: actions/checkout@v3

      - name: Install dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '6.0.x'

      - name: Install azuresigntool
        run: 'dotnet tool install --global AzureSignTool'

      - name: Install Node and NPM
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm

      - name: Install
        run: |
          npm install
      - name: Azure Sign in
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      - name: Publish releases
        env:
          # These values are used for auto updates signing
          KEYVAULT_AUTH: '${{secrets.AZURE_CREDENTIALS}}'
          # This is used for uploading release assets to github
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # This is used for uploading release assets to s3 bucket that is used for auto updates
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          npx nx run shell-desktop:make:production --publishPolicy=always

this is my customSign.js file. you can tell electron-builder to use this file instead of the default thing they do

const cp = require('child_process');

function isEmpty(value) {
  return !value || !value.length;
}

// uses configuration.path to sign the file, passes in the keyvault auth as an env variable
exports.default = async function (configuration) {
  const timeserver = 'http://timestamp.digicert.com';
  const azureURL = 'https://azureURLofTheCert/';
  const certificateName = 'nameofcert';
  const authRaw = process.env.KEYVAULT_AUTH;
  const keyVault = JSON.parse(authRaw);

  if (isEmpty(configuration.path)) {
    throw new Error('Path to file is required');
  }
  // AzureSignTool command with all the required parameters
  const command = [
    'azuresigntool.exe sign -fd sha384',
    '-kvu',
    azureURL,
    '-kvi',
    keyVault.clientId,
    '-kvt',
    keyVault.tenantId,
    '-kvs',
    keyVault.clientSecret,
    '-kvc',
    certificateName,
    '-tr',
    timeserver,
    '-td',
    'sha384',
    '-v',
  ];

  // throws an error if non-0 exit code, that's what we want.
  cp.execSync(`${command.join(' ')} "${configuration.path}"`, {
    stdio: 'inherit',
  });
};

@jcharnley Thanks so much for the input. This helped us to complete our setup. One change is we are using Global sign certificate instead of digicert. By any change, do you know the timestamp url for global sign instead of digicert. Also the exe file created after signing is opening without any issues in windows. But during download from chrome, still showing as malicious software by chrome. Any advice on this ?

darkangel081195 avatar Feb 01 '24 23:02 darkangel081195

In terms of it being "malicious", I'm wondering it it's the elevate.exe? Anyone willing to try this for win.sign: path-to-sign.js

const path = require('path')
const { doSign } = require('app-builder-lib/out/codeSign/windowsCodeSign')

/**
 * @type {import("electron-builder").CustomWindowsSign} sign
 */
module.exports = async function sign(config, packager) {
  // Do not sign if no certificate is provided.
  if (!config.cscInfo) {
    return
  }

  const targetPath = config.path
  // Do not sign elevate file, because that prompts virus warning?
  if (targetPath.endsWith('elevate.exe')) {
    return
  }

  await doSign(config, packager)
}

mmaietta avatar Feb 02 '24 00:02 mmaietta

@jcharnley Thanks so much for the input. This helped us to complete our setup. One change is we are using Global sign certificate instead of digicert. By any change, do you know the timestamp url for global sign instead of digicert. Also the exe file created after signing is opening without any issues in windows. But during download from chrome, still showing as malicious software by chrome. Any advice on this ?

I have noticed that too, I havnt looked at it for ages. I see the Publisher name is not on the exe cert part, but as this was just a test of doing it and not for production I have yet to investigate it

if u find out the issue please let me know

jcharnley avatar Feb 07 '24 13:02 jcharnley