Checksum mismatch after code sign Electron Builder / Updater
Asked Answered
C

5

5

After running electron-builder for Windows / NSIS during a build process, our dev-ops team set a build script that runs to code sign the exe before deployment. After it gets to the server, electron-updater fails with a sha512 checksum mismatch (which the error occurs during the install, after it has been fully downloaded). I have also tried pulling the exe file down from the server and running a codesign util from Visual Studio CMD, and then re-uploading. The auto updater also fails with the same error.

Is it not possible to sign the exe after it has been generated, and to still allow for the auto updater to work?

Signing:

signtool.exe sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 value "path"

Log:

Error: sha512 checksum mismatch, expected [value], got [different value]

Configuration in package.json:

"build": {
    "appId": "com.stripped.stripped.stripped",
    "directories": {
        "output": "dist-exe",
        "app": "dist"
    },
    "win": {
        "target": "nsis",
        "icon": "dist/assets/favicon/favicon-256x256.ico",
        "verifyUpdateCodeSignature": false,
        "publish": {
            "provider": "generic",
            "url": "##{ElecronAppUpdaterLocation}##"
        }
    },
    "nsis": {
        "artifactName": "Setup_${version}.${ext}",
        "installerIcon": "dist/assets/favicon/favicon-256x256.ico",
        "installerHeaderIcon": "dist/assets/favicon/favicon-256x256.ico"
    }
}
Clothe answered 25/9, 2017 at 14:14 Comment(0)
P
5

If anyone still looking for manually generating electron checksum, you can use the script mentioned here https://github.com/electron-userland/electron-builder/issues/3913#issuecomment-504698845

I have tested it and it works fine, Electron was able to update the app to the version with the manually generated checksum.

const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

const YOUR_FILE_PATH = '';  //  POPULATE THIS

function hashFile(file, algorithm = 'sha512', encoding = 'base64', options) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash(algorithm);
    hash.on('error', reject).setEncoding(encoding);
    fs.createReadStream(
      file,
      Object.assign({}, options, {
        highWaterMark: 1024 * 1024,
        /* better to use more memory but hash faster */
      })
    )
      .on('error', reject)
      .on('end', () => {
        hash.end();
        console.log('hash done');
        console.log(hash.read());
        resolve(hash.read());
      })
      .pipe(
        hash,
        {
          end: false,
        }
      );
  });
}

const installerPath = path.resolve(
  __dirname,
  YOUR_FILE_PATH
);

hashFile(installerPath);
Prosector answered 13/2, 2020 at 13:8 Comment(2)
Hi, I can't figure out how to use this code snippet. Where to put this code snippet, and how/when to run it?Disorder
I would also like to know how to use this? YOUR_FILE_PATH, is this the path to the installer? and is this the path to the installer on the web server or local installer file?Ebullience
D
3

After struggling with this issue for so many hours, here is what I found: (case: win, nsis)

  1. When I build the app with electron builder, It generates a .exe file, after that it computes its hash and saves it in another file called latest.yml

  2. These 2 are uploaded to the update server (in my case it was github)

  3. When the previous version of the app would update, it downloads the .exe and latest.yml files again, re-compute the hash of the .exe and check if it is still the same (meaning the executable was not manipulated)

  4. The issue happens because at that point when it hashes the downloaded .exe it does not get back what is expected. In my case this happened because I had a separate code signing script that signed my build. I found that the hash of my build changes after signing it, so the latest.yml file should be updated in my releases.

To fix this issue, I used the signing script given in above answers. When I had finished building and signing my .exe, I re-compute its hash using this script above, and then update the latest.yml accordingly.

When I did that auto-update worked well, hope it helps ><

Detonate answered 24/8, 2022 at 7:56 Comment(2)
I do not understand when to apply the above code. Could you please update you answer with when to do it and where to apply this code.Ebullience
I'm having same issue, How can I re-compute latest.yml?Wenz
S
1

Get the script from one of the responses and update it a little bit. This allows me to use it right out of the box.

This script requires the installation of the yaml package from npm.

After that, you can use it in your CI pipeline or elsewhere:

