update electron to v43
All checks were successful
Android Build / publish (push) Successful in 55s
Linux Build / publish (push) Successful in 1m6s

This commit is contained in:
olcxja 2026-07-09 22:38:33 +02:00
commit fb6c8b6ee9
5385 changed files with 513060 additions and 123058 deletions

View file

@ -0,0 +1,54 @@
/**
* Decrypt `data` using RC2-CBC with PKCS#7 padding removal (RFC 2268).
*
* Used for pbeWithSHAAnd40BitRC2CBC (OID 1.2.840.113549.1.12.1.6) and
* pbeWithSHAAnd128BitRC2CBC (OID 1.2.840.113549.1.12.1.5).
*
* These OIDs are commonly used to encrypt certificate bags in PKCS#12
* files produced by OpenSSL and Windows with default settings. Node.js 22+
* (OpenSSL 3 default provider) does not support RC2 via `createDecipheriv`.
*/
declare function rc2CbcDecrypt(key: Buffer, iv: Buffer, data: Buffer, effectiveBits: number): Buffer;
/**
* PKCS#12 key/IV derivation function from RFC 7292 Appendix B (SHA-1 variant).
* id = 1 to derive a key, id = 2 to derive an IV.
*
* Throws if `iterations` is outside [1, MAX_PKCS12_PBE_ITERATIONS] to prevent
* CPU exhaustion from a crafted PFX file.
*/
declare function pkcs12PbeDeriveKey(password: Buffer, salt: Buffer, iterations: number, id: number, length: number): Buffer;
/**
* Encode a password as UTF-16 Big Endian with a null terminator, which is the
* format required by PKCS#12 PBE key derivation (RFC 7292).
* An empty string yields [0x00, 0x00] (just the null terminator).
*/
declare function pkcs12PasswordToUtf16(password: string): Buffer;
/**
* Reads certificate info from a PKCS#12 (.pfx) file using pkijs (unobfuscated TypeScript).
* Mirrors the `certificate-info` subcommand of app-builder-bin.
* https://github.com/develar/app-builder/blob/master/pkg/codesign/p12.go
*
* Returns { commonName, bloodyMicrosoftSubjectDn } on success.
*
* Known divergences from the Go binary:
* - No OpenSSL fallback when the pure PKCS#12 decoder fails for a non-password reason.
* - Unknown OIDs are rendered using the raw numeric OID as the type name (e.g. `2.5.4.100=value`); Go uses `OID=#hexbytes` when ASN.1 marshal succeeds.
* - RDN ordering uses DER order; Go normalizes via pkix.Name.ToRDNSequence then reverses via
* BloodyMsString. These coincide for pkijs-generated certs (CN-first DER) but may
* differ for real CA-issued certs stored in traditional C-first DER order.
*/
export declare function readCertInfo(file: string, password: string): Promise<{
commonName: string;
bloodyMicrosoftSubjectDn: string;
}>;
/**
* Internal functions exported exclusively for unit testing.
* Not part of the public API do not use outside of test files.
*/
export declare const _testingOnly: {
pkcs12PbeDeriveKey: typeof pkcs12PbeDeriveKey;
pkcs12PasswordToUtf16: typeof pkcs12PasswordToUtf16;
rc2CbcDecrypt: typeof rc2CbcDecrypt;
MAX_PKCS12_PBE_ITERATIONS: number;
};
export {};

View file

