@napi-rs/cli 3.10.4 → 3.10.6

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/src/utils/misc.ts CHANGED
@@ -3667,10 +3667,11 @@ type ProcessExecutionIdentityResolution = Awaited<
3667
3667
  ReturnType<typeof resolveProcessExecutionIdentityForLocking>
3668
3668
  >
3669
3669
 
3670
- const getCurrentProcessExecutionIdentity = createProcessExecutionIdentityGetter(
3671
- readProcessExecutionIdentity,
3672
- incompleteProcessExecutionIdentityCacheDuration,
3673
- )
3670
+ export const getCurrentProcessExecutionIdentity =
3671
+ createProcessExecutionIdentityGetter(
3672
+ readProcessExecutionIdentity,
3673
+ incompleteProcessExecutionIdentityCacheDuration,
3674
+ )
3674
3675
 
3675
3676
  const degradedProcessExecutionIdentityMemoDuration = 30_000
3676
3677
  // One build pays the probe-failure wait once: a degraded outcome is memoized
@@ -0,0 +1,646 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ //
3
+ // ohos-selfsign.ts — lightweight OpenHarmony binary self-signing tool.
4
+ //
5
+ // 1:1 port of `selfsign.js` from https://github.com/hqzing/ohos-selfsign,
6
+ // which reimplements the `binary-sign-tool` self-sign algorithm shipped in
7
+ // the OpenHarmony SDK: it injects a 4 KiB `.codesign` section into an ELF64
8
+ // binary holding an fs-verity descriptor plus a SHA-256 signature.
9
+ //
10
+ // Usage (as a library):
11
+ // signFileAtomic(path, force?) — sign an ELF file in place, atomically
12
+ // signElf(buffer, force?) — sign an ELF buffer, returns signed bytes
13
+ // stripCodesign(buffer) — remove the `.codesign` section
14
+ // checkSelfsign(buffer) — read-only validation of a self-signature
15
+ // selfsignMain(argv) — standalone entry:
16
+ // `selfsignMain(['--force', 'in.so'])`, `--strip`, `--check`
17
+
18
+ import { createHash } from 'node:crypto'
19
+ import {
20
+ chmodSync,
21
+ readFileSync,
22
+ renameSync,
23
+ statSync,
24
+ unlinkSync,
25
+ writeFileSync,
26
+ } from 'node:fs'
27
+
28
+ import { debugFactory } from './log.js'
29
+
30
+ const debug = debugFactory('ohos-selfsign')
31
+
32
+ const DESC_SIZE = 256
33
+ const PAGE_SIZE = 4096
34
+ const FLAG_SELF_SIGN = 0x10
35
+ const FS_VERITY_DESCRIPTOR_TYPE = 1
36
+ const HASH_OUT = 32 // SHA-256 output size in bytes
37
+
38
+ // ELF64 header field offsets
39
+ const E_SHOFF = 0x28
40
+ const E_SHENTSIZE = 0x3a
41
+ const E_SHNUM = 0x3c
42
+ const E_SHSTRNDX = 0x3e
43
+
44
+ const CODESIGN_NAME = Buffer.from('.codesign\0', 'latin1') // trailing NUL included, 10 bytes
45
+
46
+ // ─────────────────────── byte read/write helpers ───────────────────────
47
+ // Note: all offsets and 64-bit values use JS Numbers (safe integers up to
48
+ // 2^53), matching the type Node Buffer offset parameters require, and far
49
+ // beyond any real ELF file size.
50
+ function sha256(b: Buffer): Buffer {
51
+ return createHash('sha256').update(b).digest()
52
+ }
53
+
54
+ function readU16(buf: Buffer, off: number): number {
55
+ return buf.readUInt16LE(off)
56
+ }
57
+
58
+ function readU32(buf: Buffer, off: number): number {
59
+ return buf.readUInt32LE(off)
60
+ }
61
+
62
+ function readU64(buf: Buffer, off: number): number {
63
+ const lo = buf.readUInt32LE(off)
64
+ const hi = buf.readUInt32LE(off + 4)
65
+ return hi * 0x100000000 + lo
66
+ }
67
+
68
+ function writeU16(buf: Buffer, off: number, v: number): void {
69
+ buf.writeUInt16LE(v, off)
70
+ }
71
+
72
+ function writeU32(buf: Buffer, off: number, v: number): void {
73
+ buf.writeUInt32LE(v >>> 0, off)
74
+ }
75
+
76
+ function writeU64(buf: Buffer, off: number, v: number): void {
77
+ const lo = v >>> 0
78
+ const hi = Math.floor(v / 0x100000000)
79
+ buf.writeUInt32LE(lo, off)
80
+ buf.writeUInt32LE(hi, off + 4)
81
+ }
82
+
83
+ function alignUp(v: number, a: number): number {
84
+ return Math.ceil(v / a) * a
85
+ }
86
+
87
+ // ─────────────────── ELF pre-clean / normalize (not required for signing) ───────────────────
88
+ export interface ElfHeader {
89
+ e_shoff: number
90
+ e_shentsize: number
91
+ e_shnum: number
92
+ e_shstrndx: number
93
+ }
94
+
95
+ export function parseElfHeader(elf: Buffer): ElfHeader {
96
+ // Validate and parse the ELF64 header (read-only pre-check).
97
+ // ELF64 allows section entries larger than 64 bytes (carrying extended
98
+ // fields), so only entries smaller than 64 are rejected; the actual entry
99
+ // size is returned and every SHT walk/bounds check below strides by it.
100
+ if (
101
+ elf.length < 64 ||
102
+ elf[0] !== 0x7f ||
103
+ elf[1] !== 0x45 ||
104
+ elf[2] !== 0x4c ||
105
+ elf[3] !== 0x46 ||
106
+ elf[4] !== 2
107
+ ) {
108
+ throw new Error('not ELF64')
109
+ }
110
+ const e_shoff = readU64(elf, E_SHOFF)
111
+ const e_shentsize = readU16(elf, E_SHENTSIZE)
112
+ const e_shnum = readU16(elf, E_SHNUM)
113
+ const e_shstrndx = readU16(elf, E_SHSTRNDX)
114
+ if (
115
+ e_shentsize < 64 ||
116
+ e_shoff === 0 ||
117
+ e_shnum === 0 ||
118
+ e_shstrndx >= e_shnum
119
+ ) {
120
+ throw new Error('ELF has no usable section header table')
121
+ }
122
+ if (
123
+ e_shoff > elf.length ||
124
+ e_shnum > Math.floor((elf.length - e_shoff) / e_shentsize)
125
+ ) {
126
+ throw new Error('section header table out of bounds')
127
+ }
128
+ return { e_shoff, e_shentsize, e_shnum, e_shstrndx }
129
+ }
130
+
131
+ export function findSectionByName(
132
+ elf: Buffer,
133
+ e_shoff: number,
134
+ e_shentsize: number,
135
+ e_shnum: number,
136
+ e_shstrndx: number,
137
+ name: Buffer,
138
+ ): number {
139
+ // Look up a section by name in the SHT (read-only pre-check); entries are
140
+ // strided by the actual e_shentsize.
141
+ // Returns the section entry offset, or -1 when not found.
142
+ const name_len = name.length
143
+ const shstr_e = e_shoff + e_shstrndx * e_shentsize
144
+ const shstr_off = readU64(elf, shstr_e + 24)
145
+ const shstr_sz = readU64(elf, shstr_e + 32)
146
+ if (shstr_off > elf.length || shstr_sz > elf.length - shstr_off) {
147
+ return -1
148
+ }
149
+ for (let i = 0; i < e_shnum; i++) {
150
+ const e = e_shoff + i * e_shentsize
151
+ const name_off = readU32(elf, e)
152
+ if (name_off + name_len <= shstr_sz) {
153
+ const start = shstr_off + name_off
154
+ if (elf.subarray(start, start + name_len).equals(name)) {
155
+ return e
156
+ }
157
+ }
158
+ }
159
+ return -1
160
+ }
161
+
162
+ export function hasCodesignSection(elf: Buffer): boolean {
163
+ try {
164
+ const { e_shoff, e_shentsize, e_shnum, e_shstrndx } = parseElfHeader(elf)
165
+ return (
166
+ findSectionByName(
167
+ elf,
168
+ e_shoff,
169
+ e_shentsize,
170
+ e_shnum,
171
+ e_shstrndx,
172
+ CODESIGN_NAME,
173
+ ) >= 0
174
+ )
175
+ } catch {
176
+ return false
177
+ }
178
+ }
179
+
180
+ function newShstrndx(old_shstrndx: number, cs_idx: number): number {
181
+ return cs_idx < old_shstrndx ? old_shstrndx - 1 : old_shstrndx
182
+ }
183
+
184
+ export function stripCodesign(buf: Buffer): {
185
+ removed: boolean
186
+ out: Buffer
187
+ } {
188
+ // Strip the .codesign section.
189
+ const elf = Buffer.from(buf)
190
+ const { e_shoff, e_shentsize, e_shnum, e_shstrndx } = parseElfHeader(elf)
191
+
192
+ const cs_entry_off = findSectionByName(
193
+ elf,
194
+ e_shoff,
195
+ e_shentsize,
196
+ e_shnum,
197
+ e_shstrndx,
198
+ CODESIGN_NAME,
199
+ )
200
+ if (cs_entry_off < 0) {
201
+ return { removed: false, out: elf }
202
+ }
203
+ const cs_idx = (cs_entry_off - e_shoff) / e_shentsize
204
+
205
+ const shstr_e = e_shoff + e_shstrndx * e_shentsize
206
+ const shstr_off = readU64(elf, shstr_e + 24)
207
+ const shstr_sz = readU64(elf, shstr_e + 32)
208
+ if (shstr_off > elf.length || shstr_sz > elf.length - shstr_off) {
209
+ throw new Error('shstrtab out of bounds')
210
+ }
211
+
212
+ // 2. new shstrtab = old shstrtab with ".codesign\0" removed
213
+ const cs_name_off = readU32(elf, cs_entry_off)
214
+ const cs_name_len = CODESIGN_NAME.length // 10, NUL included
215
+ const newShstr = Buffer.from(elf.subarray(shstr_off, shstr_off + shstr_sz))
216
+ let newShstrSz = newShstr.length
217
+ if (cs_name_off + cs_name_len <= newShstr.length) {
218
+ newShstr.copyWithin(cs_name_off, cs_name_off + cs_name_len)
219
+ newShstrSz = newShstr.length - cs_name_len
220
+ }
221
+ const newShstrTrimmed = newShstr.subarray(0, newShstrSz)
222
+
223
+ // 3. new SHT = old SHT without the cs_idx entry (entries are not
224
+ // compacted to 64B; the original e_shentsize is preserved)
225
+ const newShnum = e_shnum - 1
226
+ const newSht = Buffer.alloc(newShnum * e_shentsize)
227
+ let dst = 0
228
+ for (let i = 0; i < e_shnum; i++) {
229
+ if (i === cs_idx) continue
230
+ const e = e_shoff + i * e_shentsize
231
+ elf.copy(newSht, dst, e, e + e_shentsize)
232
+ dst += e_shentsize
233
+ }
234
+
235
+ // 4. truncate at the .codesign section file offset, then append
236
+ // new shstrtab / 8-byte-aligned new SHT in order
237
+ const cs_sec_off = readU64(elf, cs_entry_off + 24)
238
+ const keep_len = Math.min(cs_sec_off, elf.length)
239
+ const new_shstr_off = keep_len
240
+ const new_sht_off = alignUp(new_shstr_off + newShstrSz, 8)
241
+ const new_total = new_sht_off + newShnum * e_shentsize
242
+
243
+ const out = Buffer.alloc(new_total)
244
+ elf.copy(out, 0, 0, keep_len)
245
+ newShstrTrimmed.copy(out, new_shstr_off)
246
+ newSht.copy(out, new_sht_off)
247
+
248
+ // 5. rewrite the shstrtab entry
249
+ const shstr_entry_off_in_new = newShstrndx(e_shstrndx, cs_idx) * e_shentsize
250
+ writeU64(out, new_sht_off + shstr_entry_off_in_new + 24, new_shstr_off)
251
+ writeU64(out, new_sht_off + shstr_entry_off_in_new + 32, newShstrSz)
252
+
253
+ // 6. shift every sh_name > cs_name_off back by cs_name_len
254
+ for (let i = 0; i < newShnum; i++) {
255
+ const e = new_sht_off + i * e_shentsize
256
+ const noff = readU32(out, e)
257
+ if (noff > cs_name_off) writeU32(out, e, noff - cs_name_len)
258
+ }
259
+
260
+ // 7. update the header
261
+ writeU64(out, E_SHOFF, new_sht_off)
262
+ writeU16(out, E_SHNUM, newShnum)
263
+ if (cs_idx < e_shstrndx) writeU16(out, E_SHSTRNDX, e_shstrndx - 1)
264
+
265
+ return { removed: true, out }
266
+ }
267
+
268
+ // ─────────────────── algorithm core required for signing ───────────────────
269
+ export function injectCodesignSection(elf: Buffer): {
270
+ out: Buffer
271
+ cs_off: number
272
+ } {
273
+ // Inject a 4 KiB placeholder .codesign section.
274
+ const { e_shoff, e_shentsize, e_shnum, e_shstrndx } = parseElfHeader(elf)
275
+
276
+ const shstr_e = e_shoff + e_shstrndx * e_shentsize
277
+ const shstr_off = readU64(elf, shstr_e + 24)
278
+ const shstr_sz = readU64(elf, shstr_e + 32)
279
+ if (shstr_off > elf.length || shstr_sz > elf.length - shstr_off) {
280
+ throw new Error('shstrtab out of bounds')
281
+ }
282
+
283
+ // 1. cur_end: max of the SHT end, every section's off+sz
284
+ // (SHT_NOBITS=8 occupies no file space), and the file length itself
285
+ // (extra data may trail the section header table)
286
+ let cur_end = e_shoff + e_shnum * e_shentsize
287
+ for (let i = 0; i < e_shnum; i++) {
288
+ const e = e_shoff + i * e_shentsize
289
+ const sh_type = readU32(elf, e + 4)
290
+ const off = readU64(elf, e + 24)
291
+ const sz = sh_type === 8 ? 0 : readU64(elf, e + 32)
292
+ if (off + sz > cur_end) cur_end = off + sz
293
+ }
294
+ if (elf.length > cur_end) cur_end = elf.length
295
+ const cs_off = alignUp(cur_end, PAGE_SIZE)
296
+
297
+ // 2. new shstrtab = old + ".codesign\0"
298
+ const newShstr = Buffer.concat([
299
+ elf.subarray(shstr_off, shstr_off + shstr_sz),
300
+ CODESIGN_NAME,
301
+ ])
302
+ const new_shstr_sz = newShstr.length
303
+ const cs_shname = shstr_sz // offset of .codesign inside the new shstrtab
304
+
305
+ // 3. new layout
306
+ const new_shstr_off = cs_off + PAGE_SIZE
307
+ const new_sht_off = alignUp(new_shstr_off + new_shstr_sz, 8)
308
+ const new_shnum = e_shnum + 1
309
+ const new_total = new_sht_off + new_shnum * e_shentsize
310
+
311
+ const buf = Buffer.alloc(new_total)
312
+ // 4. copy original content: only up to cs_off
313
+ const copy_len = Math.min(elf.length, new_total, cs_off)
314
+ elf.copy(buf, 0, 0, copy_len)
315
+
316
+ newShstr.copy(buf, new_shstr_off)
317
+ elf.copy(buf, new_sht_off, e_shoff, e_shoff + e_shnum * e_shentsize)
318
+
319
+ // .codesign section entry (first 64B are the standard fields; any
320
+ // extension beyond 64B stays zero)
321
+ const cs_e = new_sht_off + e_shnum * e_shentsize
322
+ writeU32(buf, cs_e + 0, cs_shname) // sh_name
323
+ writeU32(buf, cs_e + 4, 1) // sh_type = SHT_PROGBITS
324
+ writeU64(buf, cs_e + 24, cs_off) // sh_offset
325
+ writeU64(buf, cs_e + 32, PAGE_SIZE) // sh_size
326
+ writeU64(buf, cs_e + 48, PAGE_SIZE) // sh_addralign
327
+
328
+ // update shstrtab entry offset/size
329
+ const shstr_e_new = new_sht_off + e_shstrndx * e_shentsize
330
+ writeU64(buf, shstr_e_new + 24, new_shstr_off)
331
+ writeU64(buf, shstr_e_new + 32, new_shstr_sz)
332
+
333
+ // update header: e_shoff / e_shnum; e_shstrndx unchanged
334
+ writeU64(buf, E_SHOFF, new_sht_off)
335
+ writeU16(buf, E_SHNUM, new_shnum)
336
+
337
+ return { out: buf, cs_off }
338
+ }
339
+
340
+ export function merkleRootHash(
341
+ data: Buffer,
342
+ cs_off: number,
343
+ cs_len: number,
344
+ ): { root: Buffer; mid: Buffer } {
345
+ // fs-verity Merkle tree root hash + intermediate-level hashes.
346
+ // `mid` holds every level between the leaf level and the level containing
347
+ // the root, concatenated bottom-up as raw 32B hashes (absent when the leaf
348
+ // level fits in 4096B); it is written after the payload inside .codesign.
349
+ if (data.length === 0) {
350
+ return { root: sha256(Buffer.alloc(PAGE_SIZE)), mid: Buffer.alloc(0) }
351
+ }
352
+
353
+ const npages = Math.ceil(data.length / PAGE_SIZE)
354
+ const cs_page_begin = Math.floor(cs_off / PAGE_SIZE)
355
+ const cs_page_end = Math.ceil((cs_off + cs_len) / PAGE_SIZE)
356
+
357
+ const hashes: Buffer[] = []
358
+ for (let i = 0; i < npages; i++) {
359
+ if (cs_len > 0 && cs_page_begin <= i && i < cs_page_end) {
360
+ hashes.push(Buffer.alloc(HASH_OUT)) // pages under the section: zero leaf hash
361
+ continue
362
+ }
363
+ let page = data.subarray(i * PAGE_SIZE, (i + 1) * PAGE_SIZE)
364
+ if (page.length < PAGE_SIZE) {
365
+ page = Buffer.concat([page, Buffer.alloc(PAGE_SIZE - page.length)])
366
+ }
367
+ hashes.push(sha256(page))
368
+ }
369
+
370
+ if (npages === 1) {
371
+ return { root: Buffer.from(hashes[0]), mid: Buffer.alloc(0) }
372
+ }
373
+
374
+ let cur = Buffer.concat(hashes)
375
+ const mid: Buffer[] = [] // intermediate levels below the root level, bottom-up
376
+ while (cur.length > PAGE_SIZE) {
377
+ const nxt: Buffer[] = []
378
+ for (let i = 0; i < cur.length; i += PAGE_SIZE) {
379
+ let page = cur.subarray(i, i + PAGE_SIZE)
380
+ if (page.length < PAGE_SIZE) {
381
+ page = Buffer.concat([page, Buffer.alloc(PAGE_SIZE - page.length)])
382
+ }
383
+ nxt.push(sha256(page))
384
+ }
385
+ cur = Buffer.concat(nxt)
386
+ if (cur.length > PAGE_SIZE) {
387
+ mid.push(cur) // the level containing the root (packed <= 4096) is not intermediate
388
+ }
389
+ }
390
+ const page = Buffer.concat([cur, Buffer.alloc(PAGE_SIZE - cur.length)])
391
+ return { root: sha256(page), mid: Buffer.concat(mid) }
392
+ }
393
+
394
+ export function buildDescriptor(
395
+ sign_size: number,
396
+ file_size: number,
397
+ root: Buffer,
398
+ flags: number,
399
+ ): Buffer {
400
+ // Build the 256-byte fs-verity descriptor
401
+ const d = Buffer.alloc(DESC_SIZE)
402
+ d[0] = 1 // version
403
+ d[1] = 1 // hashAlgorithm = SHA-256
404
+ d[2] = 12 // log2BlockSize = 2^12 = 4096
405
+ d[3] = 0 // saltSize
406
+ writeU32(d, 4, sign_size)
407
+ writeU64(d, 8, file_size)
408
+ root.copy(d, 16) // rootHash left-aligned into 64B, last 32B stay zero
409
+ writeU32(d, 112, flags)
410
+ d[255] = 3 // csVersion
411
+ return d
412
+ }
413
+
414
+ export function signElf(elf: Buffer, force = false): Buffer {
415
+ // Main signing flow
416
+ if (
417
+ elf.length < 64 ||
418
+ elf[0] !== 0x7f ||
419
+ elf[1] !== 0x45 ||
420
+ elf[2] !== 0x4c ||
421
+ elf[3] !== 0x46 ||
422
+ elf[4] !== 2
423
+ ) {
424
+ throw new Error('not ELF64')
425
+ }
426
+
427
+ let buf: Buffer = Buffer.from(elf)
428
+ if (hasCodesignSection(buf)) {
429
+ if (!force) {
430
+ throw new Error(
431
+ 'already has a .codesign section; strip first or use --force',
432
+ )
433
+ }
434
+ buf = stripCodesign(buf).out
435
+ }
436
+
437
+ // 1. inject a 4 KiB placeholder .codesign section
438
+ const { out: tmp0, cs_off } = injectCodesignSection(buf)
439
+ const file_size = tmp0.length
440
+
441
+ // 2. merkle root hash; mid holds the intermediate-level hashes
442
+ const { root, mid } = merkleRootHash(tmp0, cs_off, PAGE_SIZE)
443
+
444
+ // 3/4. descriptor(signSize=0) used for the digest
445
+ const desc_for_digest = buildDescriptor(0, file_size, root, FLAG_SELF_SIGN)
446
+ // 5. signature = SHA256(descriptor)
447
+ const signature = sha256(desc_for_digest)
448
+ // 6. descriptor(signSize=32) stored on disk
449
+ const desc_on_disk = buildDescriptor(32, file_size, root, FLAG_SELF_SIGN)
450
+
451
+ // 7. ElfSignInfo: 8B header + 256B descriptor + 32B signature = 296B
452
+ const payload = Buffer.alloc(4 + 4 + DESC_SIZE + HASH_OUT)
453
+ writeU32(payload, 0, FS_VERITY_DESCRIPTOR_TYPE) // type
454
+ writeU32(payload, 4, DESC_SIZE + HASH_OUT) // length = 288
455
+ desc_on_disk.copy(payload, 8)
456
+ signature.copy(payload, 8 + DESC_SIZE)
457
+
458
+ // 8. write into the section in place: the intermediate-level hashes follow
459
+ // the payload; capacity is 4096-296=3800B, so a longer `mid` keeps only
460
+ // its first 3800B (the leaf-level side)
461
+ payload.copy(tmp0, cs_off)
462
+ mid.copy(tmp0, cs_off + payload.length, 0, PAGE_SIZE - payload.length)
463
+ return tmp0
464
+ }
465
+
466
+ // ─────────────────── read-only validation (--check) ───────────────────
467
+ export function checkSelfsign(elf: Buffer): {
468
+ ok: boolean
469
+ reason: string | null
470
+ } {
471
+ // Read-only validation of an existing self-signature (--check).
472
+ // Checks (all must pass; the first mismatch is reported):
473
+ // 1. parseable ELF64 (all parseElfHeader checks), a .codesign section
474
+ // whose ElfSignInfo header has type=1 / length=288
475
+ // 2. descriptor fixed fields: version=1, hashAlgorithm=1,
476
+ // log2BlockSize=12, csVersion=3
477
+ // 3. signSize=32 and fileSize=actual file size
478
+ // 4. stored merkle root hash == root recomputed over the whole file
479
+ // 5. stored signature == SHA256(descriptor with signSize zeroed)
480
+ // The merkle intermediate levels (the area after the 296B payload) are
481
+ // intentionally not verified: the pages covered by .codesign are already
482
+ // zeroed at the leaf level, so tampering with them cannot change the root
483
+ // and this check still passes (expected behavior).
484
+ // Returns { ok, reason }; reason is the cause string when ok is false.
485
+
486
+ // 1a. ELF64 pre-check (parse failure includes unusable SHT)
487
+ let hdr: ElfHeader
488
+ try {
489
+ hdr = parseElfHeader(elf)
490
+ } catch {
491
+ return { ok: false, reason: 'not ELF64' }
492
+ }
493
+
494
+ // 1b. locate the .codesign section; it must hold at least the 296B payload
495
+ const cs_entry_off = findSectionByName(
496
+ elf,
497
+ hdr.e_shoff,
498
+ hdr.e_shentsize,
499
+ hdr.e_shnum,
500
+ hdr.e_shstrndx,
501
+ CODESIGN_NAME,
502
+ )
503
+ if (cs_entry_off < 0) {
504
+ return { ok: false, reason: 'no .codesign section' }
505
+ }
506
+ const cs_off = readU64(elf, cs_entry_off + 24)
507
+ const cs_size = readU64(elf, cs_entry_off + 32)
508
+ const payload_len = 8 + DESC_SIZE + HASH_OUT
509
+ if (cs_size < payload_len || cs_off + payload_len > elf.length) {
510
+ return { ok: false, reason: 'bad ElfSignInfo header' }
511
+ }
512
+
513
+ // 1c. ElfSignInfo header: type=1, length=288
514
+ if (
515
+ readU32(elf, cs_off) !== FS_VERITY_DESCRIPTOR_TYPE ||
516
+ readU32(elf, cs_off + 4) !== DESC_SIZE + HASH_OUT
517
+ ) {
518
+ return { ok: false, reason: 'bad ElfSignInfo header' }
519
+ }
520
+
521
+ const desc = elf.subarray(cs_off + 8, cs_off + 8 + DESC_SIZE)
522
+ const sig = elf.subarray(cs_off + 8 + DESC_SIZE, cs_off + payload_len)
523
+
524
+ // 2. descriptor fixed fields: version/hashAlgorithm/log2BlockSize/csVersion
525
+ if (desc[0] !== 1 || desc[1] !== 1 || desc[2] !== 12 || desc[255] !== 3) {
526
+ return { ok: false, reason: 'unsupported descriptor fields' }
527
+ }
528
+
529
+ // 3. signSize=32 and fileSize=actual file size
530
+ if (readU32(desc, 4) !== HASH_OUT) {
531
+ return { ok: false, reason: 'signSize mismatch' }
532
+ }
533
+ if (readU64(desc, 8) !== elf.length) {
534
+ return { ok: false, reason: 'fileSize mismatch' }
535
+ }
536
+
537
+ // 4. merkle root hash: recompute the root over the whole file
538
+ // (intermediate levels ignored)
539
+ const { root } = merkleRootHash(elf, cs_off, PAGE_SIZE)
540
+ if (!desc.subarray(16, 16 + HASH_OUT).equals(root)) {
541
+ return { ok: false, reason: 'merkle root mismatch' }
542
+ }
543
+
544
+ // 5. signature == SHA256(descriptor with signSize zeroed)
545
+ const desc0 = Buffer.from(desc)
546
+ desc0.fill(0, 4, 8)
547
+ if (!sig.equals(sha256(desc0))) {
548
+ return { ok: false, reason: 'signature mismatch' }
549
+ }
550
+
551
+ return { ok: true, reason: null }
552
+ }
553
+
554
+ // ─────────────────── file I/O layer ───────────────────
555
+ export function signFileAtomic(path: string, force = false): void {
556
+ const raw = readFileSync(path)
557
+ const signed = signElf(raw, force)
558
+
559
+ let mode: number | null = null
560
+ try {
561
+ mode = statSync(path).mode & 0o7777
562
+ } catch {
563
+ // ignore
564
+ }
565
+
566
+ const tmp_path = `${path}.ohos-signing.${process.pid}.tmp`
567
+ try {
568
+ unlinkSync(tmp_path)
569
+ } catch {
570
+ /* ignore */
571
+ }
572
+ writeFileSync(tmp_path, signed)
573
+ if (mode !== null) chmodSync(tmp_path, mode)
574
+ renameSync(tmp_path, path)
575
+ }
576
+
577
+ export function selfsignMain(argv: string[]): number {
578
+ let force = false
579
+ let strip_only = false
580
+ let check_only = false
581
+ const positional: string[] = []
582
+ for (const a of argv) {
583
+ if (a === '--force' || a === '-f') force = true
584
+ else if (a === '--strip') strip_only = true
585
+ else if (a === '--check') check_only = true
586
+ else positional.push(a)
587
+ }
588
+ // --check combined with an output path/--force/--strip is a usage error
589
+ if (
590
+ positional.length < 1 ||
591
+ positional.length > 2 ||
592
+ (check_only && (force || strip_only || positional.length === 2))
593
+ ) {
594
+ debug.error(
595
+ 'usage: selfsign <input_elf> [output_elf] [--force] [--strip] [--check]\n' +
596
+ ' (output defaults to input, in-place)',
597
+ )
598
+ return 1
599
+ }
600
+ const in_path = positional[0]
601
+ const out_path = positional.length === 2 ? positional[1] : in_path
602
+
603
+ try {
604
+ if (check_only) {
605
+ // --check: read-only validation, writes nothing
606
+ const raw = readFileSync(in_path)
607
+ const { ok, reason } = checkSelfsign(raw)
608
+ if (ok) {
609
+ debug.info(`check ok: ${in_path}`)
610
+ return 0
611
+ }
612
+ debug.error(`check failed: ${in_path} (${reason})`)
613
+ return 1
614
+ }
615
+
616
+ if (strip_only) {
617
+ const raw = readFileSync(in_path)
618
+ const { removed, out } = stripCodesign(Buffer.from(raw))
619
+ if (!removed) {
620
+ debug.info(`no .codesign section to strip: ${in_path}`)
621
+ return 0
622
+ }
623
+ writeFileSync(out_path, out)
624
+ debug.info(`strip ok: ${in_path} → ${out_path} (${out.length} bytes)`)
625
+ return 0
626
+ }
627
+
628
+ if (in_path === out_path) {
629
+ signFileAtomic(in_path, force)
630
+ debug.info(
631
+ `selfsign ok: ${in_path} (in-place, ${force ? 'force' : 'append-only'})`,
632
+ )
633
+ } else {
634
+ const raw = readFileSync(in_path)
635
+ const signed = signElf(raw, force)
636
+ writeFileSync(out_path, signed)
637
+ debug.info(
638
+ `selfsign ok: ${in_path} → ${out_path} (${signed.length} bytes)`,
639
+ )
640
+ }
641
+ } catch (e) {
642
+ debug.error(e)
643
+ return 2
644
+ }
645
+ return 0
646
+ }