@rightkit/release 0.2.153 → 0.2.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deps.mjs +0 -0
- package/lsclean.sh +0 -0
- package/msix-build.mjs +3 -6
- package/msix-signer.mjs +211 -0
- package/package.json +11 -9
- package/release.mjs +0 -0
- package/rightkit-versions.json +1 -1
- package/upload-large.mjs +0 -0
package/deps.mjs
CHANGED
|
File without changes
|
package/lsclean.sh
CHANGED
|
File without changes
|
package/msix-build.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { spawnSync } from "./background-process.mjs";
|
|
11
11
|
import { findWindowsSdkTool } from "./preflight.mjs";
|
|
12
12
|
import { generateAppInstaller } from "./msix-appinstaller.mjs";
|
|
13
|
+
import { readMsixSignerSubject } from "./msix-signer.mjs";
|
|
13
14
|
import { assertPublisherMatchesSigner, planMsixSigning, verifyPayloadInventory } from "./msix-plan.mjs";
|
|
14
15
|
import { createHash } from "node:crypto";
|
|
15
16
|
|
|
@@ -102,20 +103,16 @@ export function peFilesIn(root) {
|
|
|
102
103
|
|
|
103
104
|
const sha256 = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
104
105
|
|
|
105
|
-
function readSignerSubject(file, run) {
|
|
106
|
-
const r = run("powershell", ["-NoProfile", "-Command", `(Get-AuthenticodeSignature -LiteralPath '${file.replace(/'/g, "''")}').SignerCertificate.Subject`], { encoding: "utf8", windowsHide: true });
|
|
107
|
-
return r.status === 0 ? String(r.stdout ?? "").trim() : "";
|
|
108
|
-
}
|
|
109
|
-
|
|
110
106
|
/**
|
|
111
107
|
* Pack + sign. `run` is injectable for tests. Ordered per planMsixSigning():
|
|
112
108
|
* assemble, verify-inventory, sign-pe (DLLs then EXEs, raw bytes), pack, sign-msix, verify-msix,
|
|
113
109
|
* verify-publisher, appinstaller, seal. Windows-only unless dryRun.
|
|
110
|
+
* readSubject(file) defaults to reading AppxSignature.p7x (msix-signer.mjs) and may be overridden.
|
|
114
111
|
* options.runtime = { dlls, systemDlls, dependencies } (declared inventory, verified fail-closed);
|
|
115
112
|
* options.appInstaller = { uri, msixUri, out, ... } generates the update route after the package is signed.
|
|
116
113
|
*/
|
|
117
114
|
export function buildMsix({ stageDir, out, workDir, manifest, assets, runtime, appInstaller, sign = true, dryRun = false, receiptDir, peReceipt, msixReceipt },
|
|
118
|
-
{ platform = process.platform, run = spawnSync, findTool = findWindowsSdkTool, env = process.env, log = (e) => console.error(JSON.stringify(e)), readSubject =
|
|
115
|
+
{ platform = process.platform, run = spawnSync, findTool = findWindowsSdkTool, env = process.env, log = (e) => console.error(JSON.stringify(e)), readSubject = readMsixSignerSubject } = {}) {
|
|
119
116
|
out = path.resolve(out);
|
|
120
117
|
if (!out.endsWith(".msix")) throw fail("MSIX_INVALID_OPTIONS", "out must end with .msix");
|
|
121
118
|
validateMsixIdentity(manifest);
|
package/msix-signer.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Default signer-subject reader for .msix / .msixbundle.
|
|
2
|
+
// PowerShell Get-AuthenticodeSignature returns nothing for MSIX on hosted Windows runners, so the subject
|
|
3
|
+
// is read from the package's own AppxSignature.p7x: a "PKCX" magic followed by a PKCS#7 SignedData.
|
|
4
|
+
// Pure Node (node:zlib + node:crypto X509Certificate + a small DER walker); no dependency, any host.
|
|
5
|
+
// The certificate chosen is the one named by SignerInfo issuerAndSerialNumber (or subjectKeyIdentifier),
|
|
6
|
+
// never merely the first certificate. Every failure throws MSIX_PUBLISHER_MISMATCH (fail closed).
|
|
7
|
+
|
|
8
|
+
import { X509Certificate } from "node:crypto";
|
|
9
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
10
|
+
import { inflateRawSync } from "node:zlib";
|
|
11
|
+
|
|
12
|
+
const fail = (message) => Object.assign(new Error(message), { code: "MSIX_PUBLISHER_MISMATCH" });
|
|
13
|
+
const MAX_P7X = 4 * 1024 * 1024;
|
|
14
|
+
const SIGNED_DATA_OID = "1.2.840.113549.1.7.2";
|
|
15
|
+
|
|
16
|
+
// --- zip container -----------------------------------------------------------------------------
|
|
17
|
+
const u64 = (b, o) => Number(b.readBigUInt64LE(o));
|
|
18
|
+
|
|
19
|
+
function readAt(fd, position, length) {
|
|
20
|
+
const buffer = Buffer.alloc(length);
|
|
21
|
+
let done = 0;
|
|
22
|
+
while (done < length) {
|
|
23
|
+
const n = readSync(fd, buffer, done, length - done, position + done);
|
|
24
|
+
if (n === 0) break;
|
|
25
|
+
done += n;
|
|
26
|
+
}
|
|
27
|
+
return buffer.subarray(0, done);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Return the bytes of the named entry of a zip file, or null when absent. */
|
|
31
|
+
export function readZipEntry(file, wanted, { maxBytes = MAX_P7X } = {}) {
|
|
32
|
+
const fd = openSync(file, "r");
|
|
33
|
+
try {
|
|
34
|
+
const size = fstatSync(fd).size;
|
|
35
|
+
const tailLength = Math.min(size, 22 + 65535);
|
|
36
|
+
const tail = readAt(fd, size - tailLength, tailLength);
|
|
37
|
+
let entries; let dirSize; let dirOffset; let found = false;
|
|
38
|
+
for (let i = tail.length - 22; i >= 0; i -= 1) {
|
|
39
|
+
if (tail.readUInt32LE(i) !== 0x06054b50) continue;
|
|
40
|
+
if (i + 22 + tail.readUInt16LE(i + 20) > tail.length) continue;
|
|
41
|
+
entries = tail.readUInt16LE(i + 10); dirSize = tail.readUInt32LE(i + 12); dirOffset = tail.readUInt32LE(i + 16);
|
|
42
|
+
if (entries === 0xffff || dirSize === 0xffffffff || dirOffset === 0xffffffff) {
|
|
43
|
+
const at = size - tailLength + i;
|
|
44
|
+
const loc = at >= 20 ? readAt(fd, at - 20, 20) : Buffer.alloc(0);
|
|
45
|
+
if (loc.length < 20 || loc.readUInt32LE(0) !== 0x07064b50) throw new Error("zip64 locator missing");
|
|
46
|
+
const rec = readAt(fd, u64(loc, 8), 56);
|
|
47
|
+
if (rec.length < 56 || rec.readUInt32LE(0) !== 0x06064b50) throw new Error("zip64 end record missing");
|
|
48
|
+
entries = u64(rec, 32); dirSize = u64(rec, 40); dirOffset = u64(rec, 48);
|
|
49
|
+
}
|
|
50
|
+
found = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (!found) throw new Error("not a zip container (end of central directory not found)");
|
|
54
|
+
if (dirSize > 256 * 1024 * 1024 || dirOffset + dirSize > size) throw new Error("central directory out of bounds");
|
|
55
|
+
const dir = readAt(fd, dirOffset, dirSize);
|
|
56
|
+
if (dir.length !== dirSize) throw new Error("central directory truncated");
|
|
57
|
+
let at = 0;
|
|
58
|
+
for (let n = 0; n < entries; n += 1) {
|
|
59
|
+
if (at + 46 > dir.length || dir.readUInt32LE(at) !== 0x02014b50) throw new Error("bad central directory entry");
|
|
60
|
+
const flags = dir.readUInt16LE(at + 8); const method = dir.readUInt16LE(at + 10);
|
|
61
|
+
let compressed = dir.readUInt32LE(at + 20); let uncompressed = dir.readUInt32LE(at + 24);
|
|
62
|
+
const nameLen = dir.readUInt16LE(at + 28); const extraLen = dir.readUInt16LE(at + 30); const commentLen = dir.readUInt16LE(at + 32);
|
|
63
|
+
let local = dir.readUInt32LE(at + 42);
|
|
64
|
+
const end = at + 46 + nameLen + extraLen + commentLen;
|
|
65
|
+
if (end > dir.length) throw new Error("central directory entry out of bounds");
|
|
66
|
+
const name = dir.subarray(at + 46, at + 46 + nameLen).toString("utf8");
|
|
67
|
+
if (name === wanted) {
|
|
68
|
+
const extra = dir.subarray(at + 46 + nameLen, at + 46 + nameLen + extraLen);
|
|
69
|
+
for (let e = 0; e + 4 <= extra.length; e += 4 + extra.readUInt16LE(e + 2)) {
|
|
70
|
+
if (extra.readUInt16LE(e) !== 0x0001) continue;
|
|
71
|
+
let p = e + 4;
|
|
72
|
+
if (uncompressed === 0xffffffff) { uncompressed = u64(extra, p); p += 8; }
|
|
73
|
+
if (compressed === 0xffffffff) { compressed = u64(extra, p); p += 8; }
|
|
74
|
+
if (local === 0xffffffff) local = u64(extra, p);
|
|
75
|
+
}
|
|
76
|
+
if (flags & 1) throw new Error(`${wanted} is encrypted`);
|
|
77
|
+
if (uncompressed > maxBytes || compressed > maxBytes) throw new Error(`${wanted} larger than ${maxBytes} bytes`);
|
|
78
|
+
const lh = readAt(fd, local, 30);
|
|
79
|
+
if (lh.length < 30 || lh.readUInt32LE(0) !== 0x04034b50) throw new Error("bad local header");
|
|
80
|
+
const start = local + 30 + lh.readUInt16LE(26) + lh.readUInt16LE(28);
|
|
81
|
+
if (start + compressed > size) throw new Error("entry data out of bounds");
|
|
82
|
+
const raw = readAt(fd, start, compressed);
|
|
83
|
+
let data;
|
|
84
|
+
if (method === 0) data = raw;
|
|
85
|
+
else if (method === 8) data = inflateRawSync(raw, { maxOutputLength: maxBytes });
|
|
86
|
+
else throw new Error(`unsupported zip method ${method}`);
|
|
87
|
+
if (data.length !== uncompressed) throw new Error(`${wanted} length differs from its directory entry`);
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
at = end;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
} finally { closeSync(fd); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- DER ---------------------------------------------------------------------------------------
|
|
97
|
+
/** Read one TLV at `at`; returns { tag, start (of header), body (start), end }. */
|
|
98
|
+
function tlv(buf, at, limit = buf.length) {
|
|
99
|
+
if (at + 2 > limit) throw new Error("DER truncated");
|
|
100
|
+
const tag = buf[at]; let len = buf[at + 1]; let body = at + 2;
|
|
101
|
+
if ((tag & 0x1f) === 0x1f) throw new Error("DER high tag numbers unsupported");
|
|
102
|
+
if (len & 0x80) {
|
|
103
|
+
const n = len & 0x7f;
|
|
104
|
+
if (n === 0) return indefinite(buf, tag, at, limit); // BER indefinite length
|
|
105
|
+
if (n > 4 || body + n > limit) throw new Error("DER bad length");
|
|
106
|
+
len = 0;
|
|
107
|
+
for (let i = 0; i < n; i += 1) len = len * 256 + buf[body + i];
|
|
108
|
+
body += n;
|
|
109
|
+
}
|
|
110
|
+
if (body + len > limit) throw new Error("DER length exceeds container");
|
|
111
|
+
return { tag, start: at, body, end: body + len };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// BER indefinite-length constructed value: children until 00 00.
|
|
115
|
+
function indefinite(buf, tag, at, limit) {
|
|
116
|
+
if (!(tag & 0x20)) throw new Error("indefinite length on primitive");
|
|
117
|
+
const body = at + 2; let p = body;
|
|
118
|
+
while (true) {
|
|
119
|
+
if (p + 2 > limit) throw new Error("BER unterminated");
|
|
120
|
+
if (buf[p] === 0 && buf[p + 1] === 0) return { tag, start: at, body, end: p, tail: p + 2 };
|
|
121
|
+
p = next(tlv(buf, p, limit));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const next = (t) => t.tail ?? t.end;
|
|
125
|
+
const children = (buf, t) => {
|
|
126
|
+
const out = []; let p = t.body;
|
|
127
|
+
while (p < t.end) { const c = tlv(buf, p, t.end); out.push(c); p = next(c); }
|
|
128
|
+
return out;
|
|
129
|
+
};
|
|
130
|
+
const whole = (buf, t) => buf.subarray(t.start, next(t));
|
|
131
|
+
const oid = (buf, t) => {
|
|
132
|
+
const b = buf.subarray(t.body, t.end); const parts = [Math.floor(b[0] / 40), b[0] % 40]; let v = 0;
|
|
133
|
+
for (let i = 1; i < b.length; i += 1) { v = v * 128 + (b[i] & 0x7f); if (!(b[i] & 0x80)) { parts.push(v); v = 0; } }
|
|
134
|
+
return parts.join(".");
|
|
135
|
+
};
|
|
136
|
+
const intHex = (buf, t) => { let h = buf.subarray(t.body, t.end).toString("hex").replace(/^(00)+(?=.)/, ""); return h.toLowerCase(); };
|
|
137
|
+
|
|
138
|
+
/** Parse PKCS#7 SignedData; return { certificates: [Buffer], signer: { issuer: Buffer, serial: string } | { ski: string } }. */
|
|
139
|
+
export function parseSignedData(der) {
|
|
140
|
+
const ci = tlv(der, 0);
|
|
141
|
+
if (ci.tag !== 0x30) throw new Error("ContentInfo is not a SEQUENCE");
|
|
142
|
+
const [type, content] = children(der, ci);
|
|
143
|
+
if (!type || type.tag !== 0x06 || oid(der, type) !== SIGNED_DATA_OID) throw new Error("not a PKCS#7 SignedData");
|
|
144
|
+
if (!content || content.tag !== 0xa0) throw new Error("SignedData content missing");
|
|
145
|
+
const sd = children(der, content)[0];
|
|
146
|
+
if (!sd || sd.tag !== 0x30) throw new Error("SignedData is not a SEQUENCE");
|
|
147
|
+
const parts = children(der, sd);
|
|
148
|
+
const certsSet = parts.find((p) => p.tag === 0xa0);
|
|
149
|
+
const signerSet = parts.filter((p) => p.tag === 0x31).at(-1);
|
|
150
|
+
if (!certsSet) throw new Error("SignedData carries no certificates");
|
|
151
|
+
if (!signerSet) throw new Error("SignedData carries no SignerInfos");
|
|
152
|
+
const certificates = children(der, certsSet).filter((c) => c.tag === 0x30).map((c) => Buffer.from(whole(der, c)));
|
|
153
|
+
const infos = children(der, signerSet);
|
|
154
|
+
if (infos.length !== 1) throw new Error(`expected exactly one SignerInfo, found ${infos.length}`);
|
|
155
|
+
const [, sid] = children(der, infos[0]);
|
|
156
|
+
if (!sid) throw new Error("SignerInfo has no signer identifier");
|
|
157
|
+
let signer;
|
|
158
|
+
if (sid.tag === 0x30) {
|
|
159
|
+
const [issuer, serial] = children(der, sid);
|
|
160
|
+
signer = { issuer: Buffer.from(whole(der, issuer)), serial: intHex(der, serial) };
|
|
161
|
+
} else if (sid.tag === 0x80) signer = { ski: Buffer.from(der.subarray(sid.body, sid.end)).toString("hex") };
|
|
162
|
+
else throw new Error("unsupported SignerInfo signer identifier");
|
|
163
|
+
return { certificates, signer };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** TBSCertificate issuer (DER) and serial (hex) of a certificate. */
|
|
167
|
+
function certIdentity(cert) {
|
|
168
|
+
const [tbs] = children(cert, tlv(cert, 0));
|
|
169
|
+
const f = children(cert, tbs);
|
|
170
|
+
const i = f[0].tag === 0xa0 ? 1 : 0; // optional [0] version
|
|
171
|
+
return { serial: intHex(cert, f[i]), issuer: Buffer.from(whole(cert, f[i + 2])) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function signerCertificate(parsed) {
|
|
175
|
+
const match = parsed.certificates.filter((c) => {
|
|
176
|
+
try {
|
|
177
|
+
if (parsed.signer.ski) {
|
|
178
|
+
const x = new X509Certificate(c);
|
|
179
|
+
return x.raw && (x.subjectKeyIdentifier ?? "").replace(/:/g, "").toLowerCase() === parsed.signer.ski;
|
|
180
|
+
}
|
|
181
|
+
const id = certIdentity(c);
|
|
182
|
+
return id.serial === parsed.signer.serial && id.issuer.equals(parsed.signer.issuer);
|
|
183
|
+
} catch { return false; }
|
|
184
|
+
});
|
|
185
|
+
if (match.length !== 1) throw new Error(`signer certificate not found in the signature (${match.length} matches)`);
|
|
186
|
+
return new X509Certificate(match[0]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- subject rendering ---------------------------------------------------------------------------
|
|
190
|
+
const needsQuote = (v) => /[,+"\\<>;=\r\n]/.test(v) || /^[ #]|\s$/.test(v);
|
|
191
|
+
/** Node renders subject as "K=V" lines; join as an RFC 2253-style display string like .NET Subject ("CN=a, O=b"). */
|
|
192
|
+
export function renderSubject(x509) {
|
|
193
|
+
return String(x509.subject).split("\n").filter(Boolean).map((line) => {
|
|
194
|
+
const at = line.indexOf("=");
|
|
195
|
+
const key = line.slice(0, at); const value = line.slice(at + 1);
|
|
196
|
+
return `${key}=${needsQuote(value) ? `"${value.replace(/"/g, '""')}"` : value}`;
|
|
197
|
+
}).join(", ");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Read the signer certificate subject of a signed .msix / .msixbundle from AppxSignature.p7x. Throws MSIX_PUBLISHER_MISMATCH. */
|
|
201
|
+
export function readMsixSignerSubject(file) {
|
|
202
|
+
let p7x;
|
|
203
|
+
try { p7x = readZipEntry(file, "AppxSignature.p7x"); } catch (error) { throw fail(`could not read the signer certificate subject: ${error.message}`); }
|
|
204
|
+
if (!p7x) throw fail("could not read the signer certificate subject: AppxSignature.p7x is missing (package is unsigned)");
|
|
205
|
+
try {
|
|
206
|
+
if (p7x.length < 5 || p7x.subarray(0, 4).toString("latin1") !== "PKCX") throw new Error("AppxSignature.p7x lacks the PKCX header");
|
|
207
|
+
return renderSubject(signerCertificate(parseSignedData(p7x.subarray(4))));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw fail(`could not read the signer certificate subject: AppxSignature.p7x unparsable (${error.message})`);
|
|
210
|
+
}
|
|
211
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.154",
|
|
4
4
|
"description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
"!standalone-clone-evidence.json"
|
|
30
30
|
],
|
|
31
31
|
"sideEffects": false,
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
34
|
+
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
35
|
+
"test:signer:windows": "node sign-release-manifest.integration.mjs",
|
|
36
|
+
"prepublishOnly": "pnpm test && pnpm test:signer:windows && pnpm test:registry-parity",
|
|
37
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
38
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
39
|
+
},
|
|
32
40
|
"publishConfig": {
|
|
33
41
|
"registry": "https://registry.npmjs.org/",
|
|
34
42
|
"access": "public"
|
|
@@ -38,14 +46,8 @@
|
|
|
38
46
|
"url": "git+https://github.com/Orthic-Labs/rightkit.git",
|
|
39
47
|
"directory": "packages/release"
|
|
40
48
|
},
|
|
49
|
+
"packageManager": "pnpm@11.24.0",
|
|
41
50
|
"dependencies": {
|
|
42
51
|
"smol-toml": "1.9.0"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
46
|
-
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
47
|
-
"test:signer:windows": "node sign-release-manifest.integration.mjs",
|
|
48
|
-
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
49
|
-
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
50
52
|
}
|
|
51
|
-
}
|
|
53
|
+
}
|
package/release.mjs
CHANGED
|
File without changes
|
package/rightkit-versions.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@rightkit/platform-ui": "0.2.7",
|
|
28
28
|
"@rightkit/qa": "0.4.7",
|
|
29
29
|
"@rightkit/qa-chrome": "0.1.5",
|
|
30
|
-
"@rightkit/release": "0.2.
|
|
30
|
+
"@rightkit/release": "0.2.154",
|
|
31
31
|
"@rightkit/settings-ui": "0.4.3",
|
|
32
32
|
"@rightkit/tauri": "0.2.0",
|
|
33
33
|
"@rightkit/theme": "0.2.3"
|
package/upload-large.mjs
CHANGED
|
File without changes
|