Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

June 2023 Code Signing with HSM APIs #7605

Closed
petervanderwalt opened this issue Jun 7, 2023 · 41 comments
Closed

June 2023 Code Signing with HSM APIs #7605

petervanderwalt opened this issue Jun 7, 2023 · 41 comments
Labels

Comments

@petervanderwalt
Copy link

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 electron/windows-installer#473 (comment)

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

@chscott
Copy link

chscott commented Jun 13, 2023

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.

@petervanderwalt
Copy link
Author

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!

@jcharnley
Copy link

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?

@petervanderwalt
Copy link
Author

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?

@chscott
Copy link

chscott commented Jun 22, 2023

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"

@mmaietta mmaietta pinned this issue Jun 24, 2023
@github-actions
Copy link
Contributor

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 github-actions bot added the Stale label Aug 22, 2023
@petervanderwalt
Copy link
Author

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

@jcharnley
Copy link

jcharnley commented Aug 22, 2023

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

@github-actions github-actions bot removed the Stale label Aug 23, 2023
@mmaietta
Copy link
Collaborator

mmaietta commented Aug 23, 2023

@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 🤔

@petervanderwalt
Copy link
Author

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 (:

@HenriqueOtsuka
Copy link

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?

@MasterOdin
Copy link

MasterOdin commented Sep 13, 2023

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.

@davej
Copy link
Contributor

davej commented Sep 20, 2023

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.

@jcharnley
Copy link

jcharnley commented Sep 20, 2023

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

@jcharnley
Copy link

jcharnley commented Oct 6, 2023

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 ?

@MasterOdin
Copy link

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.

@jcharnley
Copy link

awesome thanks ill do that too

@Keith-CY
Copy link

Keith-CY commented Oct 26, 2023

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: nervosnetwork/neuron#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

@RamK777-stack
Copy link

RamK777-stack commented Dec 29, 2023

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 ?

@MasterOdin
Copy link

@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.

@RamK777-stack
Copy link

@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.

@MasterOdin
Copy link

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

@RamK777-stack
Copy link

Thanks @MasterOdin

@theogravity
Copy link

theogravity commented Jan 25, 2024

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)

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

@darkangel081195
Copy link

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.

@jcharnley
Copy link

jcharnley commented Jan 31, 2024

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',
  });
};

@darkangel081195
Copy link

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 ?

@mmaietta
Copy link
Collaborator

mmaietta commented Feb 2, 2024

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)
}

@jcharnley
Copy link

jcharnley commented Feb 7, 2024

@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

@darkangel081195
Copy link

@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 i will definitely check it. As a next step, have you implemented auto updates in the app. If yes, can you kindly provide any reference to it for Mac OS arm64 and x64 builds

Thanks.

@jcharnley
Copy link

jcharnley commented Feb 8, 2024 via email

@jcharnley
Copy link

@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 i will definitely check it. As a next step, have you implemented auto updates in the app. If yes, can you kindly provide any reference to it for Mac OS arm64 and x64 builds

Thanks.

did u fix the issue ?

@theogravity
Copy link

theogravity commented Mar 6, 2024

@darkangel081195 You can use any compliant timeserver. I used digicert's (http://timestamp.digicert.com) with our globalsign cert signing.

