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

how to do Code Signing via esigner from ssl.com without USB Token? #6158

Closed
timebuzzerfelix opened this issue Aug 16, 2021 · 15 comments
Closed

Comments

@timebuzzerfelix
Copy link

A few months ago ssl.com launched "esigner" and a CLI tool called CodeSignTool. This allows to sign apps for windows without a USB Token, even with EV Code Signing what normally needs the USB Token. This is really cool, allows for instance to build an app on a hosted build server and allows to fully automate the build process, no Token PIN has to be entered manually during the building process.

I haven't find any information how to setup this with electron-bulder. Is there someone who already did it?

Of course it would be super cool if electron-bulder would support the CodeSignTool, no need for local certificate and usb token anymore.

@HyperSprite
Copy link

I've done it, was going to do a writeup and submit a PR to the docs but since you have a pressing need, I'll just throw out what I have now.

I read through all the windows sign/singing on Unix etc. and you don't need much of it with this CodeSignTool. This was the most useful part and the basis for what I ended up doing.

After you have your certs paid for (and I assume you are using the team feature, were each user can have their own login)...

  • Enroll using this guide for Team Sharing for eSigner Document and EV Code Signing Certificates and get the Authenticator setup. You can stop when you get to You’re now ready to start signing code or documents with the shared eSigner certificate. Don't worry about signing the docs at the end unless you want to, it won't make any difference.
  • Download the CodeSignTool from ssl.com and unpack in a directory adjacent to your projects code (you only need one CodeSignTool per computer, not one per project you are signing, nothing changes in that dir). The ssl.com esigner codesigntool command guide has a download link and is a handy reference.
  • For our purposes you will need the following values from ssl.com:
    • WINDOWS_SIGN_USER_NAME: the email address you use for ssl.com auth
    • WINDOWS_SIGN_USER_PASSWORD: your ssl.com password
    • WINDOWS_SIGN_CREDENTIAL_ID: the certificate's credential id, located on the certificate order page.
    • WINDOWS_SIGN_USER_TOTP: the totp code - on the certificate order page, use your PIN to Show QR Code and copy the secret code under the QR.

THESE ARE YOUR ssl.com CREDENTIALS, BE CAREFUL WITH THEM!

This example uses these values running under process.env, you can devise your own way to set these values, but for example sake, we'll just prepend them to our yarn release like:

WINDOWS_SIGN_USER_NAME=my@example.com WINDOWS_SIGN_USER_PASSWORD=myPasswordWithoutBashEscapeCharsIntIt WINDOWS_SIGN_CREDENTIAL_ID=someCredentialId WINDOWS_SIGN_USER_TOTP=totpThing yarn release 

You will need to update your electron builder section, it no longer needs the certificateSubjectName but will need a path to our code sign script:

"win": {
  "signingHashAlgorithms": ["sha256"],
  "sign": "./sign.js",
  "target": ["nsis"]
}

"signingHashAlgorithms": ["sha256"], is optional, see this to decide

And here's the ./sign.js script that should be located in the root of your project.

const path = require('path');
const fs = require('fs');
const childProcess = require('child_process');

const TEMP_DIR = path.join(__dirname, 'release', 'temp');

if (!fs.statSync(TEMP_DIR).isDirectory()) {
  fs.mkdirSync(TEMP_DIR);
}

