@particle/tachyon-image 0.1.0
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/README.md +35 -0
- package/dist/bin/particle-image.d.ts +2 -0
- package/dist/bin/particle-image.js +135 -0
- package/dist/format/expand.d.ts +18 -0
- package/dist/format/expand.js +49 -0
- package/dist/hash/index.d.ts +19 -0
- package/dist/hash/index.js +78 -0
- package/dist/import/factoryDir.d.ts +9 -0
- package/dist/import/factoryDir.js +226 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +53 -0
- package/dist/manifest/imageManifestV1.d.ts +36 -0
- package/dist/manifest/imageManifestV1.js +30 -0
- package/dist/manifest/particleImageV1.d.ts +78 -0
- package/dist/manifest/particleImageV1.js +21 -0
- package/dist/model/types.d.ts +140 -0
- package/dist/model/types.js +48 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/index.js +35 -0
- package/dist/signing/jcs.d.ts +9 -0
- package/dist/signing/jcs.js +31 -0
- package/dist/signing/sign.d.ts +15 -0
- package/dist/signing/sign.js +44 -0
- package/dist/slots/index.d.ts +64 -0
- package/dist/slots/index.js +111 -0
- package/dist/sparse/decode.d.ts +29 -0
- package/dist/sparse/decode.js +91 -0
- package/dist/validate/index.d.ts +26 -0
- package/dist/validate/index.js +120 -0
- package/dist/xml/rawprogram.d.ts +34 -0
- package/dist/xml/rawprogram.js +149 -0
- package/dist/zip/index.d.ts +18 -0
- package/dist/zip/index.js +93 -0
- package/package.json +44 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy Qualcomm-style `image_manifest_v1` — the manifest.json that ships in
|
|
3
|
+
* the factory zip today and that `particle flash --tachyon` already parses
|
|
4
|
+
* (targets[].qcm6490.edl.{base,firehose,program_xml[],patch_xml[]}). The lib
|
|
5
|
+
* must read AND emit it for back-compat while the new particle_image_v1 rolls out.
|
|
6
|
+
*/
|
|
7
|
+
export interface EdlTarget {
|
|
8
|
+
base: string;
|
|
9
|
+
firehose: string;
|
|
10
|
+
program_xml: string[];
|
|
11
|
+
patch_xml: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface ImageManifestV1 {
|
|
14
|
+
$schema?: string;
|
|
15
|
+
release_name?: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
region?: string;
|
|
18
|
+
variant?: string;
|
|
19
|
+
platform?: string;
|
|
20
|
+
board?: string;
|
|
21
|
+
os?: string;
|
|
22
|
+
distribution?: string;
|
|
23
|
+
distribution_version?: string;
|
|
24
|
+
distribution_variant?: string;
|
|
25
|
+
sources?: unknown;
|
|
26
|
+
targets: Array<Record<string, {
|
|
27
|
+
edl: EdlTarget;
|
|
28
|
+
}>>;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseImageManifestV1(json: string | object): ImageManifestV1;
|
|
31
|
+
/** Extract the single EDL target block (platform-keyed) from the manifest. */
|
|
32
|
+
export declare function edlOf(m: ImageManifestV1): {
|
|
33
|
+
platform: string;
|
|
34
|
+
edl: EdlTarget;
|
|
35
|
+
};
|
|
36
|
+
export declare function buildImageManifestV1(m: ImageManifestV1): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Legacy Qualcomm-style `image_manifest_v1` — the manifest.json that ships in
|
|
4
|
+
* the factory zip today and that `particle flash --tachyon` already parses
|
|
5
|
+
* (targets[].qcm6490.edl.{base,firehose,program_xml[],patch_xml[]}). The lib
|
|
6
|
+
* must read AND emit it for back-compat while the new particle_image_v1 rolls out.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parseImageManifestV1 = parseImageManifestV1;
|
|
10
|
+
exports.edlOf = edlOf;
|
|
11
|
+
exports.buildImageManifestV1 = buildImageManifestV1;
|
|
12
|
+
function parseImageManifestV1(json) {
|
|
13
|
+
const m = typeof json === 'string' ? JSON.parse(json) : json;
|
|
14
|
+
if (!Array.isArray(m.targets) || m.targets.length === 0) {
|
|
15
|
+
throw new Error('image_manifest_v1: missing targets[]');
|
|
16
|
+
}
|
|
17
|
+
return m;
|
|
18
|
+
}
|
|
19
|
+
/** Extract the single EDL target block (platform-keyed) from the manifest. */
|
|
20
|
+
function edlOf(m) {
|
|
21
|
+
const target = m.targets[0];
|
|
22
|
+
const platform = Object.keys(target)[0];
|
|
23
|
+
const edl = target[platform]?.edl;
|
|
24
|
+
if (!edl)
|
|
25
|
+
throw new Error('image_manifest_v1: target has no edl block');
|
|
26
|
+
return { platform, edl };
|
|
27
|
+
}
|
|
28
|
+
function buildImageManifestV1(m) {
|
|
29
|
+
return JSON.stringify(m, null, 4) + '\n';
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Particle-owned `particle_image_v1` manifest — first entry of the image
|
|
3
|
+
* ZIP. It carries the per-partition operation graph + hashes + an embedded
|
|
4
|
+
* ed25519/JCS signature, and a `format` capability descriptor declaring which
|
|
5
|
+
* of factory / ota-image / ota-boot this archive can produce.
|
|
6
|
+
*/
|
|
7
|
+
import type { Compression, FormatKind, Group, Role, Slot, SigningInfo, GptPatch, ChunkRef } from '../model/types';
|
|
8
|
+
export declare const PARTICLE_IMAGE_SCHEMA = "https://linux-dist.particle.io/schema/particle_image_v1.json";
|
|
9
|
+
/** Serialized form of a PartitionOp inside the manifest (camelCase preserved). */
|
|
10
|
+
export type ManifestOp = {
|
|
11
|
+
op: 'program';
|
|
12
|
+
source?: string;
|
|
13
|
+
chunks?: ChunkRef[];
|
|
14
|
+
sparse?: boolean;
|
|
15
|
+
compression?: Compression;
|
|
16
|
+
uncompressed_size?: number;
|
|
17
|
+
file_sector_offset?: number;
|
|
18
|
+
sha256?: string;
|
|
19
|
+
payload_sha256?: string;
|
|
20
|
+
} | {
|
|
21
|
+
op: 'erase';
|
|
22
|
+
} | {
|
|
23
|
+
op: 'zero';
|
|
24
|
+
source?: string;
|
|
25
|
+
} | {
|
|
26
|
+
op: 'gpt';
|
|
27
|
+
which: 'primary' | 'backup';
|
|
28
|
+
source: string;
|
|
29
|
+
} | {
|
|
30
|
+
op: 'patch';
|
|
31
|
+
patches: GptPatch[];
|
|
32
|
+
};
|
|
33
|
+
export interface ManifestPartition {
|
|
34
|
+
label: string;
|
|
35
|
+
lun: number;
|
|
36
|
+
slot: Slot;
|
|
37
|
+
role: Role;
|
|
38
|
+
group: Group;
|
|
39
|
+
size?: number;
|
|
40
|
+
start_sector?: number;
|
|
41
|
+
num_partition_sectors?: number;
|
|
42
|
+
type_guid?: string;
|
|
43
|
+
signed?: boolean;
|
|
44
|
+
image_id?: string | null;
|
|
45
|
+
ops: ManifestOp[];
|
|
46
|
+
}
|
|
47
|
+
export interface ParticleImageV1 {
|
|
48
|
+
$schema: string;
|
|
49
|
+
schema_version: 1;
|
|
50
|
+
release_name?: string;
|
|
51
|
+
version?: string;
|
|
52
|
+
platform: string;
|
|
53
|
+
board?: string;
|
|
54
|
+
region?: string;
|
|
55
|
+
variant?: string;
|
|
56
|
+
distribution?: string;
|
|
57
|
+
distribution_version?: string;
|
|
58
|
+
sector_size: number;
|
|
59
|
+
firehose?: string;
|
|
60
|
+
format: {
|
|
61
|
+
kind: FormatKind;
|
|
62
|
+
emittable: FormatKind[];
|
|
63
|
+
slots_present: Slot[];
|
|
64
|
+
groups: Partial<Record<Group, string[]>>;
|
|
65
|
+
};
|
|
66
|
+
partitions: ManifestPartition[];
|
|
67
|
+
signing: {
|
|
68
|
+
profile: SigningInfo['profile'];
|
|
69
|
+
key_id?: string;
|
|
70
|
+
algorithm: 'ed25519';
|
|
71
|
+
digest_algorithm: 'sha256';
|
|
72
|
+
canonicalization: 'jcs';
|
|
73
|
+
signature?: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export declare const MANIFEST_ENTRY_NAME = "particle-image.json";
|
|
77
|
+
export declare function parseParticleImageV1(json: string | object): ParticleImageV1;
|
|
78
|
+
export declare function buildParticleImageV1(m: ParticleImageV1): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MANIFEST_ENTRY_NAME = exports.PARTICLE_IMAGE_SCHEMA = void 0;
|
|
4
|
+
exports.parseParticleImageV1 = parseParticleImageV1;
|
|
5
|
+
exports.buildParticleImageV1 = buildParticleImageV1;
|
|
6
|
+
exports.PARTICLE_IMAGE_SCHEMA = 'https://linux-dist.particle.io/schema/particle_image_v1.json';
|
|
7
|
+
exports.MANIFEST_ENTRY_NAME = 'particle-image.json';
|
|
8
|
+
function parseParticleImageV1(json) {
|
|
9
|
+
const m = (typeof json === 'string' ? JSON.parse(json) : json);
|
|
10
|
+
if (m.schema_version !== 1) {
|
|
11
|
+
throw new Error(`particle_image_v1: unsupported schema_version ${m.schema_version}`);
|
|
12
|
+
}
|
|
13
|
+
if (!Array.isArray(m.partitions))
|
|
14
|
+
throw new Error('particle_image_v1: missing partitions[]');
|
|
15
|
+
if (!m.format?.groups)
|
|
16
|
+
throw new Error('particle_image_v1: missing format.groups');
|
|
17
|
+
return m;
|
|
18
|
+
}
|
|
19
|
+
function buildParticleImageV1(m) {
|
|
20
|
+
return JSON.stringify(m, null, 2) + '\n';
|
|
21
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operation-graph model — the single in-memory representation that every
|
|
3
|
+
* importer (Qualcomm manifest/rawprogram/patch XML, particle_image_v1) parses
|
|
4
|
+
* INTO and every exporter (qdl XML, particle_image_v1 zip) builds FROM.
|
|
5
|
+
*
|
|
6
|
+
* It is deliberately a superset of the Qualcomm rawprogram/patch grammar so we
|
|
7
|
+
* can losslessly round-trip a real factory image AND describe Particle-specific
|
|
8
|
+
* OTA semantics (slots, groups, per-partition hashes, signing).
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_SECTOR_SIZE = 4096;
|
|
11
|
+
export type Slot = 'a' | 'b' | 'none';
|
|
12
|
+
export type Role = 'boot' | 'firmware' | 'efi' | 'system' | 'userdata' | 'persist' | 'nvm' | 'gpt' | 'other';
|
|
13
|
+
export type Group = 'BOOT' | 'FIRMWARE' | 'A' | 'B' | 'USER' | 'NVM' | 'OTHER';
|
|
14
|
+
export type Compression = 'none' | 'gzip' | 'zstd';
|
|
15
|
+
export type FormatKind = 'factory' | 'ota-image' | 'ota-boot';
|
|
16
|
+
/**
|
|
17
|
+
* One contiguous chunk of a partition payload. The real factory writes the OS
|
|
18
|
+
* as ~61 sequential <program> chunks (sysfs_1.ext4 …); each is a ChunkRef.
|
|
19
|
+
*/
|
|
20
|
+
export interface ChunkRef {
|
|
21
|
+
/** Entry name (inside the zip) or filesystem path of the payload chunk. */
|
|
22
|
+
source: string;
|
|
23
|
+
/** Absolute LBA (LUN-relative) where this chunk is written. */
|
|
24
|
+
startSector: number;
|
|
25
|
+
/** Number of sectors this chunk occupies on the partition. */
|
|
26
|
+
numSectors: number;
|
|
27
|
+
/** Sector offset into the source file (rawprogram file_sector_offset). */
|
|
28
|
+
fileSectorOffset?: number;
|
|
29
|
+
/** Android sparse image. */
|
|
30
|
+
sparse?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** A GPT header/table patch directive (rawprogram patch*.xml <patch>). */
|
|
33
|
+
export interface GptPatch {
|
|
34
|
+
/** May be a literal LBA or an expression, e.g. "NUM_DISK_SECTORS-5.". */
|
|
35
|
+
startSector: number | string;
|
|
36
|
+
byteOffset: number;
|
|
37
|
+
sizeBytes: number;
|
|
38
|
+
/** Literal or expression, e.g. "NUM_DISK_SECTORS-6." or "CRC32(2,4096)". */
|
|
39
|
+
value: string;
|
|
40
|
+
/** Target file, e.g. "gpt_main0.bin" or "DISK". */
|
|
41
|
+
filename: string;
|
|
42
|
+
what?: string;
|
|
43
|
+
}
|
|
44
|
+
/** A single operation applied to a partition, in order. */
|
|
45
|
+
export type PartitionOp = {
|
|
46
|
+
op: 'program';
|
|
47
|
+
/** Single-file payload (mutually exclusive with chunks). */
|
|
48
|
+
source?: string;
|
|
49
|
+
/** Multi-chunk payload (the sysfs_N pattern). */
|
|
50
|
+
chunks?: ChunkRef[];
|
|
51
|
+
sparse?: boolean;
|
|
52
|
+
compression?: Compression;
|
|
53
|
+
/** Size of the decompressed stream handed to the writer. */
|
|
54
|
+
uncompressedSize?: number;
|
|
55
|
+
fileSectorOffset?: number;
|
|
56
|
+
/** sha256 of the (possibly compressed) zip entry bytes. */
|
|
57
|
+
sha256?: string;
|
|
58
|
+
/** sha256 of the fully-expanded raw image (holes as zeros). */
|
|
59
|
+
payloadSha256?: string;
|
|
60
|
+
} | {
|
|
61
|
+
op: 'erase';
|
|
62
|
+
} | {
|
|
63
|
+
op: 'zero';
|
|
64
|
+
source?: string;
|
|
65
|
+
} | {
|
|
66
|
+
op: 'gpt';
|
|
67
|
+
which: 'primary' | 'backup';
|
|
68
|
+
source: string;
|
|
69
|
+
} | {
|
|
70
|
+
op: 'patch';
|
|
71
|
+
patches: GptPatch[];
|
|
72
|
+
};
|
|
73
|
+
export interface Partition {
|
|
74
|
+
label: string;
|
|
75
|
+
lun: number;
|
|
76
|
+
slot: Slot;
|
|
77
|
+
role: Role;
|
|
78
|
+
group: Group;
|
|
79
|
+
/** Declared size in bytes (from partition_ext size_in_kb); 0/undefined = grow. */
|
|
80
|
+
sizeBytes?: number;
|
|
81
|
+
startSector?: number;
|
|
82
|
+
numSectors?: number;
|
|
83
|
+
typeGuid?: string;
|
|
84
|
+
readonly?: boolean;
|
|
85
|
+
/** Qualcomm-signed boot/firmware blob. */
|
|
86
|
+
signed?: boolean;
|
|
87
|
+
/** sectoolsv2 image-id when signed (e.g. "UEFI"). */
|
|
88
|
+
imageId?: string;
|
|
89
|
+
ops: PartitionOp[];
|
|
90
|
+
}
|
|
91
|
+
export interface SigningInfo {
|
|
92
|
+
profile: 'test' | 'prod' | 'none';
|
|
93
|
+
keyId?: string;
|
|
94
|
+
algorithm: 'ed25519';
|
|
95
|
+
digestAlgorithm: 'sha256';
|
|
96
|
+
canonicalization: 'jcs';
|
|
97
|
+
/** base64 signature over the canonical digest of the manifest (blank sig). */
|
|
98
|
+
signature?: string;
|
|
99
|
+
}
|
|
100
|
+
export interface ImageMeta {
|
|
101
|
+
schemaVersion: 1;
|
|
102
|
+
releaseName?: string;
|
|
103
|
+
version?: string;
|
|
104
|
+
platform: string;
|
|
105
|
+
board?: string;
|
|
106
|
+
region?: string;
|
|
107
|
+
variant?: string;
|
|
108
|
+
os?: string;
|
|
109
|
+
distribution?: string;
|
|
110
|
+
distributionVersion?: string;
|
|
111
|
+
sectorSize: number;
|
|
112
|
+
}
|
|
113
|
+
/** Declares which emit formats this image can produce and its partition groups. */
|
|
114
|
+
export interface FormatDescriptor {
|
|
115
|
+
kind: FormatKind;
|
|
116
|
+
emittable: FormatKind[];
|
|
117
|
+
slotsPresent: Slot[];
|
|
118
|
+
/** group name -> ordered partition labels */
|
|
119
|
+
groups: Partial<Record<Group, string[]>>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The whole image. Partitions are stored flat (each carries its `lun`); group
|
|
123
|
+
* by lun with `partitionsByLun()` when emitting per-LUN rawprogram files.
|
|
124
|
+
*/
|
|
125
|
+
export interface ImageModel {
|
|
126
|
+
meta: ImageMeta;
|
|
127
|
+
format: FormatDescriptor;
|
|
128
|
+
/** In flash order (LUN1, LUN2, LUN4, LUN0, LUN5). */
|
|
129
|
+
partitions: Partition[];
|
|
130
|
+
signing: SigningInfo;
|
|
131
|
+
/** Firehose programmer filename (prog_firehose_*.elf). */
|
|
132
|
+
firehose?: string;
|
|
133
|
+
}
|
|
134
|
+
export declare function partitionsByLun(model: ImageModel): Map<number, Partition[]>;
|
|
135
|
+
/** Derive slot/role/group from a partition label and its LUN. */
|
|
136
|
+
export declare function classify(label: string, lun: number): {
|
|
137
|
+
slot: Slot;
|
|
138
|
+
role: Role;
|
|
139
|
+
group: Group;
|
|
140
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The operation-graph model — the single in-memory representation that every
|
|
4
|
+
* importer (Qualcomm manifest/rawprogram/patch XML, particle_image_v1) parses
|
|
5
|
+
* INTO and every exporter (qdl XML, particle_image_v1 zip) builds FROM.
|
|
6
|
+
*
|
|
7
|
+
* It is deliberately a superset of the Qualcomm rawprogram/patch grammar so we
|
|
8
|
+
* can losslessly round-trip a real factory image AND describe Particle-specific
|
|
9
|
+
* OTA semantics (slots, groups, per-partition hashes, signing).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DEFAULT_SECTOR_SIZE = void 0;
|
|
13
|
+
exports.partitionsByLun = partitionsByLun;
|
|
14
|
+
exports.classify = classify;
|
|
15
|
+
exports.DEFAULT_SECTOR_SIZE = 4096;
|
|
16
|
+
function partitionsByLun(model) {
|
|
17
|
+
const m = new Map();
|
|
18
|
+
for (const p of model.partitions) {
|
|
19
|
+
const arr = m.get(p.lun) ?? [];
|
|
20
|
+
arr.push(p);
|
|
21
|
+
m.set(p.lun, arr);
|
|
22
|
+
}
|
|
23
|
+
return m;
|
|
24
|
+
}
|
|
25
|
+
/** Derive slot/role/group from a partition label and its LUN. */
|
|
26
|
+
function classify(label, lun) {
|
|
27
|
+
const slot = label.endsWith('_a') ? 'a' : label.endsWith('_b') ? 'b' : 'none';
|
|
28
|
+
const base = label.replace(/_[ab]$/, '');
|
|
29
|
+
if (label === 'PrimaryGPT' || label === 'BackupGPT') {
|
|
30
|
+
return { slot: 'none', role: 'gpt', group: 'OTHER' };
|
|
31
|
+
}
|
|
32
|
+
if (base === 'efi')
|
|
33
|
+
return { slot, role: 'efi', group: slot === 'b' ? 'B' : 'A' };
|
|
34
|
+
if (base === 'system')
|
|
35
|
+
return { slot, role: 'system', group: slot === 'b' ? 'B' : 'A' };
|
|
36
|
+
if (base === 'userdata')
|
|
37
|
+
return { slot: 'none', role: 'userdata', group: 'USER' };
|
|
38
|
+
if (base === 'persist')
|
|
39
|
+
return { slot: 'none', role: 'persist', group: 'USER' };
|
|
40
|
+
if (base === 'xbl' || base === 'xbl_config')
|
|
41
|
+
return { slot, role: 'boot', group: 'BOOT' };
|
|
42
|
+
if (['fsc', 'fsg', 'modemst1', 'modemst2', 'nvdata1', 'nvdata2'].includes(base)) {
|
|
43
|
+
return { slot: 'none', role: 'nvm', group: 'NVM' };
|
|
44
|
+
}
|
|
45
|
+
if (lun === 4)
|
|
46
|
+
return { slot, role: 'firmware', group: 'FIRMWARE' };
|
|
47
|
+
return { slot, role: 'other', group: 'OTHER' };
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side consumer helpers. A cloud/release server that hosts Particle images
|
|
3
|
+
* uses these to (1) ingest+validate an uploaded image and (2) serve a per-slot
|
|
4
|
+
* OTA view to a device. It is a thin composition of the lib's parse/validate/
|
|
5
|
+
* expand primitives — the server owns transport/storage, not image logic.
|
|
6
|
+
*
|
|
7
|
+
* See OTA_SERVER.md for the integration contract.
|
|
8
|
+
*/
|
|
9
|
+
import { KeyObject } from 'node:crypto';
|
|
10
|
+
import { type ValidateReport } from '../validate';
|
|
11
|
+
import type { ParticleImageV1 } from '../manifest/particleImageV1';
|
|
12
|
+
import type { Slot } from '../model/types';
|
|
13
|
+
export interface IngestResult {
|
|
14
|
+
manifest: ParticleImageV1;
|
|
15
|
+
report: ValidateReport;
|
|
16
|
+
signatureOk: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Ingest an uploaded image zip: read the manifest, verify signature, validate. */
|
|
19
|
+
export declare function ingestImage(zipPath: string, opts: {
|
|
20
|
+
publicKey: string | Buffer | KeyObject;
|
|
21
|
+
}): Promise<IngestResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Produce the OTA-image view a server would hand a device asking to update a
|
|
24
|
+
* given slot — the manifest subset + the exact payload entry names to stream.
|
|
25
|
+
*/
|
|
26
|
+
export declare function otaViewForSlot(manifest: ParticleImageV1, slot: Slot): {
|
|
27
|
+
manifest: ParticleImageV1;
|
|
28
|
+
entries: string[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ingestImage = ingestImage;
|
|
4
|
+
exports.otaViewForSlot = otaViewForSlot;
|
|
5
|
+
const zip_1 = require("../zip");
|
|
6
|
+
const validate_1 = require("../validate");
|
|
7
|
+
const expand_1 = require("../format/expand");
|
|
8
|
+
const sign_1 = require("../signing/sign");
|
|
9
|
+
/** Ingest an uploaded image zip: read the manifest, verify signature, validate. */
|
|
10
|
+
async function ingestImage(zipPath, opts) {
|
|
11
|
+
const manifest = await (0, zip_1.readManifestFromZip)(zipPath);
|
|
12
|
+
const zipEntryNames = (await (0, zip_1.listImageZip)(zipPath)).map((e) => e.name);
|
|
13
|
+
const report = (0, validate_1.validateManifest)(manifest, { publicKey: opts.publicKey, zipEntryNames });
|
|
14
|
+
const signatureOk = manifest.signing.profile === 'none' ? true : (0, sign_1.verifyManifest)(manifest, opts.publicKey);
|
|
15
|
+
return { manifest, report, signatureOk };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Produce the OTA-image view a server would hand a device asking to update a
|
|
19
|
+
* given slot — the manifest subset + the exact payload entry names to stream.
|
|
20
|
+
*/
|
|
21
|
+
function otaViewForSlot(manifest, slot) {
|
|
22
|
+
const view = (0, expand_1.expand)(manifest, { kind: 'ota-image', slot });
|
|
23
|
+
const entries = [];
|
|
24
|
+
for (const p of view.partitions) {
|
|
25
|
+
for (const op of p.ops) {
|
|
26
|
+
if (op.op === 'program') {
|
|
27
|
+
if (op.source)
|
|
28
|
+
entries.push(op.source);
|
|
29
|
+
for (const c of op.chunks ?? [])
|
|
30
|
+
entries.push(c.source);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { manifest: view, entries };
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON canonicalization for signing. A pragmatic RFC 8785 (JCS)
|
|
3
|
+
* subset sufficient for our manifest: object keys sorted lexicographically by
|
|
4
|
+
* UTF-16 code unit, no insignificant whitespace, arrays in order. Our manifest
|
|
5
|
+
* values are strings / integers / booleans / nested objects / arrays — no
|
|
6
|
+
* floats or exotic numbers — so JSON.stringify of a key-sorted structure is a
|
|
7
|
+
* stable canonical form across the TS consumers (composer CLI, cli, device).
|
|
8
|
+
*/
|
|
9
|
+
export declare function canonicalize(value: unknown): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonicalize = canonicalize;
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic JSON canonicalization for signing. A pragmatic RFC 8785 (JCS)
|
|
6
|
+
* subset sufficient for our manifest: object keys sorted lexicographically by
|
|
7
|
+
* UTF-16 code unit, no insignificant whitespace, arrays in order. Our manifest
|
|
8
|
+
* values are strings / integers / booleans / nested objects / arrays — no
|
|
9
|
+
* floats or exotic numbers — so JSON.stringify of a key-sorted structure is a
|
|
10
|
+
* stable canonical form across the TS consumers (composer CLI, cli, device).
|
|
11
|
+
*/
|
|
12
|
+
function canonicalize(value) {
|
|
13
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'string')
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return '[' + value.map((v) => canonicalize(v)).join(',') + ']';
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'object') {
|
|
22
|
+
const obj = value;
|
|
23
|
+
const keys = Object.keys(obj)
|
|
24
|
+
.filter((k) => obj[k] !== undefined)
|
|
25
|
+
.sort();
|
|
26
|
+
return ('{' +
|
|
27
|
+
keys.map((k) => JSON.stringify(k) + ':' + canonicalize(obj[k])).join(',') +
|
|
28
|
+
'}');
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`canonicalize: unsupported value type ${typeof value}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ed25519 manifest signing + verification.
|
|
3
|
+
*
|
|
4
|
+
* The signature covers the JCS-canonical bytes of the manifest with
|
|
5
|
+
* `signing.signature` blanked to "". Node's native crypto handles ed25519
|
|
6
|
+
* (algorithm = null), so no third-party crypto dependency.
|
|
7
|
+
*/
|
|
8
|
+
import { KeyObject } from 'node:crypto';
|
|
9
|
+
import type { ParticleImageV1 } from '../manifest/particleImageV1';
|
|
10
|
+
/** The exact bytes that get signed: JCS(manifest with signing.signature=""). */
|
|
11
|
+
export declare function signingBytes(manifest: ParticleImageV1): Buffer;
|
|
12
|
+
/** Sign in place: sets manifest.signing.signature (base64) and returns the manifest. */
|
|
13
|
+
export declare function signManifest(manifest: ParticleImageV1, privateKey: string | Buffer | KeyObject): ParticleImageV1;
|
|
14
|
+
/** Verify the embedded signature against a public key. Returns true/false. */
|
|
15
|
+
export declare function verifyManifest(manifest: ParticleImageV1, publicKey: string | Buffer | KeyObject): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.signingBytes = signingBytes;
|
|
4
|
+
exports.signManifest = signManifest;
|
|
5
|
+
exports.verifyManifest = verifyManifest;
|
|
6
|
+
/**
|
|
7
|
+
* ed25519 manifest signing + verification.
|
|
8
|
+
*
|
|
9
|
+
* The signature covers the JCS-canonical bytes of the manifest with
|
|
10
|
+
* `signing.signature` blanked to "". Node's native crypto handles ed25519
|
|
11
|
+
* (algorithm = null), so no third-party crypto dependency.
|
|
12
|
+
*/
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const jcs_1 = require("./jcs");
|
|
15
|
+
/** The exact bytes that get signed: JCS(manifest with signing.signature=""). */
|
|
16
|
+
function signingBytes(manifest) {
|
|
17
|
+
const clone = JSON.parse(JSON.stringify(manifest));
|
|
18
|
+
clone.signing = { ...clone.signing, signature: '' };
|
|
19
|
+
return Buffer.from((0, jcs_1.canonicalize)(clone), 'utf8');
|
|
20
|
+
}
|
|
21
|
+
function toPrivateKey(key) {
|
|
22
|
+
return key instanceof node_crypto_1.KeyObject ? key : (0, node_crypto_1.createPrivateKey)(key);
|
|
23
|
+
}
|
|
24
|
+
function toPublicKey(key) {
|
|
25
|
+
return key instanceof node_crypto_1.KeyObject ? key : (0, node_crypto_1.createPublicKey)(key);
|
|
26
|
+
}
|
|
27
|
+
/** Sign in place: sets manifest.signing.signature (base64) and returns the manifest. */
|
|
28
|
+
function signManifest(manifest, privateKey) {
|
|
29
|
+
const sig = (0, node_crypto_1.sign)(null, signingBytes(manifest), toPrivateKey(privateKey));
|
|
30
|
+
manifest.signing.signature = sig.toString('base64');
|
|
31
|
+
return manifest;
|
|
32
|
+
}
|
|
33
|
+
/** Verify the embedded signature against a public key. Returns true/false. */
|
|
34
|
+
function verifyManifest(manifest, publicKey) {
|
|
35
|
+
const sigB64 = manifest.signing?.signature;
|
|
36
|
+
if (!sigB64)
|
|
37
|
+
return false;
|
|
38
|
+
try {
|
|
39
|
+
return (0, node_crypto_1.verify)(null, signingBytes(manifest), toPublicKey(publicKey), Buffer.from(sigB64, 'base64'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qualcomm UEFI A/B slot selection via GPT partition-entry attribute bits.
|
|
3
|
+
* Canonical home (shared by particle-cli and the on-device OTA service).
|
|
4
|
+
*
|
|
5
|
+
* bits 48-49 PRIORITY (higher preferred; 0 = not selected by priority)
|
|
6
|
+
* bit 50 ACTIVE (slot the loader currently selects)
|
|
7
|
+
* bits 51-53 RETRY/tries (decremented per boot; 0 + !success => give up)
|
|
8
|
+
* bit 54 SUCCESS (booted OK; commit)
|
|
9
|
+
* bit 55 UNBOOTABLE (never select)
|
|
10
|
+
*
|
|
11
|
+
* NOTE: confirm exact positions vs the BP QcomPkg/.../PartitionTableUpdate.h
|
|
12
|
+
* before trusting on hardware — wrong bits brick slot selection. These match
|
|
13
|
+
* the standard Qualcomm boot_control layout. Pure BigInt helpers: unit-testable
|
|
14
|
+
* with no device.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PRIORITY_SHIFT = 48n;
|
|
17
|
+
export declare const PRIORITY_MASK = 3n;
|
|
18
|
+
export declare const ACTIVE_BIT = 50n;
|
|
19
|
+
export declare const RETRY_SHIFT = 51n;
|
|
20
|
+
export declare const RETRY_MASK = 7n;
|
|
21
|
+
export declare const SUCCESS_BIT = 54n;
|
|
22
|
+
export declare const UNBOOTABLE_BIT = 55n;
|
|
23
|
+
export declare const MAX_PRIORITY = 3;
|
|
24
|
+
export declare const MAX_RETRY = 7;
|
|
25
|
+
export type Slot = 'a' | 'b';
|
|
26
|
+
export interface SlotState {
|
|
27
|
+
priority: number;
|
|
28
|
+
active: boolean;
|
|
29
|
+
retries: number;
|
|
30
|
+
success: boolean;
|
|
31
|
+
unbootable: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function getSlotState(attr: bigint | number): SlotState;
|
|
34
|
+
export declare function makeActivePending(attr: bigint | number, opts?: {
|
|
35
|
+
priority?: number;
|
|
36
|
+
retries?: number;
|
|
37
|
+
}): bigint;
|
|
38
|
+
export declare function makeInactiveFallback(attr: bigint | number, opts?: {
|
|
39
|
+
priority?: number;
|
|
40
|
+
}): bigint;
|
|
41
|
+
export declare function markSuccessful(attr: bigint | number, opts?: {
|
|
42
|
+
retries?: number;
|
|
43
|
+
}): bigint;
|
|
44
|
+
export declare function markUnbootable(attr: bigint | number): bigint;
|
|
45
|
+
export declare function activeSlot(attrA: bigint | number, attrB: bigint | number): Slot;
|
|
46
|
+
export interface SlotPart {
|
|
47
|
+
label: string;
|
|
48
|
+
lun: number;
|
|
49
|
+
slot: Slot;
|
|
50
|
+
attr: bigint | number;
|
|
51
|
+
}
|
|
52
|
+
export interface SlotChange {
|
|
53
|
+
label: string;
|
|
54
|
+
lun: number;
|
|
55
|
+
slot: Slot;
|
|
56
|
+
fromAttr: bigint;
|
|
57
|
+
toAttr: bigint;
|
|
58
|
+
}
|
|
59
|
+
export interface SlotPlan {
|
|
60
|
+
current: Slot;
|
|
61
|
+
target: Slot;
|
|
62
|
+
changes: SlotChange[];
|
|
63
|
+
}
|
|
64
|
+
export declare function planSlotSwitch(slotParts: SlotPart[], target: Slot): SlotPlan;
|