I signed in windows via:

      - name: Sign windows electron binary
        shell: powershell
        working-directory: C:\AzureSignTool
        run: |
          AzureSignTool.exe sign `
            -du "https://<host>" `
            -fd "sha256" `
            -kvu "https://${{ SIGNING_KEY_VAULT_NAME }}.vault.azure.net" `
            -kvi "${{ _SIGNING_CLIENT_ID }}" `
            -kvt "${{ SIGNING_TENANT_ID }}" `
            -kvs "${{ SIGNING_CLIENT_SECRET }}" `
            -kvc "${{ SIGNING_CERTIFICATE_NAME }}" `
            -tr "http://timestamp.digicert.com" `
            -td "sha256" `
            -v `
            -s "C:\<path to file>.exe"

I used Visual Studio to build the AzureSignTool since the published versions seem to be outdated.

Our electron app is packaged using the electron-builder one-click installer, and I made sure that the installer and application binary was signed. I did not sign elevate.exe, but our app doesn't require admin permissions to begin with. I store it in a zip file and download from chrome and get no warnings around authenticode when installing or using the app.

@jcharnley
Copy link

@darkangel081195 You can use any compliant timeserver. I used digicert's (http://timestamp.digicert.com) with our globalsign cert signing.

I signed in windows via:

      - name: Sign windows electron binary
        shell: powershell
        working-directory: C:\AzureSignTool
        run: |
          AzureSignTool.exe sign `
            -du "https://<host>" `
            -fd "sha256" `
            -kvu "https://${{ SIGNING_KEY_VAULT_NAME }}.vault.azure.net" `
            -kvi "${{ _SIGNING_CLIENT_ID }}" `
            -kvt "${{ SIGNING_TENANT_ID }}" `
            -kvs "${{ SIGNING_CLIENT_SECRET }}" `
            -kvc "${{ SIGNING_CERTIFICATE_NAME }}" `
            -tr "http://timestamp.digicert.com" `
            -td "sha256" `
            -v `
            -s "C:\<path to file>.exe"

I used Visual Studio to build the AzureSignTool since the published versions seem to be outdated.

Our electron app is packaged using the electron-builder one-click installer, and I made sure that the installer and application binary was signed. I did not sign elevate.exe, but our app doesn't require admin permissions to begin with. I store it in a zip file and download from chrome and get no warnings around authenticode when installing or using the app.

@theogravity what does the -du "https://" stand for, I think that is the part am missing in my sign command

@theogravity
Copy link

theogravity commented Mar 6, 2024

@darkangel081195 You can use any compliant timeserver. I used digicert's (http://timestamp.digicert.com) with our globalsign cert signing.
I signed in windows via:

      - name: Sign windows electron binary
        shell: powershell
        working-directory: C:\AzureSignTool
        run: |
          AzureSignTool.exe sign `
            -du "https://<host>" `
            -fd "sha256" `
            -kvu "https://${{ SIGNING_KEY_VAULT_NAME }}.vault.azure.net" `
            -kvi "${{ _SIGNING_CLIENT_ID }}" `
            -kvt "${{ SIGNING_TENANT_ID }}" `
            -kvs "${{ SIGNING_CLIENT_SECRET }}" `
            -kvc "${{ SIGNING_CERTIFICATE_NAME }}" `
            -tr "http://timestamp.digicert.com" `
            -td "sha256" `
            -v `
            -s "C:\<path to file>.exe"

I used Visual Studio to build the AzureSignTool since the published versions seem to be outdated.
Our electron app is packaged using the electron-builder one-click installer, and I made sure that the installer and application binary was signed. I did not sign elevate.exe, but our app doesn't require admin permissions to begin with. I store it in a zip file and download from chrome and get no warnings around authenticode when installing or using the app.

@theogravity what does the -du "https://" stand for, I think that is the part am missing in my sign command

See options here:

https://github.com/vcsjones/AzureSignTool

--description-url [short: -du, required: no]: A URL with more information of the signed content. This parameter serves the same purpose as the /du option in the Windows SDK signtool. If this parameter is not supplied, the signature will not contain a URL description.

I just set it to our website http://switchboard.app. Just goes to the homepage. Not sure if it needs to be any more than that.

@darkangel081195
Copy link

@darkangel081195 You can use any compliant timeserver. I used digicert's (http://timestamp.digicert.com) with our globalsign cert signing.
I signed in windows via:

      - name: Sign windows electron binary
        shell: powershell
        working-directory: C:\AzureSignTool
        run: |
          AzureSignTool.exe sign `
            -du "https://<host>" `
            -fd "sha256" `
            -kvu "https://${{ SIGNING_KEY_VAULT_NAME }}.vault.azure.net" `
            -kvi "${{ _SIGNING_CLIENT_ID }}" `
            -kvt "${{ SIGNING_TENANT_ID }}" `
            -kvs "${{ SIGNING_CLIENT_SECRET }}" `
            -kvc "${{ SIGNING_CERTIFICATE_NAME }}" `
            -tr "http://timestamp.digicert.com" `
            -td "sha256" `
            -v `
            -s "C:\<path to file>.exe"