function sign(configuration) {
  // credentials from ssl.com
  const USER_NAME = process.env.WINDOWS_SIGN_USER_NAME;
  const USER_PASSWORD = process.env.WINDOWS_SIGN_USER_PASSWORD;
  const CREDENTIAL_ID = process.env.WINDOWS_SIGN_CREDENTIAL_ID;
  const USER_TOTP = process.env.WINDOWS_SIGN_USER_TOTP;
  if (USER_NAME && USER_PASSWORD && USER_TOTP && CREDENTIAL_ID) {
    console.log(`Signing ${configuration.path}`);
    const { name, dir } = path.parse(configuration.path);
    // CodeSignTool can't sign in place without verifying the overwrite with a
    // y/m interaction so we are creating a new file in a temp directory and
    // then replacing the original file with the signed file.
    const tempFile = path.join(TEMP_DIR, name);
    const setDir = `cd ../CodeSignTool-v1.2.0-windows`;
    const signFile = `CodeSignTool sign -input_file_path="${configuration.path}" -output_dir_path="${TEMP_DIR}" -credential_id="${CREDENTIAL_ID}" -username="${USER_NAME}" -password="${USER_PASSWORD}" -totp_secret="${USER_TOTP}"`;
    const moveFile = `mv "${tempFile}" "${dir}"`;
    childProcess.execSync(`${setDir} && ${signFile} && ${moveFile}`, { stdio: 'inherit' });
  } else {
    console.warn(`sign.js - Can't sign file ${configuration.path}, missing value for:
${USER_NAME ? '' : 'WINDOWS_SIGN_USER_NAME'}
${USER_PASSWORD ? '' : 'WINDOWS_SIGN_USER_PASSWORD'}
${CREDENTIAL_ID ? '' : 'WINDOWS_SIGN_CREDENTIAL_ID'}
${USER_TOTP ? '' : 'WINDOWS_SIGN_USER_TOTP'}
`);
    process.exit(1);
  }
}

exports.default = sign;

Let me know if you have any questions etc.

@timebuzzerfelix
Copy link
Author

timebuzzerfelix commented Aug 17, 2021

Christopher you're a hero, thank you very much!

INFO
In my case the email address does not work in WINDOWS_SIGN_USER_NAME, only the username

ISSUE 1
My target folder is called dist, so the temp files should go into dist/temp
but if there's no temp folder, the fs.mkdirSync(TEMP_DIR); does not create one. It only works if I add the temp folder manually.
Do you have any idea?

ISSUE 2
Regarding const moveFile = `mv "${tempFile}" "${dir}"`;

  1. I had to change the "mv" into "move"
  2. I had to extend it with .exe So now the line looks like this:

const moveFile = `move "${tempFile}.exe" "${dir}"`;

Seems ok in case we're signing .exe files only

ISSUE 3
It worked if I put all the SSL.com credentials into the sign.js file. I tried your approach with
WINDOWS_SIGN_USER_NAME=my@example.com WINDOWS_SIGN_USER_PASSWORD=myPasswordWithoutBashEscapeCharsIntIt WINDOWS_SIGN_CREDENTIAL_ID=someCredentialId WINDOWS_SIGN_USER_TOTP=totpThing yarn release
but didn't work. Do I understand right that this is the command you start the build process with?

IDEA
If someone wants to use an external build server service like travis-ci, I could be a good idea to extend the script that downloads and unpacks the CodeSignTool?

@HyperSprite
Copy link

This is some good feedback, like I said, I wasn't quite ready to write this stuff up so I knew there would be issues.

You might be in a better place to test some of these things than I am, I primarily use a Mac to dev, going so far as to use VS Code though ssh to my Windows VM so I am on it as little as possible. So some things might work for me differently than a typical Windows dev env.

  • ISSUE 1: Node has docs about caveats with fs on windows and you may be falling into one of those when it tries to create the folder. I have all my code in c:\code\... because I don't trust Windows paths since back to the Win98 days. Try logging out TEMP_DIR to see if it is where you expect it to be. It could also be a permissions issue. Hard to say there.

  • ISSUE 2:

    • 2.a - mv vs move, oh yeah, I use bash on Windows, so this difference makes sense.
    • 2.b - I think these two lines need to be changed so you don't need to add the exe:
      const { name, dir } = path.parse(configuration.path);
      const tempFile = path.join(TEMP_DIR, name);
    

    to

      const { base, dir } = path.parse(configuration.path);
      const tempFile = path.join(TEMP_DIR, base);
    
  • ISSUE 3: tl;dr, this might need cross-env for powershell/cmd. Go read Kent's description at the link.

    • More context: Did you try logging them out? I don't put the real credentials on process.env on the command line. I have another script that I run once to collect my env vars and save them into an encrypted file, then pass only a PASSWORD=<some made up password i only use for this build process> yarn release but the results should be the same because the script still reads process.env for the PASSWORD. This too might be a difference with bash and powershell or cmd. I see a lot of stackoverflow questions about the issue of not passing process.env but it's always worked fine for me so I've been scratching my head. cross-env package might be what is needed for non-bash.
  • IDEA: yeah, wouldn't that be great. I only got any signing working last week so getting the CI stuff to work would be nice but I haven't got the time or need quite yet to prioritize that right now. Be happy to test or help out if I can.

