tutanota/buildSrc/installerSigner.js

166 lines
6.1 KiB
JavaScript
Raw Permalink Normal View History

/**
* Utility to codesign the finished Installers.
* This enables the App to verify the authenticity of the Updates, and
* enables the User to verify the authenticity of their manually downloaded
* Installer with the openssl utility.
*
* ATTENTION MAC USERS: Safari started to automatically unpack zip files and then delete them,
* so you'll have to look in your trash to get the original file.
* once we switch to dmg this won't be necessary anymore, but:
* https://github.com/electron-userland/electron-builder/issues/2199
*
2019-09-05 12:16:25 +02:00
* The installer signatures are created in the following files:
* https://app.tuta.com/desktop/win-sig.bin (for Windows)
* https://app.tuta.com/desktop/mac-sig-dmg.bin (for Mac .dmg installer)
* https://app.tuta.com/desktop/mac-sig-zip.bin (for Mac .zip update file)
* https://app.tuta.com/desktop/linux-sig.bin (for Linux)
*
2018-12-20 14:40:58 +01:00
* They allow verifying the initial download via
*
* # get public key from github
2019-09-05 12:16:25 +02:00
* wget https://raw.githubusercontent.com/tutao/tutanota/master/tutao-pub.pem
* or
2019-09-05 12:16:25 +02:00
* curl https://raw.githubusercontent.com/tutao/tutanota/master/tutao-pub.pem > tutao-pub.pem
* # validate the signature against public key
* openssl dgst -sha512 -verify tutao-pub.pem -signature signature.bin tutanota.installer.ext
*
* openssl should Print 'Verified OK' after the second command if the signature matches the certificate
*
* This prevents an attacker from getting forged Installers/updates installed/applied
*
* get pem cert from pfx:
* openssl pkcs12 -in comodo-codesign.pfx -clcerts -nokeys -out tutao-cert.pem
*
* get private key from pfx:
* openssl pkcs12 -in comodo-codesign.pfx -nocerts -out tutao.pem
*
* get public key from pem cert:
* openssl x509 -pubkey -noout -in tutao-cert.pem > tutao-pub.pem
* */
import path from "node:path"
import fs from "node:fs"
2022-12-27 15:37:40 +01:00
import { spawnSync } from "node:child_process"
2019-09-13 13:49:11 +02:00
import jsyaml from "js-yaml"
import crypto from "node:crypto"
2022-12-27 15:37:40 +01:00
import { base64ToUint8Array } from "@tutao/tutanota-utils"
const SIG_ALGO = "RSASSA-PKCS1-v1_5"
const DIGEST = "SHA-512"
/**
2019-09-05 12:16:25 +02:00
* Creates a signature on the given application file, writes it to signatureFileName and adds the signature to the yaml file.
* Requires environment variable HSM_USER_PIN to be set to the HSM user pin.
*
* if the env var DEBUG_SIGN is set to the path to a directory containing a non-encrypted PEM-encoded private key (filename test.key) and the
* matching PEM-encoded public key (test.pubkey), it will be used instead of the HSM.
*
2019-09-05 12:16:25 +02:00
* @param filePath The application file to sign. Needs to be the full path to the file.
* @param signatureFileName The signature will be written to that file. Must not contain any path.
* @param ymlFileName This yaml file will be adapted to include the signature. Must not contain any path.
*/
export async function sign(filePath, signatureFileName, ymlFileName) {
2022-12-27 15:37:40 +01:00
console.log("Signing", path.basename(filePath), "...")
const dir = path.dirname(filePath)
const sigOutPath = process.env.DEBUG_SIGN
? await signWithOwnPrivateKey(filePath, path.join(process.env.DEBUG_SIGN, "test.key"), signatureFileName, dir)
: await signWithHSM(filePath, signatureFileName, dir)
if (ymlFileName) {
console.log(`attaching signature to yml...`, ymlFileName)
const ymlPath = path.join(dir, ymlFileName)
2022-12-27 15:37:40 +01:00
let yml = jsyaml.load(fs.readFileSync(ymlPath, "utf8"))
const signatureContent = fs.readFileSync(sigOutPath)
2022-12-27 15:37:40 +01:00
yml.signature = signatureContent.toString("base64")
fs.writeFileSync(ymlPath, jsyaml.dump(yml), "utf8")
console.log("signing done")
} else {
console.log("Not attaching signature to yml")
}
}
async function signWithHSM(filePath, signatureFileName, dir) {
console.log("sign with HSM")
2022-12-27 15:37:40 +01:00
const result = spawnSync(
"/usr/bin/pkcs11-tool",
[
"-s",
"-m",
"SHA512-RSA-PKCS",
"--token-label",
"SmartCard-HSM (UserPIN)",
2022-12-27 15:37:40 +01:00
"--id",
"10", // this is the index of the installer verification key
"--pin",
"env:HSM_USER_PIN",
"-i",
path.basename(filePath),
"-o",
signatureFileName,
],
{
cwd: dir,
stdio: [process.stdin, process.stdout, process.stderr],
},
)
2019-09-05 12:16:25 +02:00
if (result.status !== 0) {
throw new Error("error during hsm signing process" + JSON.stringify(result))
}
return path.join(dir, signatureFileName)
}
/**
* takes a pem-encoded private key and returns the raw der-encoded data
* @param key {string} private key pem
* @returns {ArrayBuffer} raw binary der-encoded private key
*/
function pemToBinaryDer(key) {
2022-12-27 15:37:40 +01:00
const pemContentsB64 = key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "")
return base64ToUint8Array(pemContentsB64).buffer
}
/**
* import a key from a pem-string and import it as a WebCrypto CryptoKey
* that can be used for signing
* @param pem
* @returns {Promise<CryptoKey>}
*/
async function importPrivateKey(pem) {
2022-12-27 15:37:40 +01:00
return crypto.webcrypto.subtle.importKey("pkcs8", pemToBinaryDer(pem), { name: SIG_ALGO, hash: DIGEST }, true, ["sign"])
}
/**
* sign the contents of a file with a private key available in PEM format.
*
* a private key to use here can be created with:
* import crypto from "node:crypto"
*
* const {privateKey, publicKey} = await crypto.webcrypto.subtle.generateKey({
* name: 'RSASSA-PKCS1-v1_5',
* modulusLength: 4096,
* publicExponent: new Uint8Array([1, 0, 1]),
* hash: "SHA-512",
* }, true, ['sign', 'verify'])
*
* returns the full path to a file containing the signature in binary format
*/
async function signWithOwnPrivateKey(fileToSign, privateKeyPemFile, signatureOutFileName, dir) {
console.log("sign with private key")
const sigOutPath = path.join(dir, signatureOutFileName)
try {
const fileData = fs.readFileSync(fileToSign) // buffer
2022-12-27 15:37:40 +01:00
const privateKeyPem = fs.readFileSync(privateKeyPemFile, { encoding: "utf-8" })
const cryptoKey = await importPrivateKey(privateKeyPem)
2022-12-27 15:37:40 +01:00
const sig = await crypto.webcrypto.subtle.sign({ name: SIG_ALGO, hash: DIGEST }, cryptoKey, fileData)
fs.writeFileSync(sigOutPath, Buffer.from(sig), null)
} catch (e) {
console.log(`Error signing ${fileToSign}:`, e.message, e.stack)
process.exit(1)
}
return sigOutPath
2022-12-27 15:37:40 +01:00
}