I used Visual Studio to build the AzureSignTool since the published versions seem to be outdated.
Our electron app is packaged using the electron-builder one-click installer, and I made sure that the installer and application binary was signed. I did not sign elevate.exe, but our app doesn't require admin permissions to begin with. I store it in a zip file and download from chrome and get no warnings around authenticode when installing or using the app.

@theogravity what does the -du "https://" stand for, I think that is the part am missing in my sign command

du -> du should have the url to your product website.

@darkangel081195
Copy link

@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 i will definitely check it. As a next step, have you implemented auto updates in the app. If yes, can you kindly provide any reference to it for Mac OS arm64 and x64 builds
Thanks.

did u fix the issue ?

Not yet. Was caught up in other work. Will try this week and let you know

@petervanderwalt
Copy link
Author

Finally got mine sorted.

Using digicert + keylocker to store the cert, and Github actions + electron builder to build and sign the app

OpenBuilds/OpenBuilds-CONTROL#321 (comment)

Copy link
Contributor

github-actions bot commented Jun 9, 2024

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.

@BlackHole1
Copy link
Contributor

Hey guys. After my investigation and testing, I found a solution that requires the least amount of changes:

Github Actions Config Part

- name: Setup Code Signing (1/2)
  env:
    SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
  run: |
    CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
    echo "$SM_CLIENT_CERT_FILE_B64" | base64 --decode > $CERTIFICATE_PATH
    echo "SM_CLIENT_CERT_FILE=$CERTIFICATE_PATH" >> "$GITHUB_ENV"
    echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
    echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
    echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
    echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
    echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
    echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH
  shell: bash

- name: Setup Code Signing (2/2)
  run: |
    curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o Keylockertools-windows-x64.msi
    msiexec /i Keylockertools-windows-x64.msi /quiet /qn
    smksp_cert_sync.exe
  shell: cmd

- name: Release
  # This is a must. See: https://github.com/electron-userland/electron-builder/pull/8384#issuecomment-2257632066
  shell: powershell
  run: pnpm release
  env:
     SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}

Electron Builder Config Part

{
  "win": {
    certificateSha1: process.env.SM_CODE_SIGNING_CERT_SHA1_HASH
  }
}

No need for customSign.js, perfectly compatible with electron-builder.

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


BTW. Below is my complete win configuration:

import { doSign } from "app-builder-lib/out/codeSign/windowsCodeSign.js";
const fingerprint = process.env.SM_CODE_SIGNING_CERT_SHA1_HASH;

{
  "win": {
    // If there are no special requirements, I suggest using only sha256,
    // because starting from Windows 7, applications must use sha256 signatures to run properly,
    // and Microsoft has deprecated sha1 signature support.
    // Of course, the most important thing is:
    // KeyLocker has a limit on the number of times it can be used (1000 times a year).
    // If each file is signed twice, the quota will be exhausted quickly.
    // See: https://learn.microsoft.com/en-us/sysinternals/announce/sha1deprecation
    signingHashAlgorithms: ["sha256"],
    // According to the existing information, it is necessary to sign the node addon, otherwise,
    // it will encounter: anti-virus software that detects a *.node file as malicious.
    // See: https://github.com/electron-userland/electron-builder/issues/1723
    signExts: [".exe", ".dll", ".node"],
    certificateSha1: fingerprint,
    sign: async (configuration, packager) => {
      // Allow local normal packaging
      if (fingerprint === undefined) {
        return;
      }

      // Ignore signing
      if (configuration.path.includes(path.join("resources", "need_ignore_dir"))) {
        return;
      }

      // Let electron-builder execute the signing step.
      return await doSign(configuration, packager);
    },
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

13 participants