@ -0,0 +1,466 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports._testingOnly = void 0;
exports.readCertInfo = readCertInfo;
const crypto_1 = require("crypto");
const asn1js = require("asn1js");
const pkijs = require("pkijs");
const webcrypto_1 = require("@peculiar/webcrypto");
const fs_extra_1 = require("fs-extra");
const builder_util_1 = require("builder-util");
// OID for codeSigning extended key usage
const CODE_SIGNING_OID = "1.3.6.1.5.5.7.3.3";
// OID for the PKCS#12 certificate bag type
const CERT_BAG_OID = "1.2.840.113549.1.12.10.1.3";
// Maps certificate attribute OIDs to their short names.
// Matches the attributeTypeNames map in the Go reference implementation exactly:
// https://github.com/develar/app-builder/blob/master/pkg/codesign/p12.go
const ATTRIBUTE_TYPE_NAMES = {
"2.5.4.6": "C",
"2.5.4.10": "O",
"2.5.4.11": "OU",
"2.5.4.3": "CN",
"2.5.4.5": "SERIALNUMBER",
"2.5.4.7": "L",
"2.5.4.8": "ST",
"2.5.4.9": "STREET",
"2.5.4.17": "POSTALCODE",
};
// Characters that must be quoted in a DN value to match Go binary BloodyMsString output
const NEEDS_DN_ESCAPING = /[,+"\\<>;]/;
function escapeDnValue(value) {
if (NEEDS_DN_ESCAPING.test(value)) {
// Escape embedded double-quotes by doubling them, then wrap entire value in quotes
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
// Set up the pkijs WebCrypto engine once. @peculiar/webcrypto supports legacy cipher suites
// (RC2, 3DES) used by real-world CA-issued PFX files, unlike native Node.js WebCrypto.
const peculiarCrypto = new webcrypto_1.Crypto();
pkijs.setEngine("peculiar", new pkijs.CryptoEngine({ name: "peculiar", crypto: peculiarCrypto, subtle: peculiarCrypto.subtle }));
function toArrayBuffer(buf) {
const ab = new ArrayBuffer(buf.byteLength);
new Uint8Array(ab).set(buf);
return ab;
}
// ── PKCS#12 legacy PBE support (RFC 7292 Appendix B) ───────────────────────
//
// pkijs's CryptoEngine.decryptEncryptedContentInfo only handles PBES2
// (OID 1.2.840.113549.1.5.13). Many real-world PFX files use the older
// pkcs-12PbeIds ciphers (SHA1+3DES, SHA1+2DES, SHA1+RC2-128, SHA1+RC2-40).
// We implement 3DES/2DES using Node.js's built-in `crypto` module and RC2
// with a pure-TypeScript RFC 2268 implementation — RC2 was moved to
// OpenSSL 3's legacy provider (not loaded by default) and is unavailable
// via `createDecipheriv` in Node.js 22+.
// ── RFC 2268 RC2-CBC implementation ─────────────────────────────────────────
/** Permutation table from RFC 2268. */
const RC2_PITABLE = Uint8Array.from([
0xd9, 0x78, 0xf9, 0xc4, 0x19, 0xdd, 0xb5, 0xed, 0x28, 0xe9, 0xfd, 0x79, 0x4a, 0xa0, 0xd8, 0x9d, 0xc6, 0x7e, 0x37, 0x83, 0x2b, 0x76, 0x53, 0x8e, 0x62, 0x4c, 0x64, 0x88, 0x44,
0x8b, 0xfb, 0xa2, 0x17, 0x9a, 0x59, 0xf5, 0x87, 0xb3, 0x4f, 0x13, 0x61, 0x45, 0x6d, 0x8d, 0x09, 0x81, 0x7d, 0x32, 0xbd, 0x8f, 0x40, 0xeb, 0x86, 0xb7, 0x7b, 0x0b, 0xf0, 0x95,
0x21, 0x22, 0x5c, 0x6b, 0x4e, 0x82, 0x54, 0xd6, 0x65, 0x93, 0xce, 0x60, 0xb2, 0x1c, 0x73, 0x56, 0xc0, 0x14, 0xa7, 0x8c, 0xf1, 0xdc, 0x12, 0x75, 0xca, 0x1f, 0x3b, 0xbe, 0xe4,
0xd1, 0x42, 0x3d, 0xd4, 0x30, 0xa3, 0x3c, 0xb6, 0x26, 0x6f, 0xbf, 0x0e, 0xda, 0x46, 0x69, 0x07, 0x57, 0x27, 0xf2, 0x1d, 0x9b, 0xbc, 0x94, 0x43, 0x03, 0xf8, 0x11, 0xc7, 0xf6,
0x90, 0xef, 0x3e, 0xe7, 0x06, 0xc3, 0xd5, 0x2f, 0xc8, 0x66, 0x1e, 0xd7, 0x08, 0xe8, 0xea, 0xde, 0x80, 0x52, 0xee, 0xf7, 0x84, 0xaa, 0x72, 0xac, 0x35, 0x4d, 0x6a, 0x2a, 0x96,
0x1a, 0xd2, 0x71, 0x5a, 0x15, 0x49, 0x74, 0x4b, 0x9f, 0xd0, 0x5e, 0x04, 0x18, 0xa4, 0xec, 0xc2, 0xe0, 0x41, 0x6e, 0x0f, 0x51, 0xcb, 0xcc, 0x24, 0x91, 0xaf, 0x50, 0xa1, 0xf4,
0x70, 0x39, 0x99, 0x7c, 0x3a, 0x85, 0x23, 0xb8, 0xb4, 0x7a, 0xfc, 0x02, 0x36, 0x5b, 0x25, 0x55, 0x97, 0x31, 0x2d, 0x5d, 0xfa, 0x98, 0xe3, 0x8a, 0x92, 0xae, 0x05, 0xdf, 0x29,
0x10, 0x67, 0x6c, 0xba, 0xc9, 0xd3, 0x00, 0xe6, 0xcf, 0xe1, 0x9e, 0xa8, 0x2c, 0x63, 0x16, 0x01, 0x3f, 0x58, 0xe2, 0x89, 0xa9, 0x0d, 0x38, 0x34, 0x1b, 0xab, 0x33, 0xff, 0xb0,
0xbb, 0x48, 0x0c, 0x5f, 0xb9, 0xb1, 0xcd, 0x2e, 0xc5, 0xf3, 0xdb, 0x47, 0xe5, 0xa5, 0x9c, 0x77, 0x0a, 0xa6, 0x20, 0x68, 0xfe, 0x7f, 0xc1, 0xad,
]);
/** Rotation amounts for R[0], R[1], R[2], R[3] in each mix round. */
const RC2_ROT = [1, 2, 3, 5];
/**
* Expand a variable-length key to 64 sixteen-bit subkeys (RFC 2268 Section 2).
* `effectiveBits` controls the effective key length for export-grade keys
* (40 = RC2-40, 128 = RC2-128).
*/
function rc2ExpandKey(key, effectiveBits) {
// Guard: effectiveBits = 0 causes T8 = 0, making L[128-0] = L[128] an OOB
// access on a 128-element Uint8Array (silently returns undefined, producing
// a wrong key schedule). Values > 1024 exceed the RFC 2268 key schedule table.
if (!Number.isInteger(effectiveBits) || effectiveBits < 1 || effectiveBits > 1024) {
throw new Error(`rc2ExpandKey: effectiveBits must be an integer in [1, 1024], got ${effectiveBits}`);
}
const T = key.length;
const T8 = Math.ceil(effectiveBits / 8);
// 0xff >> (effectiveBits & 7) gives 0xff for multiples-of-8 effective lengths
// (no masking), and a smaller mask for non-multiples.
const TM = 0xff >> (effectiveBits & 7);
const L = new Uint8Array(128);
for (let i = 0; i < T; i++) {
L[i] = key[i];
}
for (let i = T; i < 128; i++) {
L[i] = RC2_PITABLE[(L[i - 1] + L[i - T]) & 0xff];
}
L[128 - T8] = RC2_PITABLE[L[128 - T8] & TM];
for (let i = 127 - T8; i >= 0; i--) {
L[i] = RC2_PITABLE[L[i + 1] ^ L[i + T8]];
}
const K = new Array(64);
for (let i = 0; i < 64; i++) {
K[i] = L[2 * i] | (L[2 * i + 1] << 8);
}
return K;
}
/** Rotate a 16-bit word right by `bits` positions. */
function ror16(word, bits) {
return ((word & 0xffff) >> bits) | ((word << (16 - bits)) & 0xffff);
}
/**
* Decrypt `data` using RC2-CBC with PKCS#7 padding removal (RFC 2268).
*
* Used for pbeWithSHAAnd40BitRC2CBC (OID 1.2.840.113549.1.12.1.6) and
* pbeWithSHAAnd128BitRC2CBC (OID 1.2.840.113549.1.12.1.5).
*
* These OIDs are commonly used to encrypt certificate bags in PKCS#12
* files produced by OpenSSL and Windows with default settings. Node.js 22+
* (OpenSSL 3 default provider) does not support RC2 via `createDecipheriv`.
*/
function rc2CbcDecrypt(key, iv, data, effectiveBits) {
// Guard: a non-multiple-of-8 ciphertext causes the last partial block to be
// processed with `undefined` bytes. In JS, `undefined | (undefined << 8)`
// produces NaN, and `NaN & 0xffff` produces 0 — so partial blocks silently
// produce garbage output rather than throwing.
if (data.length === 0 || data.length % 8 !== 0) {
throw new Error(`rc2CbcDecrypt: ciphertext length ${data.length} is not a positive multiple of the 8-byte RC2 block size`);
}
if (iv.length !== 8) {
throw new Error(`rc2CbcDecrypt: IV must be exactly 8 bytes, got ${iv.length}`);
}
const K = rc2ExpandKey(key, effectiveBits);
const out = Buffer.alloc(data.length);
const prev = Buffer.from(iv); // CBC running state; starts as the IV
for (let offset = 0; offset < data.length; offset += 8) {
const ct = data.subarray(offset, offset + 8);
// Parse ciphertext block as four little-endian 16-bit words
const R = [(ct[0] | (ct[1] << 8)) & 0xffff, (ct[2] | (ct[3] << 8)) & 0xffff, (ct[4] | (ct[5] << 8)) & 0xffff, (ct[6] | (ct[7] << 8)) & 0xffff];
// Reverse the encryption round plan [5 mix, 1 mash, 6 mix, 1 mash, 5 mix].
// For decryption j starts at 63 and decrements in each reverse-mix step.
let j = 63;
const rMix = () => {
for (let i = 3; i >= 0; i--) {
R[i] = ror16(R[i], RC2_ROT[i]);
const r3 = R[(i + 3) % 4];
R[i] = (R[i] - K[j] - (r3 & R[(i + 2) % 4]) - (~r3 & 0xffff & R[(i + 1) % 4])) & 0xffff;
j--;
}
};
const rMash = () => {
for (let i = 3; i >= 0; i--) {
R[i] = (R[i] - K[R[(i + 3) % 4] & 63]) & 0xffff;
}
};
for (let n = 0; n < 5; n++) {
rMix();
}
rMash();
for (let n = 0; n < 6; n++) {
rMix();
}
rMash();
for (let n = 0; n < 5; n++) {
rMix();
}
// XOR decrypted words with previous ciphertext block (CBC mode)
for (let i = 0; i < 4; i++) {
out[offset + i * 2] = (R[i] & 0xff) ^ prev[i * 2];
out[offset + i * 2 + 1] = (R[i] >> 8) ^ prev[i * 2 + 1];
}
// Advance CBC state to current ciphertext block
ct.copy(prev);
}
// Validate and strip PKCS#7 padding.
// Without this guard, padLen = 0 silently returns the full buffer (no stripping)
// and padLen > 8 causes Buffer.subarray(0, negative) → empty buffer, both
// silently. A wrong key/IV produces decrypted bytes that fail this check.
const padLen = out[out.length - 1];
if (padLen < 1 || padLen > 8) {
throw new Error(`rc2CbcDecrypt: invalid PKCS#7 pad byte 0x${padLen.toString(16).padStart(2, "0")} — the ciphertext is corrupt or the wrong key/IV was used`);
}
for (let i = out.length - padLen; i < out.length; i++) {
if (out[i] !== padLen) {
throw new Error(`rc2CbcDecrypt: invalid PKCS#7 padding — the ciphertext is corrupt or the wrong key/IV was used`);
}
}
return out.subarray(0, out.length - padLen);
}
/** PKCS#12 legacy PBE OIDs (pkcs-12PbeIds) and their cipher parameters. */
const PKCS12_PBE_ALGOS = {
"1.2.840.113549.1.12.1.3": { cipher: "des-ede3-cbc", keyLen: 24, ivLen: 8 }, // pbeWithSHAAnd3KeyTripleDESCBC
"1.2.840.113549.1.12.1.4": { cipher: "des-ede-cbc", keyLen: 16, ivLen: 8 }, // pbeWithSHAAnd2KeyTripleDESCBC
"1.2.840.113549.1.12.1.5": { cipher: "rc2-cbc", keyLen: 16, ivLen: 8, rc2Bits: 128 }, // pbeWithSHAAnd128BitRC2CBC
"1.2.840.113549.1.12.1.6": { cipher: "rc2-cbc", keyLen: 5, ivLen: 8, rc2Bits: 40 }, // pbeWithSHAAnd40BitRC2CBC
};
/**
* Maximum PKCS#12 PBE iteration count accepted.
*
* A maliciously crafted PFX can set iterations to Number.MAX_SAFE_INTEGER,
* causing the SHA-1 loop to run for an arbitrary amount of time (DoS).
* Real-world PFX files use 1 00050 000 iterations; this cap is generous
* enough to accommodate even unusually high-security certs while blocking
* obviously hostile inputs.
*/
const MAX_PKCS12_PBE_ITERATIONS = 300000;
/**
* PKCS#12 key/IV derivation function from RFC 7292 Appendix B (SHA-1 variant).
* id = 1 to derive a key, id = 2 to derive an IV.
*
* Throws if `iterations` is outside [1, MAX_PKCS12_PBE_ITERATIONS] to prevent
* CPU exhaustion from a crafted PFX file.
*/
function pkcs12PbeDeriveKey(password, salt, iterations, id, length) {
if (!Number.isInteger(iterations) || iterations < 1 || iterations > MAX_PKCS12_PBE_ITERATIONS) {
throw new Error(`PKCS#12 PBE iteration count ${iterations} is outside safe range [1, ${MAX_PKCS12_PBE_ITERATIONS}]; refusing to process — the file may be crafted to exhaust CPU`);
}
// A crafted PFX could supply a multi-megabyte salt, causing Buffer.alloc(sLen) inside
// the KDF to allocate gigabytes (sLen = ceil(salt.length / 64) * 64).
const MAX_SALT_BYTES = 4096;
if (salt.length > MAX_SALT_BYTES) {
throw new Error(`PKCS#12 PBE salt length ${salt.length} exceeds the safe maximum of ${MAX_SALT_BYTES} bytes — the file may be crafted to exhaust memory`);
}
const u = 20; // SHA-1 output bytes
const v = 64; // SHA-1 block bytes
// Step 1: D = ID byte repeated v times
const D = Buffer.alloc(v, id);
// Step 2: S = salt bytes repeated to fill ceil(salt.length / v) * v bytes
const sLen = salt.length > 0 ? Math.ceil(salt.length / v) * v : 0;
const S = Buffer.alloc(sLen);
for (let i = 0; i < sLen; i++) {
S[i] = salt[i % salt.length];
}
// Step 3: P = password bytes repeated to fill ceil(password.length / v) * v bytes
const pLen = password.length > 0 ? Math.ceil(password.length / v) * v : 0;
const P = Buffer.alloc(pLen);
for (let i = 0; i < pLen; i++) {
P[i] = password[i % password.length];
}
// Step 4: I = S || P (mutable, updated in step 6C)
const I = new Uint8Array(Buffer.concat([S, P]));
const c = Math.ceil(length / u);
const result = Buffer.alloc(c * u);
for (let i = 0; i < c; i++) {
// Step 6A: A_i = H^iterations(D || I)
let Ai = (0, crypto_1.createHash)("sha1").update(D).update(Buffer.from(I)).digest();
for (let j = 1; j < iterations; j++) {
Ai = (0, crypto_1.createHash)("sha1").update(Ai).digest();
}
Ai.copy(result, i * u);
// Step 6B: B = A_i repeated to fill v bytes
const B = new Uint8Array(v);
for (let j = 0; j < v; j++) {
B[j] = Ai[j % u];
}
// Step 6C: each v-byte block of I is incremented by (B + 1) mod 2^(v*8)
const blockCount = Math.ceil(I.length / v);
for (let j = 0; j < blockCount; j++) {
let carry = 1;
for (let b = v - 1; b >= 0; b--) {
const idx = j * v + b;
if (idx < I.length) {
const sum = I[idx] + B[b] + carry;
I[idx] = sum & 0xff;
carry = sum >> 8;
}
}
}
}
return result.subarray(0, length);
}
/**
* Encode a password as UTF-16 Big Endian with a null terminator, which is the
* format required by PKCS#12 PBE key derivation (RFC 7292).
* An empty string yields [0x00, 0x00] (just the null terminator).
*/
function pkcs12PasswordToUtf16(password) {
const buf = Buffer.alloc((password.length + 1) * 2);
for (let i = 0; i < password.length; i++) {
const code = password.charCodeAt(i);
buf[i * 2] = (code >> 8) & 0xff;
buf[i * 2 + 1] = code & 0xff;
}
// Last two bytes are already 0x00 0x00 (null terminator) from Buffer.alloc.
return buf;
}
/**
* Decrypt PKCS#12 legacy PBE encrypted content using Node.js crypto.
*
* @param algId - OID from PKCS12_PBE_ALGOS
* @param algParams - asn1js object for PKCS12PBEParams (SEQUENCE { OCTET STRING, INTEGER })
* @param encContent - asn1js object for the encrypted content (Constructed or Primitive [0] IMPLICIT)
* @param password - plain-text password string
*/
function decryptLegacyPkcs12Pbe(algId, algParams, encContent, password) {
var _a;
const algo = PKCS12_PBE_ALGOS[algId];
// PKCS12PBEParams ::= SEQUENCE { salt OCTET STRING, iterations INTEGER }
const salt = Buffer.from(algParams.valueBlock.value[0].valueBlock.valueHexView);
const iterations = Number(algParams.valueBlock.value[1].valueBlock.valueDec);
const pwdBytes = pkcs12PasswordToUtf16(password);
const key = pkcs12PbeDeriveKey(pwdBytes, salt, iterations, 1, algo.keyLen);
const iv = pkcs12PbeDeriveKey(pwdBytes, salt, iterations, 2, algo.ivLen);
// Extract raw encrypted bytes from the [0] IMPLICIT OCTET STRING.
// The encryptedContent field can be a Constructed (fragmented) or Primitive tag.
let encBytes;
if ((_a = encContent.idBlock) === null || _a === void 0 ? void 0 : _a.isConstructed) {
encBytes = Buffer.concat(encContent.valueBlock.value.map((p) => Buffer.from(p.valueBlock.valueHexView)));
}
else {
encBytes = Buffer.from(encContent.valueBlock.valueHexView);
}
if (algo.rc2Bits != null) {
// RC2 is not available in Node.js 22+ via createDecipheriv (OpenSSL 3 moved
// it to the legacy provider which is not loaded by default). Use our pure-TS
// RFC 2268 implementation instead.
return rc2CbcDecrypt(key, iv, encBytes, algo.rc2Bits);
}
const decipher = (0, crypto_1.createDecipheriv)(algo.cipher, key, iv);
return Buffer.concat([decipher.update(encBytes), decipher.final()]);
}
/**
* Reads certificate info from a PKCS#12 (.pfx) file using pkijs (unobfuscated TypeScript).
* Mirrors the `certificate-info` subcommand of app-builder-bin.
* https://github.com/develar/app-builder/blob/master/pkg/codesign/p12.go
*
* Returns { commonName, bloodyMicrosoftSubjectDn } on success.
*
* Known divergences from the Go binary:
* - No OpenSSL fallback when the pure PKCS#12 decoder fails for a non-password reason.
* - Unknown OIDs are rendered using the raw numeric OID as the type name (e.g. `2.5.4.100=value`); Go uses `OID=#hexbytes` when ASN.1 marshal succeeds.
* - RDN ordering uses DER order; Go normalizes via pkix.Name.ToRDNSequence then reverses via
* BloodyMsString. These coincide for pkijs-generated certs (CN-first DER) but may
* differ for real CA-issued certs stored in traditional C-first DER order.
*/
async function readCertInfo(file, password) {
var _a, _b;
const pfxDer = await (0, fs_extra_1.readFile)(file);
// asn1js requires a plain ArrayBuffer
const pfxBuf = toArrayBuffer(pfxDer);
let asn1;
try {
asn1 = asn1js.fromBER(pfxBuf);
if (asn1.offset === -1) {
throw new Error("offset -1: invalid BER encoding");
}
}
catch (err) {
throw new Error(`PKCS#12 file "${file}" contains invalid ASN.1/DER data: ${err instanceof Error ? err.message : String(err)}`);
}
const pfx = new pkijs.PFX({ schema: asn1.result });
const pwBuf = toArrayBuffer(Buffer.from(password, "utf-8"));
// Step 1: Verify MAC (or signature) integrity and parse the AuthenticatedSafe container.
// pkijs throws "Integrity for the PKCS#12 data is broken!" on wrong password.
try {
await pfx.parseInternalValues({ password: pwBuf, checkIntegrity: true });
}
catch (err) {
const detail = err instanceof Error ? err.message : String(err);
const lower = detail.toLowerCase();
if (lower.includes("integrity") || lower.includes("mac") || lower.includes("password") || lower.includes("pkcs#12")) {
throw new Error(`password incorrect for certificate file "${file}" — verify the password matches the PFX. pkijs detail: ${detail}`);
}
builder_util_1.log.debug({ file, error: detail }, "pkijs failed to decode PKCS#12; no OpenSSL fallback available in Node.js");
throw new Error(`Failed to decode PKCS#12 file "${file}" — the file may be corrupt, use an unsupported cipher, or require OpenSSL. pkijs detail: ${detail}`);
}
const authSafe = (_a = pfx.parsedValue) === null || _a === void 0 ? void 0 : _a.authenticatedSafe;
if (authSafe == null) {
throw new Error(`Failed to parse AuthenticatedSafe in PKCS#12 file "${file}"`);
}
// Step 2: Iterate over the authenticated-safe ContentInfos and extract all certificates.
//
// We do NOT call authSafe.parseInternalValues() because pkijs's CryptoEngine only handles
// PBES2 (OID 1.2.840.113549.1.5.13) for EncryptedData. Real-world PFX files (including
// CA-issued and electron-builder-generated certs) often use the older pkcs-12PbeIds ciphers
// (e.g. pbeWithSHAAnd3KeyTripleDESCBC). We handle those with our own Node.js crypto path.
const certs = [];
for (const ci of authSafe.safeContents) {
let safeBER;
if (ci.contentType === pkijs.id_ContentType_Data) {
// Data: content is an OCTET STRING containing DER-encoded SafeContents.
safeBER = ci.content.getValue();
}
else if (ci.contentType === pkijs.id_ContentType_EncryptedData) {
// EncryptedData: decrypt the SafeContents, routing by algorithm OID.
const encData = new pkijs.EncryptedData({ schema: ci.content });
const algId = encData.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId;
if (Object.prototype.hasOwnProperty.call(PKCS12_PBE_ALGOS, algId)) {
// Legacy PKCS#12 PBE (pbeWithSHAAnd*) — use our RFC 7292 implementation.
const decrypted = decryptLegacyPkcs12Pbe(algId, encData.encryptedContentInfo.contentEncryptionAlgorithm.algorithmParams, encData.encryptedContentInfo.encryptedContent, password);
safeBER = toArrayBuffer(decrypted);
}
else {
// PBES2 or other modern OID — delegate to the pkijs engine.
safeBER = await encData.decrypt({ password: pwBuf });
}
}
else {
// EnvelopedData or other types — skip (not needed for cert extraction).
continue;
}
// Parse SafeContents and collect X.509 certificates from cert bags.
const sc = pkijs.SafeContents.fromBER(safeBER);
for (const bag of sc.safeBags) {
if (bag.bagId === CERT_BAG_OID) {
const certBag = bag.bagValue;
if (certBag.parsedValue instanceof pkijs.Certificate) {
certs.push(certBag.parsedValue);
}
}
}
}
if (certs.length === 0) {
throw new Error(`No certificates found in PKCS#12 file "${file}" — the file may be a key-only PFX or be empty`);
}
// Find the certificate with the codeSigning Extended Key Usage.
// pkijs auto-parses known extensions via ExtensionValueFactory (registered at module init).
const signingCert = certs.find(cert => {
var _a;
const ekuExt = (_a = cert.extensions) === null || _a === void 0 ? void 0 : _a.find(e => e.extnID === pkijs.id_ExtKeyUsage);
if (ekuExt == null) {
return false;
}
if (ekuExt.parsedValue instanceof pkijs.ExtKeyUsage) {
return ekuExt.parsedValue.keyPurposes.includes(CODE_SIGNING_OID);
}
// Fallback: manually parse the extension OctetString value.
try {
const inner = asn1js.fromBER(ekuExt.extnValue.valueBlock.valueHexView);
if (inner.offset === -1) {
return false;
}
const eku = new pkijs.ExtKeyUsage({ schema: inner.result });
return eku.keyPurposes.includes(CODE_SIGNING_OID);
}
catch {
return false;
}
});
if (signingCert == null) {
throw new Error(`No certificate with ExtKeyUsageCodeSigning found in "${file}" — ${certs.length} certificate(s) present but none have the codeSigning extended key usage. ` +
`Ensure the PFX contains a code-signing certificate, not just a CA or TLS certificate.`);
}
const cnAttr = signingCert.subject.typesAndValues.find(a => a.type === "2.5.4.3");
const commonName = String((_b = cnAttr === null || cnAttr === void 0 ? void 0 : cnAttr.value.valueBlock.value) !== null && _b !== void 0 ? _b : "");
// Format DN as "CN=X,O=X,..." matching Go's BloodyMsString output.
// Uses ATTRIBUTE_TYPE_NAMES to mirror Go's attributeTypeNames map exactly, so STREET,
// POSTALCODE, and SERIALNUMBER (present in EV code-signing certs) are handled correctly.
// Unknown OIDs fall back to the bare OID string as the type name.
const bloodyMicrosoftSubjectDn = signingCert.subject.typesAndValues
.map(a => {
var _a;
const typeName = (_a = ATTRIBUTE_TYPE_NAMES[a.type]) !== null && _a !== void 0 ? _a : a.type;
return `${typeName}=${escapeDnValue(String(a.value.valueBlock.value))}`;
})
.join(",");
return { commonName, bloodyMicrosoftSubjectDn };
}
/**
* Internal functions exported exclusively for unit testing.
* Not part of the public API do not use outside of test files.
*/
exports._testingOnly = {
pkcs12PbeDeriveKey,
pkcs12PasswordToUtf16,
rc2CbcDecrypt,
MAX_PKCS12_PBE_ITERATIONS,
};
//# sourceMappingURL=certInfo.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,42 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.importCertificate = void 0;
const fs_extra_1 = require("fs-extra");
const os_1 = require("os");
const path = require("path");
exports.importCertificate = importCertificate;
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const fs_extra_1 = require("fs-extra");
const binDownload_1 = require("../binDownload");
/** @private */
async function importCertificate(cscLink, tmpDir, currentDir) {
var _a, _b;
cscLink = cscLink.trim();
let file = null;
if ((cscLink.length > 3 && cscLink[1] === ":") || cscLink.startsWith("/") || cscLink.startsWith(".")) {
file = cscLink;
}
else if (cscLink.startsWith("file://")) {
file = cscLink.substring("file://".length);
}
else if (cscLink.startsWith("~/")) {
file = path.join(os_1.homedir(), cscLink.substring("~/".length));
}
else if (cscLink.startsWith("https://")) {
if (cscLink.startsWith("https://")) {
const tempFile = await tmpDir.getTempFile({ suffix: ".p12" });
await binDownload_1.download(cscLink, tempFile);
await (0, binDownload_1.download)(cscLink, tempFile);
return tempFile;
}
else {
const mimeType = (_a = /data:.*;base64,/.exec(cscLink)) === null || _a === void 0 ? void 0 : _a[0];
if (mimeType || cscLink.length > 2048 || cscLink.endsWith("=")) {
const tempFile = await tmpDir.getTempFile({ suffix: ".p12" });
await fs_extra_1.outputFile(tempFile, Buffer.from(cscLink.substring((_b = mimeType === null || mimeType === void 0 ? void 0 : mimeType.length) !== null && _b !== void 0 ? _b : 0), "base64"));
return tempFile;
}
file = cscLink;
const decoded = (0, builder_util_1.decodeCscLinkBase64)(cscLink);
if (decoded) {
const tempFile = await tmpDir.getTempFile({ suffix: ".p12" });
await (0, fs_extra_1.outputFile)(tempFile, decoded);
return tempFile;
}
file = path.resolve(currentDir, file);
const stat = await fs_1.statOrNull(file);
const file = (0, builder_util_1.resolveCscLinkPath)(cscLink, currentDir);
const stat = await (0, builder_util_1.statOrNull)(file);
if (stat == null) {
throw new builder_util_1.InvalidConfigurationError(`${file} doesn't exist`);
}
@ -47,5 +30,4 @@ async function importCertificate(cscLink, tmpDir, currentDir) {
return file;
}
}
exports.importCertificate = importCertificate;
//# sourceMappingURL=codesign.js.map

View file

@ -1 +1 @@
{"version":3,"file":"codesign.js","sourceRoot":"","sources":["../../src/codeSign/codesign.ts"],"names":[],"mappings":";;;AAAA,uCAAqC;AACrC,2BAA4B;AAC5B,6BAA4B;AAE5B,+CAAwD;AACxD,4CAAgD;AAChD,gDAAyC;AAEzC,eAAe;AACR,KAAK,UAAU,iBAAiB,CAAC,OAAe,EAAE,MAAc,EAAE,UAAkB;;IACzF,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IAExB,IAAI,IAAI,GAAkB,IAAI,CAAA;IAC9B,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;QACpG,IAAI,GAAG,OAAO,CAAA;KACf;SAAM,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;QACxC,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;KAC3C;SAAM,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE;QACnC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,YAAO,EAAE,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;KAC5D;SAAM,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;QACzC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QAC7D,MAAM,sBAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QACjC,OAAO,QAAQ,CAAA;KAChB;SAAM;QACL,MAAM,QAAQ,GAAG,MAAA,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,0CAAG,CAAC,CAAC,CAAA;QACrD,IAAI,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;YAC9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;YAC7D,MAAM,qBAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,MAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,MAAM,mCAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAA;YAC3F,OAAO,QAAQ,CAAA;SAChB;QACD,IAAI,GAAG,OAAO,CAAA;KACf;IAED,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;IACrC,MAAM,IAAI,GAAG,MAAM,eAAU,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,IAAI,IAAI,IAAI,EAAE;QAChB,MAAM,IAAI,wCAAyB,CAAC,GAAG,IAAI,gBAAgB,CAAC,CAAA;KAC7D;SAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;QACzB,MAAM,IAAI,wCAAyB,CAAC,GAAG,IAAI,aAAa,CAAC,CAAA;KAC1D;SAAM;QACL,OAAO,IAAI,CAAA;KACZ;AACH,CAAC;AAjCD,8CAiCC","sourcesContent":["import { outputFile } from \"fs-extra\"\nimport { homedir } from \"os\"\nimport * as path from \"path\"\nimport { TmpDir } from \"temp-file\"\nimport { InvalidConfigurationError } from \"builder-util\"\nimport { statOrNull } from \"builder-util/out/fs\"\nimport { download } from \"../binDownload\"\n\n/** @private */\nexport async function importCertificate(cscLink: string, tmpDir: TmpDir, currentDir: string): Promise<string> {\n cscLink = cscLink.trim()\n\n let file: string | null = null\n if ((cscLink.length > 3 && cscLink[1] === \":\") || cscLink.startsWith(\"/\") || cscLink.startsWith(\".\")) {\n file = cscLink\n } else if (cscLink.startsWith(\"file://\")) {\n file = cscLink.substring(\"file://\".length)\n } else if (cscLink.startsWith(\"~/\")) {\n file = path.join(homedir(), cscLink.substring(\"~/\".length))\n } else if (cscLink.startsWith(\"https://\")) {\n const tempFile = await tmpDir.getTempFile({ suffix: \".p12\" })\n await download(cscLink, tempFile)\n return tempFile\n } else {\n const mimeType = /data:.*;base64,/.exec(cscLink)?.[0]\n if (mimeType || cscLink.length > 2048 || cscLink.endsWith(\"=\")) {\n const tempFile = await tmpDir.getTempFile({ suffix: \".p12\" })\n await outputFile(tempFile, Buffer.from(cscLink.substring(mimeType?.length ?? 0), \"base64\"))\n return tempFile\n }\n file = cscLink\n }\n\n file = path.resolve(currentDir, file)\n const stat = await statOrNull(file)\n if (stat == null) {\n throw new InvalidConfigurationError(`${file} doesn't exist`)\n } else if (!stat.isFile()) {\n throw new InvalidConfigurationError(`${file} not a file`)\n } else {\n return file\n }\n}\n"]}
{"version":3,"file":"codesign.js","sourceRoot":"","sources":["../../src/codeSign/codesign.ts"],"names":[],"mappings":";;AAMA,8CAyBC;AA/BD,+CAA6G;AAC7G,uCAAqC;AAErC,gDAAyC;AAEzC,eAAe;AACR,KAAK,UAAU,iBAAiB,CAAC,OAAe,EAAE,MAAc,EAAE,UAAkB;IACzF,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;IAExB,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QAC7D,MAAM,IAAA,sBAAQ,EAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;QACjC,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,kCAAmB,EAAC,OAAO,CAAC,CAAA;IAC5C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QAC7D,MAAM,IAAA,qBAAU,EAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QACnC,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,MAAM,IAAI,GAAG,IAAA,iCAAkB,EAAC,OAAO,EAAE,UAAU,CAAC,CAAA;IACpD,MAAM,IAAI,GAAG,MAAM,IAAA,yBAAU,EAAC,IAAI,CAAC,CAAA;IACnC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,wCAAyB,CAAC,GAAG,IAAI,gBAAgB,CAAC,CAAA;IAC9D,CAAC;SAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1B,MAAM,IAAI,wCAAyB,CAAC,GAAG,IAAI,aAAa,CAAC,CAAA;IAC3D,CAAC;SAAM,CAAC;QACN,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC","sourcesContent":["import { decodeCscLinkBase64, InvalidConfigurationError, resolveCscLinkPath, statOrNull } from \"builder-util\"\nimport { outputFile } from \"fs-extra\"\nimport { TmpDir } from \"temp-file\"\nimport { download } from \"../binDownload\"\n\n/** @private */\nexport async function importCertificate(cscLink: string, tmpDir: TmpDir, currentDir: string): Promise<string> {\n cscLink = cscLink.trim()\n\n if (cscLink.startsWith(\"https://\")) {\n const tempFile = await tmpDir.getTempFile({ suffix: \".p12\" })\n await download(cscLink, tempFile)\n return tempFile\n }\n\n const decoded = decodeCscLinkBase64(cscLink)\n if (decoded) {\n const tempFile = await tmpDir.getTempFile({ suffix: \".p12\" })\n await outputFile(tempFile, decoded)\n return tempFile\n }\n\n const file = resolveCscLinkPath(cscLink, currentDir)\n const stat = await statOrNull(file)\n if (stat == null) {\n throw new InvalidConfigurationError(`${file} doesn't exist`)\n } else if (!stat.isFile()) {\n throw new InvalidConfigurationError(`${file} not a file`)\n } else {\n return file\n }\n}\n"]}

View file

@ -1,11 +1,13 @@
import { TmpDir } from "builder-util/out/util";
import type { SignOptions } from "@electron/osx-sign/dist/cjs/types";
import { TmpDir } from "builder-util";
import { Nullish } from "builder-util-runtime";
export declare const appleCertificatePrefixes: string[];
export declare type CertType = "Developer ID Application" | "Developer ID Installer" | "3rd Party Mac Developer Application" | "3rd Party Mac Developer Installer" | "Mac Developer" | "Apple Development" | "Apple Distribution";
export type CertType = "Developer ID Application" | "Developer ID Installer" | "3rd Party Mac Developer Application" | "3rd Party Mac Developer Installer" | "Mac Developer" | "Apple Development" | "Apple Distribution";
export interface CodeSigningInfo {
keychainFile?: string | null;
}
export declare function isSignAllowed(isPrintWarn?: boolean): boolean;
export declare function reportError(isMas: boolean, certificateTypes: CertType[], qualifier: string | null | undefined, keychainFile: string | null | undefined, isForceCodeSigning: boolean): Promise<void>;
export declare function reportError(isMas: boolean, certificateTypes: CertType[], qualifier: string | Nullish, keychainFile: string | Nullish, isForceCodeSigning: boolean): Promise<void>;
export interface CreateKeychainOptions {
tmpDir: TmpDir;
cscLink: string;
@ -16,12 +18,11 @@ export interface CreateKeychainOptions {
}
export declare function removeKeychain(keychainFile: string, printWarn?: boolean): Promise<any>;
export declare function createKeychain({ tmpDir, cscLink, cscKeyPassword, cscILink, cscIKeyPassword, currentDir }: CreateKeychainOptions): Promise<CodeSigningInfo>;
/** @private */
export declare function sign(path: string, name: string, keychain: string): Promise<any>;
export declare function sign(opts: SignOptions): Promise<void>;
export declare let findIdentityRawResult: Promise<Array<string>> | null;
export declare class Identity {
readonly name: string;
readonly hash: string;
constructor(name: string, hash: string);
readonly hash?: string;
constructor(name: string, hash?: string);
}
export declare function findIdentity(certType: CertType, qualifier?: string | null, keychain?: string | null): Promise<Identity | null>;

View file

@ -1,10 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findIdentity = exports.findIdentityRawResult = exports.sign = exports.createKeychain = exports.removeKeychain = exports.reportError = exports.isSignAllowed = exports.appleCertificatePrefixes = void 0;
const bluebird_lst_1 = require("bluebird-lst");
const util_1 = require("builder-util/out/util");
const fs_1 = require("builder-util/out/fs");
const log_1 = require("builder-util/out/log");
exports.findIdentityRawResult = exports.appleCertificatePrefixes = void 0;
exports.isSignAllowed = isSignAllowed;
exports.reportError = reportError;
exports.removeKeychain = removeKeychain;
exports.createKeychain = createKeychain;
exports.sign = sign;
exports.findIdentity = findIdentity;
const builder_util_1 = require("builder-util");
const dynamicImport_1 = require("../util/dynamicImport");
const crypto_1 = require("crypto");
const promises_1 = require("fs/promises");
const lazy_val_1 = require("lazy-val");
@ -17,38 +21,37 @@ exports.appleCertificatePrefixes = ["Developer ID Application:", "Developer ID I
function isSignAllowed(isPrintWarn = true) {
if (process.platform !== "darwin") {
if (isPrintWarn) {
util_1.log.warn({ reason: "supported only on macOS" }, "skipped macOS application code signing");
builder_util_1.log.warn({ reason: "supported only on macOS" }, "skipped macOS application code signing");
}
return false;
}
const buildForPrWarning = "There are serious security concerns with CSC_FOR_PULL_REQUEST=true (see the CircleCI documentation (https://circleci.com/docs/1.0/fork-pr-builds/) for details)" +
"\nIf you have SSH keys, sensitive env vars or AWS credentials stored in your project settings and untrusted forks can make pull requests against your repo, then this option isn't for you.";
if (util_1.isPullRequest()) {
if (util_1.isEnvTrue(process.env.CSC_FOR_PULL_REQUEST)) {
if ((0, builder_util_1.isPullRequest)()) {
const buildForPrWarning = "There are serious security concerns with CSC_FOR_PULL_REQUEST=true (see the CircleCI documentation (https://circleci.com/docs/1.0/fork-pr-builds/) for details)" +
"\nIf you have SSH keys, sensitive env vars or AWS credentials stored in your project settings and untrusted forks can make pull requests against your repo, then this option isn't for you.";
if ((0, builder_util_1.isEnvTrue)(process.env.CSC_FOR_PULL_REQUEST)) {
if (isPrintWarn) {
util_1.log.warn(buildForPrWarning);
builder_util_1.log.warn(buildForPrWarning);
}
}
else {
if (isPrintWarn) {
// https://github.com/electron-userland/electron-builder/issues/1524
util_1.log.warn("Current build is a part of pull request, code signing will be skipped." + "\nSet env CSC_FOR_PULL_REQUEST to true to force code signing." + `\n${buildForPrWarning}`);
builder_util_1.log.warn("Current build is a part of pull request, code signing will be skipped." + "\nSet env CSC_FOR_PULL_REQUEST to true to force code signing." + `\n${buildForPrWarning}`);
}
return false;
}
}
return true;
}
exports.isSignAllowed = isSignAllowed;
async function reportError(isMas, certificateTypes, qualifier, keychainFile, isForceCodeSigning) {
const logFields = {};
if (qualifier == null) {
logFields.reason = "";
if (flags_1.isAutoDiscoveryCodeSignIdentity()) {
if ((0, flags_1.isAutoDiscoveryCodeSignIdentity)()) {
logFields.reason += `cannot find valid "${certificateTypes.join(", ")}" identity${isMas ? "" : ` or custom non-Apple code signing certificate, it could cause some undefined behaviour, e.g. macOS localized description not visible`}`;
}
logFields.reason += ", see https://electron.build/code-signing";
if (!flags_1.isAutoDiscoveryCodeSignIdentity()) {
if (!(0, flags_1.isAutoDiscoveryCodeSignIdentity)()) {
logFields.CSC_IDENTITY_AUTO_DISCOVERY = false;
}
}
@ -60,21 +63,21 @@ async function reportError(isMas, certificateTypes, qualifier, keychainFile, isF
if (keychainFile != null) {
args.push(keychainFile);
}
if (qualifier != null || flags_1.isAutoDiscoveryCodeSignIdentity()) {
logFields.allIdentities = (await util_1.exec("security", args))
if (qualifier != null || (0, flags_1.isAutoDiscoveryCodeSignIdentity)()) {
logFields.allIdentities = (await (0, builder_util_1.exec)("/usr/bin/security", args))
.trim()
.split("\n")
.filter(it => !(it.includes("Policy: X.509 Basic") || it.includes("Matching identities")))
.join("\n");
}
const skipMessage = "skipped macOS application code signing";
if (isMas || isForceCodeSigning) {
throw new Error(log_1.Logger.createMessage("skipped macOS application code signing", logFields, "error", it => it));
throw new Error(builder_util_1.Logger.createMessage(skipMessage, logFields, "error", it => it));
}
else {
util_1.log.warn(logFields, "skipped macOS application code signing");
builder_util_1.log.warn(logFields, skipMessage);
}
}
exports.reportError = reportError;
// "Note that filename will not be searched to resolve the signing identity's certificate chain unless it is also on the user's keychain search list."
// but "security list-keychains" doesn't support add - we should 1) get current list 2) set new list - it is very bad http://stackoverflow.com/questions/10538942/add-a-keychain-to-search-list
// "overly complicated and introduces a race condition."
@ -82,23 +85,23 @@ exports.reportError = reportError;
const bundledCertKeychainAdded = new lazy_val_1.Lazy(async () => {
// copy to temp and then atomic rename to final path
const cacheDir = getCacheDirectory();
const tmpKeychainPath = path.join(cacheDir, temp_file_1.getTempName("electron-builder-root-certs"));
const tmpKeychainPath = path.join(cacheDir, (0, temp_file_1.getTempName)("electron-builder-root-certs"));
const keychainPath = path.join(cacheDir, "electron-builder-root-certs.keychain");
const results = await Promise.all([
listUserKeychains(),
fs_1.copyFile(path.join(__dirname, "..", "..", "certs", "root_certs.keychain"), tmpKeychainPath).then(() => promises_1.rename(tmpKeychainPath, keychainPath)),
(0, builder_util_1.copyFile)(path.join(__dirname, "..", "..", "certs", "root_certs.keychain"), tmpKeychainPath).then(() => (0, promises_1.rename)(tmpKeychainPath, keychainPath)),
]);
const list = results[0];
if (!list.includes(keychainPath)) {
await util_1.exec("security", ["list-keychains", "-d", "user", "-s", keychainPath].concat(list));
await (0, builder_util_1.exec)("/usr/bin/security", ["list-keychains", "-d", "user", "-s", keychainPath].concat(list));
}
});
function getCacheDirectory() {
const env = process.env.ELECTRON_BUILDER_CACHE;
return util_1.isEmptyOrSpaces(env) ? path.join(os_1.homedir(), "Library", "Caches", "electron-builder") : path.resolve(env);
return (0, builder_util_1.isEmptyOrSpaces)(env) ? path.join((0, os_1.homedir)(), "Library", "Caches", "electron-builder") : path.resolve(env);
}
function listUserKeychains() {
return util_1.exec("security", ["list-keychains", "-d", "user"]).then(it => it
return (0, builder_util_1.exec)("/usr/bin/security", ["list-keychains", "-d", "user"]).then(it => it
.split("\n")
.map(it => {
const r = it.trim();
@ -107,14 +110,13 @@ function listUserKeychains() {
.filter(it => it.length > 0));
}
function removeKeychain(keychainFile, printWarn = true) {
return util_1.exec("security", ["delete-keychain", keychainFile]).catch(e => {
return (0, builder_util_1.exec)("/usr/bin/security", ["delete-keychain", keychainFile]).catch((e) => {
if (printWarn) {
util_1.log.warn({ file: keychainFile, error: e.stack || e }, "cannot delete keychain");
builder_util_1.log.warn({ file: keychainFile, error: e.stack || e }, "cannot delete keychain");
}
return fs_1.unlinkIfExists(keychainFile);
return (0, builder_util_1.unlinkIfExists)(keychainFile);
});
}
exports.removeKeychain = removeKeychain;
async function createKeychain({ tmpDir, cscLink, cscKeyPassword, cscILink, cscIKeyPassword, currentDir }) {
// travis has correct AppleWWDRCA cert
if (process.env.TRAVIS !== "true") {
@ -122,9 +124,8 @@ async function createKeychain({ tmpDir, cscLink, cscKeyPassword, cscILink, cscIK
}
// https://github.com/electron-userland/electron-builder/issues/3685
// use constant file
const keychainFile = path.join(process.env.APP_BUILDER_TMP_DIR || os_1.tmpdir(), `${crypto_1.createHash("sha256").update(currentDir).update("app-builder").digest("hex")}.keychain`);
const keychainFile = path.join(process.env.APP_BUILDER_TMP_DIR || (0, os_1.tmpdir)(), `${(0, crypto_1.createHash)("sha256").update(currentDir).update("app-builder").digest("hex")}.keychain`);
// noinspection JSUnusedLocalSymbols
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await removeKeychain(keychainFile, false).catch(_ => {
/* ignore*/
});
@ -133,7 +134,7 @@ async function createKeychain({ tmpDir, cscLink, cscKeyPassword, cscILink, cscIK
certLinks.push(cscILink);
}
const certPaths = new Array(certLinks.length);
const keychainPassword = crypto_1.randomBytes(32).toString("base64");
const keychainPassword = (0, crypto_1.randomBytes)(32).toString("base64");
const securityCommands = [
["create-keychain", "-p", keychainPassword, keychainFile],
["unlock-keychain", "-p", keychainPassword, keychainFile],
@ -147,33 +148,37 @@ async function createKeychain({ tmpDir, cscLink, cscKeyPassword, cscILink, cscIK
}
await Promise.all([
// we do not clear downloaded files - will be removed on tmpDir cleanup automatically. not a security issue since in any case data is available as env variables and protected by password.
bluebird_lst_1.default.map(certLinks, (link, i) => codesign_1.importCertificate(link, tmpDir, currentDir).then(it => (certPaths[i] = it))),
bluebird_lst_1.default.mapSeries(securityCommands, it => util_1.exec("security", it)),
...certLinks.map((link, i) => (0, codesign_1.importCertificate)(link, tmpDir, currentDir).then(it => (certPaths[i] = it))),
// queue each security command
securityCommands.reduce((promise, cmd) => promise.then(() => (0, builder_util_1.exec)("/usr/bin/security", cmd)), new Promise(resolve => resolve(null))),
]);
return await importCerts(keychainFile, certPaths, [cscKeyPassword, cscIKeyPassword].filter(it => it != null));
const cscPasswords = [cscKeyPassword];
if (cscIKeyPassword != null) {
cscPasswords.push(cscIKeyPassword);
}
return await importCerts(keychainFile, certPaths, cscPasswords);
}
exports.createKeychain = createKeychain;
async function importCerts(keychainFile, paths, keyPasswords) {
var _a;
for (let i = 0; i < paths.length; i++) {
const password = keyPasswords[i];
await util_1.exec("security", ["import", paths[i], "-k", keychainFile, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password]);
const password = (_a = keyPasswords[i]) !== null && _a !== void 0 ? _a : "";
await (0, builder_util_1.exec)("/usr/bin/security", ["import", paths[i], "-k", keychainFile, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password]);
// https://stackoverflow.com/questions/39868578/security-codesign-in-sierra-keychain-ignores-access-control-settings-and-ui-p
// https://github.com/electron-userland/electron-packager/issues/701#issuecomment-322315996
await util_1.exec("security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]);
await (0, builder_util_1.exec)("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]);
}
return {
keychainFile,
};
}
/** @private */
function sign(path, name, keychain) {
const args = ["--deep", "--force", "--sign", name, path];
if (keychain != null) {
args.push("--keychain", keychain);
}
return util_1.exec("codesign", args);
async function sign(opts) {
const { signAsync } = await (0, dynamicImport_1.dynamicImport)("@electron/osx-sign");
return (0, builder_util_1.retry)(() => signAsync(opts), {
retries: 3,
interval: 5000,
backoff: 5000,
});
}
exports.sign = sign;
exports.findIdentityRawResult = null;
async function getValidIdentities(keychain) {
function addKeychain(args) {
@ -187,7 +192,7 @@ async function getValidIdentities(keychain) {
// https://github.com/electron-userland/electron-builder/issues/481
// https://github.com/electron-userland/electron-builder/issues/535
result = Promise.all([
util_1.exec("security", addKeychain(["find-identity", "-v"])).then(it => it
(0, builder_util_1.exec)("/usr/bin/security", addKeychain(["find-identity", "-v"])).then(it => it
.trim()
.split("\n")
.filter(it => {
@ -198,7 +203,7 @@ async function getValidIdentities(keychain) {
}
return false;
})),
util_1.exec("security", addKeychain(["find-identity", "-v", "-p", "codesigning"])).then(it => it.trim().split("\n")),
(0, builder_util_1.exec)("/usr/bin/security", addKeychain(["find-identity", "-v", "-p", "codesigning"])).then(it => it.trim().split("\n")),
]).then(it => {
const array = it[0]
.concat(it[1])
@ -223,7 +228,7 @@ async function _findIdentity(type, qualifier, keychain) {
continue;
}
if (line.includes(namePrefix)) {
return parseIdentity(line);
return await parseIdentity(line);
}
}
if (type === "Developer ID Application") {
@ -241,22 +246,22 @@ async function _findIdentity(type, qualifier, keychain) {
continue l;
}
}
return parseIdentity(line);
return await parseIdentity(line);
}
}
return null;
}
const _Identity = require("electron-osx-sign/util-identities").Identity;
function parseIdentity(line) {
async function parseIdentity(line) {
const firstQuoteIndex = line.indexOf('"');
const name = line.substring(firstQuoteIndex + 1, line.lastIndexOf('"'));
const hash = line.substring(0, firstQuoteIndex - 1);
return new _Identity(name, hash);
const { Identity: IdentityClass } = await (0, dynamicImport_1.dynamicImport)("@electron/osx-sign/dist/cjs/util-identities");
return new IdentityClass(name, hash);
}
function findIdentity(certType, qualifier, keychain) {
let identity = qualifier || process.env.CSC_NAME;
if (util_1.isEmptyOrSpaces(identity)) {
if (flags_1.isAutoDiscoveryCodeSignIdentity()) {
if ((0, builder_util_1.isEmptyOrSpaces)(identity)) {
if ((0, flags_1.isAutoDiscoveryCodeSignIdentity)()) {
return _findIdentity(certType, null, keychain);
}
else {
@ -271,10 +276,9 @@ function findIdentity(certType, qualifier, keychain) {
return _findIdentity(certType, identity, keychain);
}
}
exports.findIdentity = findIdentity;
function checkPrefix(name, prefix) {
if (name.startsWith(prefix)) {
throw new util_1.InvalidConfigurationError(`Please remove prefix "${prefix}" from the specified name — appropriate certificate will be chosen automatically`);
throw new builder_util_1.InvalidConfigurationError(`Please remove prefix "${prefix}" from the specified name — appropriate certificate will be chosen automatically`);
}
}
//# sourceMappingURL=macCodeSign.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
import { MemoLazy, Nullish } from "builder-util-runtime";
import { Lazy } from "lazy-val";
import { Target } from "../core";
import { WindowsConfiguration } from "../options/winOptions";
import { WindowsSignOptions } from "./windowsCodeSign";
import { CertificateFromStoreInfo, FileCodeSigningInfo } from "./windowsSignToolManager";
export interface SignManager {
readonly computedPublisherName: Lazy<Array<string> | null>;
readonly cscInfo: MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>;
computePublisherName(target: Target, publisherName: string | Nullish): Promise<string>;
initialize(): Promise<void>;
signFile(options: WindowsSignOptions): Promise<boolean>;
}

View file

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=signManager.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"signManager.js","sourceRoot":"","sources":["../../src/codeSign/signManager.ts"],"names":[],"mappings":"","sourcesContent":["import { MemoLazy, Nullish } from \"builder-util-runtime\"\nimport { Lazy } from \"lazy-val\"\nimport { Target } from \"../core\"\nimport { WindowsConfiguration } from \"../options/winOptions\"\nimport { WindowsSignOptions } from \"./windowsCodeSign\"\nimport { CertificateFromStoreInfo, FileCodeSigningInfo } from \"./windowsSignToolManager\"\n\nexport interface SignManager {\n readonly computedPublisherName: Lazy<Array<string> | null>\n readonly cscInfo: MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>\n computePublisherName(target: Target, publisherName: string | Nullish): Promise<string>\n initialize(): Promise<void>\n signFile(options: WindowsSignOptions): Promise<boolean>\n}\n"]}

View file

@ -1,38 +1,7 @@
import { WindowsConfiguration } from "../options/winOptions";
import { VmManager } from "../vm/vm";
import { WinPackager } from "../winPackager";
export declare function getSignVendorPath(): Promise<string>;
export declare type CustomWindowsSign = (configuration: CustomWindowsSignTaskConfiguration, packager?: WinPackager) => Promise<any>;
export interface WindowsSignOptions {
readonly path: string;
readonly name?: string | null;
readonly cscInfo?: FileCodeSigningInfo | CertificateFromStoreInfo | null;
readonly site?: string | null;
readonly options: WindowsConfiguration;
}
export interface WindowsSignTaskConfiguration extends WindowsSignOptions {
resultOutputPath?: string;
hash: string;
isNest: boolean;
}
export interface CustomWindowsSignTaskConfiguration extends WindowsSignTaskConfiguration {
computeSignToolArgs(isWin: boolean): Array<string>;
}
export declare function sign(options: WindowsSignOptions, packager: WinPackager): Promise<void>;
export interface FileCodeSigningInfo {
readonly file: string;
readonly password: string | null;
}
export declare function getCertInfo(file: string, password: string): Promise<CertificateInfo>;
export interface CertificateInfo {
readonly commonName: string;
readonly bloodyMicrosoftSubjectDn: string;
}
export interface CertificateFromStoreInfo {
thumbprint: string;
subject: string;
store: string;
isLocalMachineStore: boolean;
}
export declare function getCertificateFromStoreInfo(options: WindowsConfiguration, vm: VmManager): Promise<CertificateFromStoreInfo>;
export declare function doSign(configuration: CustomWindowsSignTaskConfiguration, packager: WinPackager): Promise<void>;
export declare function signWindows(options: WindowsSignOptions, packager: WinPackager): Promise<boolean>;

View file

@ -1,251 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isOldWin6 = exports.doSign = exports.getCertificateFromStoreInfo = exports.getCertInfo = exports.sign = exports.getSignVendorPath = void 0;
const util_1 = require("builder-util/out/util");
const binDownload_1 = require("../binDownload");
const appBuilder_1 = require("../util/appBuilder");
const bundledTool_1 = require("../util/bundledTool");
const fs_extra_1 = require("fs-extra");
const os = require("os");
const path = require("path");
const platformPackager_1 = require("../platformPackager");
const flags_1 = require("../util/flags");
const vm_1 = require("../vm/vm");
function getSignVendorPath() {
return binDownload_1.getBin("winCodeSign");
}
exports.getSignVendorPath = getSignVendorPath;
async function sign(options, packager) {
let hashes = options.options.signingHashAlgorithms;
// msi does not support dual-signing
if (options.path.endsWith(".msi")) {
hashes = [hashes != null && !hashes.includes("sha1") ? "sha256" : "sha1"];
}
else if (options.path.endsWith(".appx")) {
hashes = ["sha256"];
}
else if (hashes == null) {
hashes = ["sha1", "sha256"];
exports.signWindows = signWindows;
const builder_util_1 = require("builder-util");
async function signWindows(options, packager) {
if (options.options.azureSignOptions) {
if (options.options.signtoolOptions) {
builder_util_1.log.warn(null, "ignoring signtool options, using Azure Trusted Signing; please only configure one");
}
builder_util_1.log.info({ path: builder_util_1.log.filePath(options.path) }, "signing with Azure Trusted Signing");
}
else {
hashes = Array.isArray(hashes) ? hashes : [hashes];
}
const executor = platformPackager_1.resolveFunction(options.options.sign, "sign") || doSign;
let isNest = false;
for (const hash of hashes) {
const taskConfiguration = { ...options, hash, isNest };
await Promise.resolve(executor({
...taskConfiguration,
computeSignToolArgs: isWin => computeSignToolArgs(taskConfiguration, isWin),
}, packager));
isNest = true;
if (taskConfiguration.resultOutputPath != null) {
await fs_extra_1.rename(taskConfiguration.resultOutputPath, options.path);
}
builder_util_1.log.info({ path: builder_util_1.log.filePath(options.path) }, "signing with signtool.exe");
}
const packageManager = await packager.signingManager.value;
return signWithRetry(async () => packageManager.signFile(options));
}
exports.sign = sign;
async function getCertInfo(file, password) {
let result = null;
const errorMessagePrefix = "Cannot extract publisher name from code signing certificate. As workaround, set win.publisherName. Error: ";
try {
result = await appBuilder_1.executeAppBuilderAsJson(["certificate-info", "--input", file, "--password", password]);
}
catch (e) {
throw new Error(`${errorMessagePrefix}${e.stack || e}`);
}
if (result.error != null) {
// noinspection ExceptionCaughtLocallyJS
throw new util_1.InvalidConfigurationError(`${errorMessagePrefix}${result.error}`);
}
return result;
}
exports.getCertInfo = getCertInfo;
async function getCertificateFromStoreInfo(options, vm) {
const certificateSubjectName = options.certificateSubjectName;
const certificateSha1 = options.certificateSha1 ? options.certificateSha1.toUpperCase() : options.certificateSha1;
// ExcludeProperty doesn't work, so, we cannot exclude RawData, it is ok
// powershell can return object if the only item
const rawResult = await vm.exec("powershell.exe", [
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-ChildItem -Recurse Cert: -CodeSigningCert | Select-Object -Property Subject,PSParentPath,Thumbprint | ConvertTo-Json -Compress",
]);
const certList = rawResult.length === 0 ? [] : util_1.asArray(JSON.parse(rawResult));
for (const certInfo of certList) {
if ((certificateSubjectName != null && !certInfo.Subject.includes(certificateSubjectName)) ||
(certificateSha1 != null && certInfo.Thumbprint.toUpperCase() !== certificateSha1)) {
continue;
}
const parentPath = certInfo.PSParentPath;
const store = parentPath.substring(parentPath.lastIndexOf("\\") + 1);
util_1.log.debug({ store, PSParentPath: parentPath }, "auto-detect certificate store");
// https://github.com/electron-userland/electron-builder/issues/1717
const isLocalMachineStore = parentPath.includes("Certificate::LocalMachine");
util_1.log.debug(null, "auto-detect using of LocalMachine store");
return {
thumbprint: certInfo.Thumbprint,
subject: certInfo.Subject,
store,
isLocalMachineStore,
};
}
throw new Error(`Cannot find certificate ${certificateSubjectName || certificateSha1}, all certs: ${rawResult}`);
}
exports.getCertificateFromStoreInfo = getCertificateFromStoreInfo;
async function doSign(configuration, packager) {
// https://github.com/electron-userland/electron-builder/pull/1944
const timeout = parseInt(process.env.SIGNTOOL_TIMEOUT, 10) || 10 * 60 * 1000;
// unify logic of signtool path location
const toolInfo = await getToolPath();
const tool = toolInfo.path;
// decide runtime argument by cases
let args;
let env = process.env;
let vm;
if (configuration.path.endsWith(".appx") || !("file" in configuration.cscInfo) /* certificateSubjectName and other such options */) {
vm = await packager.vm.value;
args = computeSignToolArgs(configuration, true, vm);
}
else {
vm = new vm_1.VmManager();
args = configuration.computeSignToolArgs(process.platform === "win32");
if (toolInfo.env != null) {
env = toolInfo.env;
}
}
try {
await vm.exec(tool, args, { timeout, env });
}
catch (e) {
if (e.message.includes("The file is being used by another process") || e.message.includes("The specified timestamp server either could not be reached")) {
util_1.log.warn(`First attempt to code sign failed, another attempt will be made in 15 seconds: ${e.message}`);
await new Promise((resolve, reject) => {
setTimeout(() => {
vm.exec(tool, args, { timeout, env }).then(resolve).catch(reject);
}, 15000);
});
}
throw e;
}
}
exports.doSign = doSign;
// on windows be aware of http://stackoverflow.com/a/32640183/1910191
function computeSignToolArgs(options, isWin, vm = new vm_1.VmManager()) {
const inputFile = vm.toVmFile(options.path);
const outputPath = isWin ? inputFile : getOutputPath(inputFile, options.hash);
if (!isWin) {
options.resultOutputPath = outputPath;
}
const args = isWin ? ["sign"] : ["-in", inputFile, "-out", outputPath];
if (process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
const timestampingServiceUrl = options.options.timeStampServer || "http://timestamp.digicert.com";
if (isWin) {
args.push(options.isNest || options.hash === "sha256" ? "/tr" : "/t", options.isNest || options.hash === "sha256" ? options.options.rfc3161TimeStampServer || "http://timestamp.digicert.com" : timestampingServiceUrl);
}
else {
args.push("-t", timestampingServiceUrl);
}
}
const certificateFile = options.cscInfo.file;
if (certificateFile == null) {
const cscInfo = options.cscInfo;
const subjectName = cscInfo.thumbprint;
if (!isWin) {
throw new Error(`${subjectName == null ? "certificateSha1" : "certificateSubjectName"} supported only on Windows`);
}
args.push("/sha1", cscInfo.thumbprint);
args.push("/s", cscInfo.store);
if (cscInfo.isLocalMachineStore) {
args.push("/sm");
}
}
else {
const certExtension = path.extname(certificateFile);
if (certExtension === ".p12" || certExtension === ".pfx") {
args.push(isWin ? "/f" : "-pkcs12", vm.toVmFile(certificateFile));
}
else {
throw new Error(`Please specify pkcs12 (.p12/.pfx) file, ${certificateFile} is not correct`);
}
}
if (!isWin || options.hash !== "sha1") {
args.push(isWin ? "/fd" : "-h", options.hash);
if (isWin && process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
args.push("/td", "sha256");
}
}
if (options.name) {
args.push(isWin ? "/d" : "-n", options.name);
}
if (options.site) {
args.push(isWin ? "/du" : "-i", options.site);
}
// msi does not support dual-signing
if (options.isNest) {
args.push(isWin ? "/as" : "-nest");
}
const password = options.cscInfo == null ? null : options.cscInfo.password;
if (password) {
args.push(isWin ? "/p" : "-pass", password);
}
if (options.options.additionalCertificateFile) {
args.push(isWin ? "/ac" : "-ac", vm.toVmFile(options.options.additionalCertificateFile));
}
const httpsProxyFromEnv = process.env.HTTPS_PROXY;
if (!isWin && httpsProxyFromEnv != null && httpsProxyFromEnv.length) {
args.push("-p", httpsProxyFromEnv);
}
if (isWin) {
// https://github.com/electron-userland/electron-builder/issues/2875#issuecomment-387233610
args.push("/debug");
// must be last argument
args.push(inputFile);
}
return args;
}
function getOutputPath(inputPath, hash) {
const extension = path.extname(inputPath);
return path.join(path.dirname(inputPath), `${path.basename(inputPath, extension)}-signed-${hash}${extension}`);
}
/** @internal */
function isOldWin6() {
const winVersion = os.release();
return winVersion.startsWith("6.") && !winVersion.startsWith("6.3");
}
exports.isOldWin6 = isOldWin6;
function getWinSignTool(vendorPath) {
// use modern signtool on Windows Server 2012 R2 to be able to sign AppX
if (isOldWin6()) {
return path.join(vendorPath, "windows-6", "signtool.exe");
}
else {
return path.join(vendorPath, "windows-10", process.arch, "signtool.exe");
}
}
async function getToolPath() {
if (flags_1.isUseSystemSigncode()) {
return { path: "osslsigncode" };
}
const result = process.env.SIGNTOOL_PATH;
if (result) {
return { path: result };
}
const vendorPath = await getSignVendorPath();
if (process.platform === "win32") {
// use modern signtool on Windows Server 2012 R2 to be able to sign AppX
return { path: getWinSignTool(vendorPath) };
}
else if (process.platform === "darwin") {
const toolDirPath = path.join(vendorPath, process.platform, "10.12");
return {
path: path.join(toolDirPath, "osslsigncode"),
env: bundledTool_1.computeToolEnv([path.join(toolDirPath, "lib")]),
};
}
else {
return { path: path.join(vendorPath, process.platform, "osslsigncode") };
}
function signWithRetry(signer) {
return (0, builder_util_1.retry)(signer, {
retries: 3,
interval: 1000,
backoff: 1000,
shouldRetry: (e) => {
const message = e.message;
if (
// https://github.com/electron-userland/electron-builder/issues/1414
(message === null || message === void 0 ? void 0 : message.includes("Couldn't resolve host name")) ||
(
// https://github.com/electron-userland/electron-builder/issues/8615
message === null || message === void 0 ? void 0 : message.includes("being used by another process."))) {
builder_util_1.log.warn({ error: message }, "attempt to sign failed, another attempt will be made");
return true;
}
return false;
},
});
}
//# sourceMappingURL=windowsCodeSign.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,17 @@
import { MemoLazy } from "builder-util-runtime";
import { Lazy } from "lazy-val";
import { WindowsConfiguration } from "../options/winOptions";
import { WinPackager } from "../winPackager";
import { SignManager } from "./signManager";
import { WindowsSignOptions } from "./windowsCodeSign";
import { CertificateFromStoreInfo, FileCodeSigningInfo } from "./windowsSignToolManager";
export declare class WindowsSignAzureManager implements SignManager {
private readonly packager;
private readonly platformSpecificBuildOptions;
readonly computedPublisherName: Lazy<string[] | null>;
constructor(packager: WinPackager);
initialize(): Promise<void>;
computePublisherName(): Promise<string>;
readonly cscInfo: MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>;
signFile(options: WindowsSignOptions): Promise<boolean>;
}

View file

@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WindowsSignAzureManager = void 0;
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const lazy_val_1 = require("lazy-val");
class WindowsSignAzureManager {
constructor(packager) {
this.packager = packager;
this.computedPublisherName = new lazy_val_1.Lazy(() => {
var _a;
const publisherName = (_a = this.platformSpecificBuildOptions.azureSignOptions) === null || _a === void 0 ? void 0 : _a.publisherName;
if (publisherName === null) {
return Promise.resolve(null);
}
else if (publisherName != null) {
return Promise.resolve((0, builder_util_1.asArray)(publisherName));
}
// TODO: Is there another way to automatically pull Publisher Name from AzureTrusted service?
// For now return null.
return Promise.resolve(null);
});
this.cscInfo = new builder_util_runtime_1.MemoLazy(() => this.packager.platformSpecificBuildOptions, _selected => Promise.resolve(null));
this.platformSpecificBuildOptions = packager.platformSpecificBuildOptions;
}
async initialize() {
const vm = await this.packager.vm.value;
const ps = await vm.powershellCommand.value;
builder_util_1.log.info(null, "installing required module (TrustedSigning) with scope CurrentUser");
try {
await vm.exec(ps, ["-NoProfile", "-NonInteractive", "-Command", "Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser"]);
}
catch (error) {
// Might not be needed, seems GH runners already have NuGet set up.
// Logging to debug just in case users run into this. If NuGet isn't present, Install-Module -Name TrustedSigning will fail, so we'll get the logs at that point
builder_util_1.log.debug({ message: error.message || error.stack }, "unable to install PackageProvider Nuget. Might be a false alarm though as some systems already have it installed");
}
await vm.exec(ps, ["-NoProfile", "-NonInteractive", "-Command", "Install-Module -Name TrustedSigning -MinimumVersion 0.5.0 -Force -Repository PSGallery -Scope CurrentUser"]);
// If signing has been misconfigured it, the error from the TrustedSigning module should be descriptive enough to help them fix their configuration.
// Options: https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet#definition
}
computePublisherName() {
return Promise.resolve(this.packager.platformSpecificBuildOptions.azureSignOptions.publisherName);
}
// prerequisite: requires `initializeProviderModules` to already have been executed
async signFile(options) {
const vm = await this.packager.vm.value;
const ps = await vm.powershellCommand.value;
const { publisherName: _publisher, // extract from `extraSigningArgs`
endpoint, certificateProfileName, codeSigningAccountName, fileDigest, timestampRfc3161, timestampDigest, ...extraSigningArgs } = options.options.azureSignOptions;
const params = {
...extraSigningArgs,
Endpoint: endpoint,
CertificateProfileName: certificateProfileName,
CodeSigningAccountName: codeSigningAccountName,
TimestampRfc3161: timestampRfc3161 || "http://timestamp.acs.microsoft.com",
TimestampDigest: timestampDigest || "SHA256",
FileDigest: fileDigest || "SHA256",
Files: vm.toVmFile(options.path),
};
const paramsString = Object.entries(params)
.filter(([_, value]) => value != null)
.reduce((res, [field, value]) => {
const escapedValue = String(value).replace(/'/g, "''");
return [...res, `-${field}`, `'${escapedValue}'`];
}, [])
.join(" ");
await vm.exec(ps, ["-NoProfile", "-NonInteractive", "-Command", `Invoke-TrustedSigning ${paramsString}`]);
return true;
}
}
exports.WindowsSignAzureManager = WindowsSignAzureManager;
//# sourceMappingURL=windowsSignAzureManager.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,58 @@
import { MemoLazy } from "builder-util-runtime";
import { Lazy } from "lazy-val";
import { Target } from "../core";
import { WindowsConfiguration } from "../options/winOptions";
import { ToolInfo } from "../util/bundledTool";
import { VmManager } from "../vm/vm";
import { WinPackager } from "../winPackager";
import { SignManager } from "./signManager";
import { WindowsSignOptions } from "./windowsCodeSign";
export type CustomWindowsSign = (configuration: CustomWindowsSignTaskConfiguration, packager?: WinPackager) => Promise<any>;
export interface WindowsSignToolOptions extends WindowsSignOptions {
readonly name: string;
readonly site: string | null;
}
export interface FileCodeSigningInfo {
readonly file: string;
readonly password: string | null;
}
export interface WindowsSignTaskConfiguration extends WindowsSignToolOptions {
readonly cscInfo: FileCodeSigningInfo | CertificateFromStoreInfo | null;
resultOutputPath?: string;
hash: string;
isNest: boolean;
}
export interface CustomWindowsSignTaskConfiguration extends WindowsSignTaskConfiguration {
computeSignToolArgs(isWin: boolean): Array<string>;
}
export interface CertificateInfo {
readonly commonName: string;
readonly bloodyMicrosoftSubjectDn: string;
}
export interface CertificateFromStoreInfo {
thumbprint: string;
subject: string;
store: string;
isLocalMachineStore: boolean;
}
export declare class WindowsSignToolManager implements SignManager {
private readonly packager;
private readonly platformSpecificBuildOptions;
constructor(packager: WinPackager);
readonly computedPublisherName: Lazy<string[] | null>;
readonly lazyCertInfo: MemoLazy<MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>, CertificateInfo | null>;
readonly cscInfo: MemoLazy<WindowsConfiguration, FileCodeSigningInfo | CertificateFromStoreInfo | null>;
initialize(): Promise<void>;
computePublisherName(target: Target, publisherName: string): Promise<string>;
signFile(options: WindowsSignOptions): Promise<boolean>;
getCertInfo(file: string, password: string): Promise<CertificateInfo>;
computeSignToolArgs(options: WindowsSignTaskConfiguration, isWin: boolean, vm?: VmManager): Array<string>;
private computeWindowsSignArgs;
private computeOsslsigncodeArgs;
private addCertificateArgs;
private addCommonSigningArgs;
getOutputPath(inputPath: string, hash: string): string;
getCertificateFromStoreInfo(options: WindowsConfiguration, vm: VmManager): Promise<CertificateFromStoreInfo>;
getToolPath(isWin?: boolean): Promise<ToolInfo>;
doSign(configuration: CustomWindowsSignTaskConfiguration, packager: WinPackager): Promise<void>;
}

View file

@ -0,0 +1,376 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WindowsSignToolManager = void 0;
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_extra_1 = require("fs-extra");
const lazy_val_1 = require("lazy-val");
const path = require("path");
const AppxTarget_1 = require("../targets/AppxTarget");
const windows_1 = require("../toolsets/windows");
const resolve_1 = require("../util/resolve");
const certInfo_1 = require("./certInfo");
const vm_1 = require("../vm/vm");
const codesign_1 = require("./codesign");
class WindowsSignToolManager {
constructor(packager) {
this.packager = packager;
this.computedPublisherName = new lazy_val_1.Lazy(async () => {
var _a;
const publisherName = (_a = this.platformSpecificBuildOptions.signtoolOptions) === null || _a === void 0 ? void 0 : _a.publisherName;
if (publisherName === null) {
return null;
}
else if (publisherName != null) {
return (0, builder_util_1.asArray)(publisherName);
}
const certInfo = await this.lazyCertInfo.value;
return certInfo == null ? null : [certInfo.commonName];
});
this.lazyCertInfo = new builder_util_runtime_1.MemoLazy(() => this.cscInfo, async (csc) => {
const cscInfo = await csc.value;
if (cscInfo == null) {
return null;
}
if ("subject" in cscInfo) {
const bloodyMicrosoftSubjectDn = cscInfo.subject;
return {
commonName: (0, builder_util_runtime_1.parseDn)(bloodyMicrosoftSubjectDn).get("CN"),
bloodyMicrosoftSubjectDn,
};
}
const cscFile = cscInfo.file;
if (cscFile == null) {
return null;
}
return await this.getCertInfo(cscFile, cscInfo.password || "");
});
this.cscInfo = new builder_util_runtime_1.MemoLazy(() => this.platformSpecificBuildOptions, platformSpecificBuildOptions => {
var _a, _b, _c;
const subjectName = (_a = platformSpecificBuildOptions.signtoolOptions) === null || _a === void 0 ? void 0 : _a.certificateSubjectName;
const shaType = (_b = platformSpecificBuildOptions.signtoolOptions) === null || _b === void 0 ? void 0 : _b.certificateSha1;
if (subjectName != null || shaType != null) {
return this.packager.vm.value
.then(vm => this.getCertificateFromStoreInfo(platformSpecificBuildOptions, vm))
.catch((e) => {
var _a;
// https://github.com/electron-userland/electron-builder/pull/2397
if (((_a = platformSpecificBuildOptions.signtoolOptions) === null || _a === void 0 ? void 0 : _a.sign) == null) {
throw e;
}
else {
builder_util_1.log.debug({ error: e }, "getCertificateFromStoreInfo error");
return null;
}
});
}
const certificateFile = (_c = platformSpecificBuildOptions.signtoolOptions) === null || _c === void 0 ? void 0 : _c.certificateFile;
if (certificateFile != null) {
const certificatePassword = this.packager.getCscPassword();
return Promise.resolve({
file: certificateFile,
password: certificatePassword == null ? null : certificatePassword.trim(),
});
}
const cscLink = this.packager.getCscLink("WIN_CSC_LINK");
if (cscLink == null || cscLink === "") {
return Promise.resolve(null);
}
return ((0, codesign_1.importCertificate)(cscLink, this.packager.info.tempDirManager, this.packager.projectDir)
// before then
.catch((e) => {
if (e instanceof builder_util_1.InvalidConfigurationError) {
throw new builder_util_1.InvalidConfigurationError(`Env WIN_CSC_LINK is not correct, cannot resolve: ${e.message}`);
}
else {
throw e;
}
})
.then(path => {
return {
file: path,
password: this.packager.getCscPassword(),
};
}));
});
this.platformSpecificBuildOptions = packager.platformSpecificBuildOptions;
}
initialize() {
return Promise.resolve();
}
// https://github.com/electron-userland/electron-builder/issues/2108#issuecomment-333200711
async computePublisherName(target, publisherName) {
if (target instanceof AppxTarget_1.default && (await this.cscInfo.value) == null) {
builder_util_1.log.info({ reason: "Windows Store only build" }, "AppX is not signed");
return publisherName || "CN=ms";
}
const certInfo = await this.lazyCertInfo.value;
const publisher = publisherName || (certInfo == null ? null : certInfo.bloodyMicrosoftSubjectDn);
if (publisher == null) {
throw new Error("Internal error: cannot compute subject using certificate info");
}
return publisher;
}
async signFile(options) {
var _a, _b;
let hashes = (_a = options.options.signtoolOptions) === null || _a === void 0 ? void 0 : _a.signingHashAlgorithms;
// msi does not support dual-signing
if (options.path.endsWith(".msi")) {
hashes = [hashes != null && !hashes.includes("sha1") ? "sha256" : "sha1"];
}
else if (options.path.endsWith(".appx")) {
hashes = ["sha256"];
}
else if (hashes == null) {
hashes = ["sha1", "sha256"];
}
else {
hashes = Array.isArray(hashes) ? hashes : [hashes];
}
const name = this.packager.appInfo.productName;
const site = await this.packager.appInfo.computePackageUrl();
const customSign = await (0, resolve_1.resolveFunction)(this.packager.appInfo.type, (_b = options.options.signtoolOptions) === null || _b === void 0 ? void 0 : _b.sign, "sign", await this.packager.info.getWorkspaceRoot());
const cscInfo = await this.cscInfo.value;
if (cscInfo) {
let logInfo = {
file: builder_util_1.log.filePath(options.path),
};
if ("file" in cscInfo) {
logInfo = {
...logInfo,
certificateFile: cscInfo.file,
};
}
else {
logInfo = {
...logInfo,
subject: cscInfo.subject,
thumbprint: cscInfo.thumbprint,
store: cscInfo.store,
user: cscInfo.isLocalMachineStore ? "local machine" : "current user",
};
}
builder_util_1.log.info(logInfo, "signing");
}
else if (!customSign) {
builder_util_1.log.debug({ signHook: !!customSign, cscInfo }, "no signing info identified, signing is skipped");
return false;
}
const executor = customSign || ((config, packager) => this.doSign(config, packager));
let isNest = false;
for (const hash of hashes) {
const taskConfiguration = { ...options, name, site, cscInfo, hash, isNest };
await Promise.resolve(executor({
...taskConfiguration,
computeSignToolArgs: isWin => this.computeSignToolArgs(taskConfiguration, isWin),
}, this.packager));
isNest = true;
if (taskConfiguration.resultOutputPath != null) {
await (0, fs_extra_1.rename)(taskConfiguration.resultOutputPath, options.path);
}
}
return true;
}
async getCertInfo(file, password) {
const errorMessagePrefix = "Cannot extract publisher name from code signing certificate. As workaround, set win.publisherName. Error: ";
try {
return await (0, certInfo_1.readCertInfo)(file, password);
}
catch (e) {
throw new builder_util_1.InvalidConfigurationError(`${errorMessagePrefix}${e.message || e}`);
}
}
// on windows be aware of http://stackoverflow.com/a/32640183/1910191
computeSignToolArgs(options, isWin, vm = new vm_1.VmManager()) {
return isWin ? this.computeWindowsSignArgs(options, vm) : this.computeOsslsigncodeArgs(options, vm);
}
computeWindowsSignArgs(options, vm) {
var _a, _b, _c, _d;
const inputFile = vm.toVmFile(options.path);
const args = ["sign"];
// Timestamping
if (process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
const isRfc3161 = options.isNest || options.hash === "sha256";
args.push(isRfc3161 ? "/tr" : "/t");
const timestampUrl = isRfc3161
? ((_a = options.options.signtoolOptions) === null || _a === void 0 ? void 0 : _a.rfc3161TimeStampServer) || "http://timestamp.digicert.com"
: ((_b = options.options.signtoolOptions) === null || _b === void 0 ? void 0 : _b.timeStampServer) || "http://timestamp.digicert.com";
args.push(timestampUrl);
}
// Certificate
this.addCertificateArgs(args, options, vm, true);
// Hash algorithm
const isLegacyToolset = ((_c = this.packager.config.toolsets) === null || _c === void 0 ? void 0 : _c.winCodeSign) === "0.0.0" || ((_d = this.packager.config.toolsets) === null || _d === void 0 ? void 0 : _d.winCodeSign) == null;
if (isLegacyToolset) {
// Legacy || v0.0.0: Only add /fd for non-SHA1 (original behavior)
if (options.hash !== "sha1") {
args.push("/fd", options.hash);
if (process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
args.push("/td", "sha256");
}
}
}
else {
// Modern: Always add /fd (required by new Windows Kits)
args.push("/fd", options.hash.toLowerCase());
// Only add /td for RFC3161 timestamps (incompatible with /t)
if (process.env.ELECTRON_BUILDER_OFFLINE !== "true" && (options.isNest || options.hash === "sha256")) {
args.push("/td", "sha256");
}
}
// Optional parameters
this.addCommonSigningArgs(args, options, vm, true);
// Windows-specific
args.push("/debug");
args.push(inputFile); // Must be last
return args;
}
computeOsslsigncodeArgs(options, vm) {
var _a;
const inputFile = vm.toVmFile(options.path);
const outputPath = this.getOutputPath(inputFile, options.hash);
options.resultOutputPath = outputPath;
const args = ["sign", "-in", inputFile, "-out", outputPath];
// Timestamping
if (process.env.ELECTRON_BUILDER_OFFLINE !== "true") {
const timestampUrl = ((_a = options.options.signtoolOptions) === null || _a === void 0 ? void 0 : _a.timeStampServer) || "http://timestamp.digicert.com";
args.push("-t", timestampUrl);
}
// Certificate
this.addCertificateArgs(args, options, vm, false);
// Hash algorithm
args.push("-h", options.hash.toLowerCase());
// Optional parameters
this.addCommonSigningArgs(args, options, vm, false);
// Proxy support
const httpsProxy = process.env.HTTPS_PROXY;
if (httpsProxy === null || httpsProxy === void 0 ? void 0 : httpsProxy.length) {
args.push("-p", httpsProxy);
}
return args;
}
addCertificateArgs(args, options, vm, isWin) {
if (options.cscInfo == null) {
throw new Error("No code signing certificate configured. Provide certificateFile, certificateSha1, or certificateSubjectName.");
}
const certificateFile = options.cscInfo.file;
if (certificateFile == null) {
// Certificate from store (Windows only)
if (!isWin) {
throw new Error("certificateSha1/certificateSubjectName supported only on Windows");
}
const cscInfo = options.cscInfo;
args.push("/sha1", cscInfo.thumbprint);
args.push("/s", cscInfo.store);
if (cscInfo.isLocalMachineStore) {
args.push("/sm");
}
}
else {
// Certificate file
const certExtension = path.extname(certificateFile);
if (certExtension === ".p12" || certExtension === ".pfx") {
args.push(isWin ? "/f" : "-pkcs12", vm.toVmFile(certificateFile));
}
else {
throw new Error(`Please specify pkcs12 (.p12/.pfx) file, ${certificateFile} is not correct`);
}
}
}
addCommonSigningArgs(args, options, vm, isWin) {
var _a, _b;
if (options.name) {
args.push(isWin ? "/d" : "-n", options.name);
}
if (options.site) {
args.push(isWin ? "/du" : "-i", options.site);
}
if (options.isNest) {
args.push(isWin ? "/as" : "-nest");
}
const password = (_a = options.cscInfo) === null || _a === void 0 ? void 0 : _a.password;
if (password) {
args.push(isWin ? "/p" : "-pass", password);
}
const additionalCert = (_b = options.options.signtoolOptions) === null || _b === void 0 ? void 0 : _b.additionalCertificateFile;
if (additionalCert) {
args.push(isWin ? "/ac" : "-ac", vm.toVmFile(additionalCert));
}
}
getOutputPath(inputPath, hash) {
const extension = path.extname(inputPath);
return path.join(path.dirname(inputPath), `${path.basename(inputPath, extension)}-signed-${hash}${extension}`);
}
async getCertificateFromStoreInfo(options, vm) {
var _a, _b, _c;
const certificateSubjectName = (_a = options.signtoolOptions) === null || _a === void 0 ? void 0 : _a.certificateSubjectName;
const certificateSha1 = (_c = (_b = options.signtoolOptions) === null || _b === void 0 ? void 0 : _b.certificateSha1) === null || _c === void 0 ? void 0 : _c.toUpperCase();
const ps = await vm.powershellCommand.value;
const rawResult = await vm.exec(ps, [
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-ChildItem -Recurse Cert: -CodeSigningCert | Select-Object -Property Subject,PSParentPath,Thumbprint | ConvertTo-Json -Compress",
]);
const certList = rawResult.length === 0 ? [] : (0, builder_util_1.asArray)(JSON.parse(rawResult));
for (const certInfo of certList) {
if ((certificateSubjectName != null && !certInfo.Subject.includes(certificateSubjectName)) ||
(certificateSha1 != null && certInfo.Thumbprint.toUpperCase() !== certificateSha1)) {
continue;
}
const parentPath = certInfo.PSParentPath;
const store = parentPath.substring(parentPath.lastIndexOf("\\") + 1);
builder_util_1.log.debug({ store, PSParentPath: parentPath }, "auto-detect certificate store");
// https://github.com/electron-userland/electron-builder/issues/1717
const isLocalMachineStore = parentPath.includes("Certificate::LocalMachine");
builder_util_1.log.debug(null, "auto-detect using of LocalMachine store");
return {
thumbprint: certInfo.Thumbprint,
subject: certInfo.Subject,
store,
isLocalMachineStore,
};
}
throw new Error(`Cannot find certificate ${certificateSubjectName || certificateSha1}, all certs: ${rawResult}`);
}
async getToolPath(isWin = process.platform === "win32") {
var _a;
return (0, windows_1.getSignToolPath)((_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.winCodeSign, isWin);
}
async doSign(configuration, packager) {
var _a;
// https://github.com/electron-userland/electron-builder/pull/1944
const timeout = parseInt(process.env.SIGNTOOL_TIMEOUT, 10) || 10 * 60 * 1000;
// decide runtime argument by cases
let args;
let vm;
const useVmIfNotOnWin = configuration.path.endsWith(".appx") || !("file" in configuration.cscInfo); /* certificateSubjectName and other such options */
const isWin = process.platform === "win32" || useVmIfNotOnWin;
const toolInfo = await (0, windows_1.getSignToolPath)((_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.winCodeSign, isWin);
const tool = toolInfo.path;
if (useVmIfNotOnWin) {
vm = await packager.vm.value;
args = this.computeSignToolArgs(configuration, isWin, vm);
}
else {
vm = new vm_1.VmManager();
args = configuration.computeSignToolArgs(isWin);
}
await (0, builder_util_1.retry)(() => vm.exec(tool, args, { timeout, env: { ...process.env, ...(toolInfo.env || {}) } }), {
retries: 2,
interval: 15000,
backoff: 10000,
shouldRetry: (e) => {
if (e.message.includes("The file is being used by another process") ||
e.message.includes("The specified timestamp server either could not be reached") ||
e.message.includes("No certificates were found that met all the given criteria.")) {
builder_util_1.log.warn(`Attempt to code sign failed, another attempt will be made in 15 seconds: ${e.message}`);
return true;
}
return false;
},
});
}
}
exports.WindowsSignToolManager = WindowsSignToolManager;
//# sourceMappingURL=windowsSignToolManager.js.map

File diff suppressed because one or more lines are too long