@timebuzzerfelix
Copy link
Author

It's a few months ago, now SSL.com changed it and the service is now not free anymore.. Seems they want a subscription fee. Not very cool if you need signing your app only few times a year.. Is there any other solution on the market for signing apps on Github actions with an EV code signing certificate without a usb dongle?

@stale
Copy link

stale bot commented Apr 16, 2022

Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

@stale stale bot added the backlog label Apr 16, 2022
@stale stale bot closed this as completed Apr 30, 2022
@mhellsten-ml
Copy link

Thanks for this! One more small tweak:

The temp dir creation didn't work for me, but this does:

if (!fs.existsSync(TEMP_DIR)) {
  fs.mkdirSync(TEMP_DIR, { recursive: true });
}

@arjun-formicsio
Copy link

Hey does the ssl.com code signing certificate still work to sign electron apps? I'm trying to build an electron app that has code signing and I'm considering this code signing certificate:
https://www.ssl.com/certificates/code-signing/
Is this still working?

Thanks

@t3chguy
Copy link
Contributor

t3chguy commented Apr 11, 2023

@arjun-formicsio I managed to do SSL.com eSigner signing in a much simpler manner using the codesign support in electron-builder along with SSL.com's Cloud Key Adapter. You can see it at https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_windows.yaml#L118-L163

@mhellsten-ml
Copy link

Is this still working?

Yes

@c960657
Copy link
Contributor

c960657 commented Jun 12, 2023

@arjun-formicsio I managed to do SSL.com eSigner signing in a much simpler manner using the codesign support in electron-builder along with SSL.com's Cloud Key Adapter. You can see it at https://github.com/vector-im/element-desktop/blob/develop/.github/workflows/build_windows.yaml#L118-L163

Thanks, this was very useful 🤷‍♂️.

