@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/dist/cli.js +1943 -153
- package/dist/index.cjs +1942 -152
- package/dist/index.d.cts +19 -7
- package/dist/index.d.ts +19 -7
- package/dist/index.js +1943 -153
- package/docs/build.md +1 -0
- package/docs/wasi.md +351 -8
- package/package.json +2 -2
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +2629 -86
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/templates.spec.ts +2192 -10
- package/src/api/build.ts +38 -1
- package/src/api/templates/load-wasi-template.ts +1412 -30
- package/src/api/templates/wasi-worker-template.ts +191 -63
- package/src/commands/__tests__/build.spec.ts +6 -0
- package/src/def/build.ts +13 -0
- package/src/utils/__tests__/ohos-selfsign.spec.ts +285 -0
- package/src/utils/__tests__/reconciliation.spec.ts +19 -0
- package/src/utils/config.ts +10 -6
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +5 -4
- package/src/utils/ohos-selfsign.ts +646 -0
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 =
|
|
3671
|
-
|
|
3672
|
-
|
|
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
|
+
}
|