ts-node ./.erb/scripts/recompute-hash.ts
import YAML from "yaml";
import crypto from "crypto";
import fs from "fs";
import fsPromises from "fs/promises";
import path from "path";

interface ILatestDto {
  version: string;
  files: IFileDto[];
  path: string;
  sha512: string;
  releaseDate: string;
}

interface IFileDto {
  url: string;
  size: number;
  sha512: string;
}

const hashFile = (file: string): Promise<string> => {
  return new Promise<string>((resolve, reject) => {
    const hash = crypto.createHash("sha512");
    hash.on("error", reject).setEncoding("base64");

    fs.createReadStream(file, {
      highWaterMark: 1024 * 1024,
    })
      .on("error", reject)
      .on("end", () => {
        hash.end();
        resolve(hash.read());
      })
      .pipe(hash, {
        end: false,
      });
    const stats = fs.statSync(file);
    console.log("File size: ", stats.size);
  });
};

const updateLatestYaml = async (
  latestYamlPath: string,
  targetPath: string,
  newHash: string,
): Promise<void> => {
  const latestYaml = await fsPromises.readFile(latestYamlPath, {
    encoding: "utf-8",
  });
  const latestDto = YAML.parse(latestYaml) as ILatestDto;
  const parsedPath = path.parse(targetPath);
  const targetFileName = parsedPath.name + parsedPath.ext;
  const targetFileSize = fs.statSync(targetPath).size;

  if (latestDto.path.includes(targetFileName)) {
    latestDto.sha512 = newHash;
  }

  for (const file of latestDto.files) {
    if (file.url.includes(targetFileName)) {
      file.sha512 = newHash;
      file.size = targetFileSize;
    }
  }

  await fsPromises.writeFile(latestYamlPath, YAML.stringify(latestDto));
};

void (async () => {
  try {
    if (!process.env.TARGET_PATH) {
      console.error("TARGET_PATH is missing");
      process.exit(1);
    }

    if (!process.env.LATEST_YAML_PATH) {
      console.error("LATEST_YAML_PATH is missing");
      process.exit(1);
    }

    const newHash = await hashFile(process.env.TARGET_PATH);
    console.log("New hash: ", newHash);

    await updateLatestYaml(
      process.env.LATEST_YAML_PATH,
      process.env.TARGET_PATH,
      newHash,
    );
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
})();
Sentimental answered 24/1 at 8:23 Comment(0)
C
0

Per the response to the issue on electron-builder in GH, this is not allowed to be signed after generated, which unfortunately changes our build process.

Clothe answered 27/9, 2017 at 14:14 Comment(0)
H
0

I came across this question yesterday whilst struggling with a very similar setup. In my case I was able to use electron-builder's built in support for a custom signing script, which may well have been added in after this question was originally asked.

My setup is that I'm building a Windows NSIS target on Linux and I want to sign it with an EV Code Signing Certificate stored on a USB stick. I haven't had the strength to try and get this to work on Linux yet so currently my script will pause the electron-bulder process until I've finished manually signing the .exe using a Windows VM running on the same box (basic but it works).

The relevant bits of my package.json are

"build": {
  ...
  "win": {
    ...
    "sign": "scripts/sign.js",
    "signingHashAlgorithms": ["sha256"],  # sign.js will be called once for each hash algorithm
    ...
  },
  "nsis": {
    ...
  }
}

and my sign.js file looks like

const readline = require("node:readline")
const { stdin: input, stdout: output } = require("node:process")

const INSTALLER_PREFIX = ... // Set expected installer prefix here

// A very low tech placeholder to last until we can work out how to sign using the 
// Sectigo EV USB on the linux command line
exports.default = async function (configuration) {

    if (configuration.path.startsWith(INSTALLER_PREFIX)) {
        
        const rl = readline.createInterface({ input, output })

        await new Promise(function(resolve) {
            rl.question("Please sign the exe and press enter when complete... ", () => {
                rl.close()
                resolve()
            })
        });

    }
}

This script could be simplified a bit with the new Node JS promises API but it's not available in the version of NodeJS that I'm currently using.

Once the signing is complete, electron-builder will then proceed to calculate the sha512 checksum of the signed exe and all works well with the auto updater.

Harbin answered 8/11, 2023 at 21:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.