-
Notifications
You must be signed in to change notification settings - Fork 538
/
Copy pathinstallerSigner.js
165 lines (153 loc) · 6.07 KB
/
installerSigner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
* 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
*
* 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)
*
* They allow verifying the initial download via
*
* # get public key from github
* wget https://raw.githubusercontent.com/tutao/tutanota/master/tutao-pub.pem
* or
* 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"
import { spawnSync } from "node:child_process"
import jsyaml from "js-yaml"
import crypto from "node:crypto"
import { base64ToUint8Array } from "@tutao/tutanota-utils"
const SIG_ALGO = "RSASSA-PKCS1-v1_5"
const DIGEST = "SHA-512"
/**
* 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.
*
* @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) {
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)
let yml = jsyaml.load(fs.readFileSync(ymlPath, "utf8"))
const signatureContent = fs.readFileSync(sigOutPath)
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")
const result = spawnSync(
"/usr/bin/pkcs11-tool",
[
"-s",
"-m",
"SHA512-RSA-PKCS",
"--token-label",
"SmartCard-HSM (UserPIN)",
"--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],
},
)
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) {
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) {
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
const privateKeyPem = fs.readFileSync(privateKeyPemFile, { encoding: "utf-8" })
const cryptoKey = await importPrivateKey(privateKeyPem)
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
}