For those using this approach, make sure that you specify signingHashAlgorithms and override SIGNTOOL_PATH (apparently the version of SignTool.exe included in electron-builder isn't sufficient).

@rajjagani021
Copy link

@HyperSprite

Do you have an idea to do the same procedure with Electron Forge? Like, as per their doc, they are asking .pfx certificate, which ssl.com won't issue?

@HyperSprite
Copy link

@rajjagani021 Sadly no, the trick in electron-builder is the escape hatch "sign": "./sign.js", where it replaces the built in signer with a custom script that handles the signing outside of the regular flow.
I don't seen anything like that in https://github.com/electron/windows-installer. It's one of the reasons I haven't migrated yet.

@tibicle-henil
Copy link

I tried with the above solution but its giving some error like bellow

The system cannot find the path specified.
⨯ Command failed: cd ../CodeSignTool-v1.2.0-windows && CodeSignTool sign

@yuvalkarmi
Copy link

yuvalkarmi commented Mar 13, 2024

3 days of trying to figure this out, so I figured I'd provide detailed instructions for anyone else coming across this:

By far the easiest way I've found of signing with the SSL.com eSigner and electron-builder is to use their Github Action, but not directly from within a Github Actions step, but from within the build process.

The benefit of using their Github Action is that it downloads and configures all of the right software for you directly on the github runner with zero involvement on your end, with the latest version, and cross-platform.

Because electron-builder cannot "pause" its build process for you to sign in a separate Github Action step, you have to call the eSigner from within the build process javascript directly. Here's how:

  1. In a Github Action step, checkout the eSigner Github Action code (e.g. clone it) before running electron builder.
  2. In the Github Action step that calls electron-builder, pass in all the required parameters for for the eSigner action, but as env vars and prefixed with INPUT_ and all caps - that's how Github Actions passes in params to the underlying code that runs the action when it's run as its own step normally
  3. Call the eSigner code through the electron-builder.js config file (under the windows.sign) key to do the actual signing

Billing alert:
Important: electron-builder calls the sign function once for every file it tries to sign - for me that's 6 times. But since SSL.com charges per eSigner signing, I'm only signing the one file I need. The eSigner technically does allow for batch signing, but I just need the installer file signed - everything else is unnecessary. So I opted out of that.
Adjust as needed for your setup.

Here's how I do this:

In your github actions config file (e.g. build.yml):

# download 'SSLcom/esigner-codesign' to a folder called 'esigner-codesign' in the root of the project
- name: Checkout esigner-codesign repository (Windows)
  if: contains(matrix.os, 'windows')
  uses: actions/checkout@v3
  with:
    repository: 'SSLcom/esigner-codesign'
    path: esigner-codesign

# builds, signs, and publishes - actual signing happens within electron-builder.js
- name: Publish (Windows)
  if: contains(matrix.os, 'windows')
  run: npm run build && npx electron-builder build --config electron-builder.js -p always
  env: 
    GH_TOKEN: ${{ secrets.GITHUB_ACCESS_TOKEN }}
    # NOTE: must explicitly pass in even the parameters that esigner-codesign says are optional since we're not using the action directly, but rather passing the params in as env vars:
    CODE_SIGN_SCRIPT_PATH: "${{ github.workspace }}/esigner-codesign/dist/index.js"
    INPUT_COMMAND: "sign"
    INPUT_FILE_PATH: "${{ github.workspace }}/dist/REPLACE_WITH_YOUR_APP_NAME Setup ${{ needs.get-versions.outputs.package_version }}.exe"
    INPUT_OVERRIDE: "true"
    INPUT_MALWARE_BLOCK: "false"
    INPUT_CLEAN_LOGS: "false"
    INPUT_JVM_MAX_MEMORY: "1024M"
    INPUT_ENVIRONMENT_NAME: "PROD"
    INPUT_USERNAME: ${{ secrets.SSL_COM_USERNAME }}
    INPUT_PASSWORD: ${{ secrets.SSL_COM_PASSWORD }}
    INPUT_TOTP_SECRET: ${{ secrets.SSL_COM_TOTP_SECRET }}
    INPUT_CREDENTIAL_ID: ${{ secrets.SSL_COM_CREDENTIAL_ID }}

Then in your electron-builder.js

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

const config = {
  // your config goes here
  win: {
    target: [
      "nsis"
    ]
  },
}

if (process.env.CODE_SIGN_SCRIPT_PATH) {
  // Dynamically get the version number from package.json
  const version = execSync('node -p "require(\'./package.json\').version"').toString().trim();
  const versionedExe = `REPLACE_WITH_YOUR_APP_NAME Setup ${version}.exe`;

  config.win.sign = (configuration) => {
    console.log("Requested signing for ", configuration.path);

    // Only proceed if the versioned exe file is in the configuration path - skip signing everything else
    if (!configuration.path.includes(versionedExe)) {
      console.log("Configuration path does not include the versioned exe, signing skipped.");
      return true;
    }

    const scriptPath = process.env.CODE_SIGN_SCRIPT_PATH;

    try {
      // Execute the sign script synchronously
      const output = execSync(`node "${scriptPath}"`).toString();
      console.log(`Script output: ${output}`);
    } catch (error) {
      console.error(`Error executing script: ${error.message}`);
      if (error.stdout) {
        console.log(`Script stdout: ${error.stdout.toString()}`);
      }
      if (error.stderr) {
        console.error(`Script stderr: ${error.stderr.toString()}`);
      }
      return false;
    }

    return true; // Return true at the end of successful signing
  };

  // sign only for Windows 10 and above - adjust for your code as needed
  config.win.signingHashAlgorithms = ["sha256"];

}

module.exports = config;

One important bit, notice the needs.get-versions.outputs.package_version in the Github Action step? I get that by running a separate build job first that reads the version from the package.json. Probably overkill, so if I find a way to read the version that's better, please write back. Here's the job I run first, if you want to use my code verbatim:

jobs:
  get-versions:
    # if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    outputs:
      package_version: ${{ steps.set_versions.outputs.package_version }}
    steps:
      - uses: actions/checkout@v3
      - name: Get the version
        id: set_versions
        run: |
          echo "::set-output name=package_version::$(node -p "require('./package.json').version")"

Thanks @HyperSprite for the useful reference code on how to do custom signing!

@t3chguy
Copy link
Contributor

t3chguy commented Mar 13, 2024

For completeness, if you wanted to more heavily leverage the code signing support in electron-builder with no custom signing script you can use the eSigner CKA:

https://github.com/element-hq/element-desktop/blob/develop/.github/workflows/build_windows.yaml#L117-L154

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

No branches or pull requests

9 participants