@metamask/device-mcp 0.3.2 → 0.4.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/CHANGELOG.md +18 -1
- package/README.md +110 -21
- package/dist/android/NOTICE.md +65 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk +0 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk.idsig +0 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.manifest.json +10 -0
- package/dist/backends/adb-backend.cjs +326 -17
- package/dist/backends/adb-backend.cjs.map +1 -1
- package/dist/backends/adb-backend.d.cts +1 -0
- package/dist/backends/adb-backend.d.cts.map +1 -1
- package/dist/backends/adb-backend.d.mts +1 -0
- package/dist/backends/adb-backend.d.mts.map +1 -1
- package/dist/backends/adb-backend.mjs +327 -18
- package/dist/backends/adb-backend.mjs.map +1 -1
- package/dist/backends/android-instrumentation/adb.cjs +28 -0
- package/dist/backends/android-instrumentation/adb.cjs.map +1 -0
- package/dist/backends/android-instrumentation/adb.d.cts +24 -0
- package/dist/backends/android-instrumentation/adb.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/adb.d.mts +24 -0
- package/dist/backends/android-instrumentation/adb.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/adb.mjs +25 -0
- package/dist/backends/android-instrumentation/adb.mjs.map +1 -0
- package/dist/backends/android-instrumentation/artifact.cjs +139 -0
- package/dist/backends/android-instrumentation/artifact.cjs.map +1 -0
- package/dist/backends/android-instrumentation/artifact.d.cts +28 -0
- package/dist/backends/android-instrumentation/artifact.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/artifact.d.mts +28 -0
- package/dist/backends/android-instrumentation/artifact.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/artifact.mjs +154 -0
- package/dist/backends/android-instrumentation/artifact.mjs.map +1 -0
- package/dist/backends/android-instrumentation/errors.cjs +79 -0
- package/dist/backends/android-instrumentation/errors.cjs.map +1 -0
- package/dist/backends/android-instrumentation/errors.d.cts +68 -0
- package/dist/backends/android-instrumentation/errors.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/errors.d.mts +68 -0
- package/dist/backends/android-instrumentation/errors.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/errors.mjs +73 -0
- package/dist/backends/android-instrumentation/errors.mjs.map +1 -0
- package/dist/backends/android-instrumentation/installer.cjs +235 -0
- package/dist/backends/android-instrumentation/installer.cjs.map +1 -0
- package/dist/backends/android-instrumentation/installer.d.cts +123 -0
- package/dist/backends/android-instrumentation/installer.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/installer.d.mts +123 -0
- package/dist/backends/android-instrumentation/installer.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/installer.mjs +225 -0
- package/dist/backends/android-instrumentation/installer.mjs.map +1 -0
- package/dist/backends/android-instrumentation/signer.cjs +435 -0
- package/dist/backends/android-instrumentation/signer.cjs.map +1 -0
- package/dist/backends/android-instrumentation/signer.d.cts +40 -0
- package/dist/backends/android-instrumentation/signer.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/signer.d.mts +40 -0
- package/dist/backends/android-instrumentation/signer.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/signer.mjs +431 -0
- package/dist/backends/android-instrumentation/signer.mjs.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.cjs +159 -0
- package/dist/backends/android-instrumentation/snapshot.cjs.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.d.cts +67 -0
- package/dist/backends/android-instrumentation/snapshot.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.d.mts +67 -0
- package/dist/backends/android-instrumentation/snapshot.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.mjs +154 -0
- package/dist/backends/android-instrumentation/snapshot.mjs.map +1 -0
- package/dist/backends/appium-backend.cjs +19 -1
- package/dist/backends/appium-backend.cjs.map +1 -1
- package/dist/backends/appium-backend.d.cts +1 -0
- package/dist/backends/appium-backend.d.cts.map +1 -1
- package/dist/backends/appium-backend.d.mts +1 -0
- package/dist/backends/appium-backend.d.mts.map +1 -1
- package/dist/backends/appium-backend.mjs +19 -1
- package/dist/backends/appium-backend.mjs.map +1 -1
- package/dist/backends/idb-backend.cjs +128 -31
- package/dist/backends/idb-backend.cjs.map +1 -1
- package/dist/backends/idb-backend.d.cts +1 -0
- package/dist/backends/idb-backend.d.cts.map +1 -1
- package/dist/backends/idb-backend.d.mts +1 -0
- package/dist/backends/idb-backend.d.mts.map +1 -1
- package/dist/backends/idb-backend.mjs +129 -32
- package/dist/backends/idb-backend.mjs.map +1 -1
- package/dist/backends/index.cjs +3 -0
- package/dist/backends/index.cjs.map +1 -1
- package/dist/backends/index.d.cts.map +1 -1
- package/dist/backends/index.d.mts.map +1 -1
- package/dist/backends/index.mjs +3 -0
- package/dist/backends/index.mjs.map +1 -1
- package/dist/backends/types.cjs.map +1 -1
- package/dist/backends/types.d.cts +6 -0
- package/dist/backends/types.d.cts.map +1 -1
- package/dist/backends/types.d.mts +6 -0
- package/dist/backends/types.d.mts.map +1 -1
- package/dist/backends/types.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/tools/swipe.cjs +3 -1
- package/dist/tools/swipe.cjs.map +1 -1
- package/dist/tools/swipe.d.cts.map +1 -1
- package/dist/tools/swipe.d.mts.map +1 -1
- package/dist/tools/swipe.mjs +3 -1
- package/dist/tools/swipe.mjs.map +1 -1
- package/dist/utils/output-path.cjs +13 -0
- package/dist/utils/output-path.cjs.map +1 -1
- package/dist/utils/output-path.d.cts +10 -0
- package/dist/utils/output-path.d.cts.map +1 -1
- package/dist/utils/output-path.d.mts +10 -0
- package/dist/utils/output-path.d.mts.map +1 -1
- package/dist/utils/output-path.mjs +12 -0
- package/dist/utils/output-path.mjs.map +1 -1
- package/dist/utils/platform.cjs +65 -7
- package/dist/utils/platform.cjs.map +1 -1
- package/dist/utils/platform.d.cts +33 -0
- package/dist/utils/platform.d.cts.map +1 -1
- package/dist/utils/platform.d.mts +33 -0
- package/dist/utils/platform.d.mts.map +1 -1
- package/dist/utils/platform.mjs +63 -7
- package/dist/utils/platform.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Implemented from the public AOSP APK Signature Scheme v2/v3 spec.
|
|
3
|
+
/**
|
|
4
|
+
* Pure-JS verifier for APK Signature Scheme v2 and v3.
|
|
5
|
+
*
|
|
6
|
+
* This proves an APK was cryptographically SIGNED by a given key — it verifies
|
|
7
|
+
* the signature over the recomputed APK content digest, then binds the leaf
|
|
8
|
+
* certificate to that key. It is NOT a cert-presence check: reading a
|
|
9
|
+
* certificate out of an APK proves nothing (certificates are public), so this
|
|
10
|
+
* module recomputes the content digest and verifies the signature before
|
|
11
|
+
* trusting the signer. Used to gate `am instrument` against a helper that could
|
|
12
|
+
* otherwise be a package-name squatter.
|
|
13
|
+
*
|
|
14
|
+
* No external dependencies, no Android SDK, no shelling out — `node:crypto` and
|
|
15
|
+
* `node:fs` only, so it runs on any consumer machine that runs the MCP server.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.verifyApkSignerSha256 = verifyApkSignerSha256;
|
|
19
|
+
exports.assertApkSignerSha256 = assertApkSignerSha256;
|
|
20
|
+
const node_crypto_1 = require("node:crypto");
|
|
21
|
+
const promises_1 = require("node:fs/promises");
|
|
22
|
+
const errors_js_1 = require("./errors.cjs");
|
|
23
|
+
const MAX_APK_BYTES = 200 * 1024 * 1024;
|
|
24
|
+
const EOCD_SIGNATURE = 101010256;
|
|
25
|
+
const EOCD_MIN_SIZE = 22;
|
|
26
|
+
const EOCD_CD_OFFSET_FIELD = 16;
|
|
27
|
+
const EOCD_COMMENT_LENGTH_FIELD = 20;
|
|
28
|
+
const SIG_BLOCK_MAGIC = Buffer.from('APK Sig Block 42', 'latin1');
|
|
29
|
+
const SIG_BLOCK_FOOTER_SIZE = 24;
|
|
30
|
+
const V2_BLOCK_ID = 1896449818;
|
|
31
|
+
const V3_BLOCK_ID = 4031998144;
|
|
32
|
+
const CHUNK_SIZE = 1024 * 1024;
|
|
33
|
+
const CHUNK_PREFIX = 0xa5;
|
|
34
|
+
const TOP_LEVEL_PREFIX = 0x5a;
|
|
35
|
+
const RSA_PKCS1_PSS_PADDING = 6;
|
|
36
|
+
/**
|
|
37
|
+
* Map an APK signature-algorithm ID to its hash and verification strategy.
|
|
38
|
+
* IDs and their crypto meanings are fixed by the AOSP spec.
|
|
39
|
+
*
|
|
40
|
+
* @param id - The 32-bit signature algorithm identifier.
|
|
41
|
+
* @returns The algorithm descriptor, or null when unsupported (e.g. fs-verity).
|
|
42
|
+
*/
|
|
43
|
+
function sigAlgo(id) {
|
|
44
|
+
switch (id) {
|
|
45
|
+
case 0x0101:
|
|
46
|
+
return {
|
|
47
|
+
hash: 'sha256',
|
|
48
|
+
strength: 1,
|
|
49
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha256', data, { key, padding: RSA_PKCS1_PSS_PADDING, saltLength: 32 }, signature),
|
|
50
|
+
};
|
|
51
|
+
case 0x0102:
|
|
52
|
+
return {
|
|
53
|
+
hash: 'sha512',
|
|
54
|
+
strength: 3,
|
|
55
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha512', data, { key, padding: RSA_PKCS1_PSS_PADDING, saltLength: 64 }, signature),
|
|
56
|
+
};
|
|
57
|
+
case 0x0103:
|
|
58
|
+
return {
|
|
59
|
+
hash: 'sha256',
|
|
60
|
+
strength: 0,
|
|
61
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha256', data, key, signature),
|
|
62
|
+
};
|
|
63
|
+
case 0x0104:
|
|
64
|
+
return {
|
|
65
|
+
hash: 'sha512',
|
|
66
|
+
strength: 2,
|
|
67
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha512', data, key, signature),
|
|
68
|
+
};
|
|
69
|
+
case 0x0201:
|
|
70
|
+
return {
|
|
71
|
+
hash: 'sha256',
|
|
72
|
+
strength: 1,
|
|
73
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha256', data, key, signature),
|
|
74
|
+
};
|
|
75
|
+
case 0x0202:
|
|
76
|
+
return {
|
|
77
|
+
hash: 'sha512',
|
|
78
|
+
strength: 3,
|
|
79
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha512', data, key, signature),
|
|
80
|
+
};
|
|
81
|
+
case 0x0301:
|
|
82
|
+
return {
|
|
83
|
+
hash: 'sha256',
|
|
84
|
+
strength: 1,
|
|
85
|
+
verify: (data, key, signature) => (0, node_crypto_1.verify)('sha256', data, key, signature),
|
|
86
|
+
};
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Raise an {@link SnapshotHelperError} for any malformed or unverifiable APK.
|
|
93
|
+
*
|
|
94
|
+
* @param message - What went wrong.
|
|
95
|
+
* @throws Always.
|
|
96
|
+
*/
|
|
97
|
+
function invalid(message) {
|
|
98
|
+
throw new errors_js_1.SnapshotHelperError('ARTIFACT_INVALID', message);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Find the ZIP End Of Central Directory record by scanning backwards.
|
|
102
|
+
*
|
|
103
|
+
* @param apk - The whole APK file.
|
|
104
|
+
* @returns The EOCD start offset plus the central-directory offset and size it
|
|
105
|
+
* declares.
|
|
106
|
+
*/
|
|
107
|
+
function findEocd(apk) {
|
|
108
|
+
const minStart = Math.max(0, apk.length - EOCD_MIN_SIZE - 0xffff);
|
|
109
|
+
for (let i = apk.length - EOCD_MIN_SIZE; i >= minStart; i -= 1) {
|
|
110
|
+
if (apk.readUInt32LE(i) !== EOCD_SIGNATURE) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const commentLength = apk.readUInt16LE(i + EOCD_COMMENT_LENGTH_FIELD);
|
|
114
|
+
if (i + EOCD_MIN_SIZE + commentLength !== apk.length) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
eocdOffset: i,
|
|
119
|
+
centralDirOffset: apk.readUInt32LE(i + EOCD_CD_OFFSET_FIELD),
|
|
120
|
+
centralDirSize: apk.readUInt32LE(i + 12),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return invalid('APK is missing a ZIP End Of Central Directory record');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Locate the APK Signing Block that sits immediately before the central
|
|
127
|
+
* directory.
|
|
128
|
+
*
|
|
129
|
+
* @param apk - The whole APK file.
|
|
130
|
+
* @param centralDirOffset - The central-directory offset from the EOCD.
|
|
131
|
+
* @param eocdOffset - The EOCD start offset.
|
|
132
|
+
* @param centralDirSize - The central-directory size from the EOCD.
|
|
133
|
+
* @returns The signing-block offset and its payload (between the two size
|
|
134
|
+
* fields, excluding the trailing size and magic).
|
|
135
|
+
*/
|
|
136
|
+
function findSigningBlock(apk, centralDirOffset, eocdOffset, centralDirSize) {
|
|
137
|
+
if (centralDirOffset + centralDirSize !== eocdOffset) {
|
|
138
|
+
invalid('Central directory is not immediately followed by the EOCD');
|
|
139
|
+
}
|
|
140
|
+
if (centralDirOffset < SIG_BLOCK_FOOTER_SIZE) {
|
|
141
|
+
invalid('No APK Signing Block before the central directory');
|
|
142
|
+
}
|
|
143
|
+
const footer = apk.subarray(centralDirOffset - SIG_BLOCK_FOOTER_SIZE, centralDirOffset);
|
|
144
|
+
if (!footer.subarray(8, 24).equals(SIG_BLOCK_MAGIC)) {
|
|
145
|
+
invalid('APK Signing Block magic not found (APK is not v2/v3 signed)');
|
|
146
|
+
}
|
|
147
|
+
const trailerSize = footer.readBigUInt64LE(0);
|
|
148
|
+
const totalSize = Number(trailerSize) + 8;
|
|
149
|
+
const apkSigBlockOffset = centralDirOffset - totalSize;
|
|
150
|
+
if (apkSigBlockOffset < 0) {
|
|
151
|
+
invalid('APK Signing Block size is out of range');
|
|
152
|
+
}
|
|
153
|
+
const headerSize = apk.readBigUInt64LE(apkSigBlockOffset);
|
|
154
|
+
if (headerSize !== trailerSize) {
|
|
155
|
+
invalid('APK Signing Block header/footer size mismatch');
|
|
156
|
+
}
|
|
157
|
+
const pairs = apk.subarray(apkSigBlockOffset + 8, centralDirOffset - SIG_BLOCK_FOOTER_SIZE);
|
|
158
|
+
return { apkSigBlockOffset, pairs };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Extract a scheme block (v2 or v3) from the signing block's ID/value pairs.
|
|
162
|
+
*
|
|
163
|
+
* @param pairs - The concatenated ID/value pairs.
|
|
164
|
+
* @returns The v3 block if present, else the v2 block, with its scheme label.
|
|
165
|
+
*/
|
|
166
|
+
function selectSchemeBlock(pairs) {
|
|
167
|
+
const blocks = new Map();
|
|
168
|
+
let cursor = 0;
|
|
169
|
+
while (cursor + 8 <= pairs.length) {
|
|
170
|
+
const pairSize = Number(pairs.readBigUInt64LE(cursor));
|
|
171
|
+
cursor += 8;
|
|
172
|
+
if (pairSize < 4 || cursor + pairSize > pairs.length) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const id = pairs.readUInt32LE(cursor);
|
|
176
|
+
const value = pairs.subarray(cursor + 4, cursor + pairSize);
|
|
177
|
+
blocks.set(id, value);
|
|
178
|
+
cursor += pairSize;
|
|
179
|
+
}
|
|
180
|
+
const v3 = blocks.get(V3_BLOCK_ID);
|
|
181
|
+
if (v3) {
|
|
182
|
+
return { scheme: 'v3', block: v3 };
|
|
183
|
+
}
|
|
184
|
+
const v2 = blocks.get(V2_BLOCK_ID);
|
|
185
|
+
if (v2) {
|
|
186
|
+
return { scheme: 'v2', block: v2 };
|
|
187
|
+
}
|
|
188
|
+
return invalid('No v2 or v3 signature block in the APK Signing Block');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Read a `uint32`-length-prefixed slice, advancing the cursor past it.
|
|
192
|
+
*
|
|
193
|
+
* @param bytes - The buffer to read from.
|
|
194
|
+
* @param cursor - The current read offset.
|
|
195
|
+
* @returns The inner bytes and the offset just past them.
|
|
196
|
+
*/
|
|
197
|
+
function readLenPrefixed(bytes, cursor) {
|
|
198
|
+
if (cursor + 4 > bytes.length) {
|
|
199
|
+
invalid('Truncated length-prefixed field');
|
|
200
|
+
}
|
|
201
|
+
const len = bytes.readUInt32LE(cursor);
|
|
202
|
+
const start = cursor + 4;
|
|
203
|
+
const end = start + len;
|
|
204
|
+
if (end > bytes.length) {
|
|
205
|
+
invalid('Length-prefixed field runs past end of block');
|
|
206
|
+
}
|
|
207
|
+
return { value: bytes.subarray(start, end), next: end };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Split a length-prefixed sequence into its length-prefixed elements.
|
|
211
|
+
*
|
|
212
|
+
* @param seq - The sequence bytes (already unwrapped from its own prefix).
|
|
213
|
+
* @returns The element buffers in order.
|
|
214
|
+
*/
|
|
215
|
+
function readSequence(seq) {
|
|
216
|
+
const items = [];
|
|
217
|
+
let cursor = 0;
|
|
218
|
+
while (cursor < seq.length) {
|
|
219
|
+
const { value, next } = readLenPrefixed(seq, cursor);
|
|
220
|
+
items.push(value);
|
|
221
|
+
cursor = next;
|
|
222
|
+
}
|
|
223
|
+
return items;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Parse the `{ uint32 algoId; len-prefixed payload }` records used by both the
|
|
227
|
+
* signed-data digests list and the signatures list.
|
|
228
|
+
*
|
|
229
|
+
* @param seq - The sequence bytes.
|
|
230
|
+
* @returns The parsed algorithm-id/payload records in order.
|
|
231
|
+
*/
|
|
232
|
+
function readAlgoRecords(seq) {
|
|
233
|
+
return readSequence(seq).map((element) => {
|
|
234
|
+
if (element.length < 4) {
|
|
235
|
+
invalid('Algorithm record too short');
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
algoId: element.readUInt32LE(0),
|
|
239
|
+
payload: readLenPrefixed(element, 4).value,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse a single signer out of a v2/v3 scheme block.
|
|
245
|
+
*
|
|
246
|
+
* @param block - The scheme block value (a length-prefixed signers sequence).
|
|
247
|
+
* @param scheme - Which scheme is being parsed (v3 carries extra SDK fields).
|
|
248
|
+
* @returns The parsed signer.
|
|
249
|
+
*/
|
|
250
|
+
function parseSigner(block, scheme) {
|
|
251
|
+
const signers = readSequence(readLenPrefixed(block, 0).value);
|
|
252
|
+
if (signers.length !== 1) {
|
|
253
|
+
invalid(`Expected exactly one signer, found ${signers.length}`);
|
|
254
|
+
}
|
|
255
|
+
const signer = signers[0];
|
|
256
|
+
let cursor = 0;
|
|
257
|
+
const signedDataRead = readLenPrefixed(signer, cursor);
|
|
258
|
+
const signedData = signedDataRead.value;
|
|
259
|
+
cursor = signedDataRead.next;
|
|
260
|
+
let signerMinSdk;
|
|
261
|
+
let signerMaxSdk;
|
|
262
|
+
if (scheme === 'v3') {
|
|
263
|
+
if (cursor + 8 > signer.length) {
|
|
264
|
+
invalid('v3 signer missing min/max SDK');
|
|
265
|
+
}
|
|
266
|
+
signerMinSdk = signer.readUInt32LE(cursor);
|
|
267
|
+
signerMaxSdk = signer.readUInt32LE(cursor + 4);
|
|
268
|
+
cursor += 8;
|
|
269
|
+
}
|
|
270
|
+
const signaturesRead = readLenPrefixed(signer, cursor);
|
|
271
|
+
cursor = signaturesRead.next;
|
|
272
|
+
const publicKeyRead = readLenPrefixed(signer, cursor);
|
|
273
|
+
let sdCursor = 0;
|
|
274
|
+
const digestsRead = readLenPrefixed(signedData, sdCursor);
|
|
275
|
+
sdCursor = digestsRead.next;
|
|
276
|
+
const certificatesRead = readLenPrefixed(signedData, sdCursor);
|
|
277
|
+
sdCursor = certificatesRead.next;
|
|
278
|
+
let signedMinSdk;
|
|
279
|
+
let signedMaxSdk;
|
|
280
|
+
if (scheme === 'v3') {
|
|
281
|
+
if (sdCursor + 8 > signedData.length) {
|
|
282
|
+
invalid('v3 signed-data missing min/max SDK');
|
|
283
|
+
}
|
|
284
|
+
signedMinSdk = signedData.readUInt32LE(sdCursor);
|
|
285
|
+
signedMaxSdk = signedData.readUInt32LE(sdCursor + 4);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
signedData,
|
|
289
|
+
digests: readAlgoRecords(digestsRead.value),
|
|
290
|
+
certificates: readSequence(certificatesRead.value),
|
|
291
|
+
signatures: readAlgoRecords(signaturesRead.value),
|
|
292
|
+
publicKey: publicKeyRead.value,
|
|
293
|
+
signedMinSdk,
|
|
294
|
+
signedMaxSdk,
|
|
295
|
+
signerMinSdk,
|
|
296
|
+
signerMaxSdk,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Compute the chunked content digest of one segment, appending each chunk's
|
|
301
|
+
* digest to `out`.
|
|
302
|
+
*
|
|
303
|
+
* @param segment - The segment bytes.
|
|
304
|
+
* @param hash - The digest algorithm.
|
|
305
|
+
* @param out - Accumulator for per-chunk digests.
|
|
306
|
+
* @returns The number of chunks produced.
|
|
307
|
+
*/
|
|
308
|
+
function digestSegment(segment, hash, out) {
|
|
309
|
+
let chunks = 0;
|
|
310
|
+
for (let offset = 0; offset < segment.length; offset += CHUNK_SIZE) {
|
|
311
|
+
const chunk = segment.subarray(offset, offset + CHUNK_SIZE);
|
|
312
|
+
const prefix = Buffer.allocUnsafe(5);
|
|
313
|
+
prefix.writeUInt8(CHUNK_PREFIX, 0);
|
|
314
|
+
prefix.writeUInt32LE(chunk.length, 1);
|
|
315
|
+
out.push((0, node_crypto_1.createHash)(hash).update(prefix).update(chunk).digest());
|
|
316
|
+
chunks += 1;
|
|
317
|
+
}
|
|
318
|
+
return chunks;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Recompute the APK content digest per the v2/v3 spec: chunk the pre-signing
|
|
322
|
+
* bytes, the central directory, and the EOCD (patched so its central-directory
|
|
323
|
+
* offset points at the signing block), then hash the concatenated chunk
|
|
324
|
+
* digests.
|
|
325
|
+
*
|
|
326
|
+
* @param apk - The whole APK file.
|
|
327
|
+
* @param apkSigBlockOffset - Offset of the APK Signing Block.
|
|
328
|
+
* @param centralDirOffset - Central-directory offset.
|
|
329
|
+
* @param eocdOffset - EOCD start offset.
|
|
330
|
+
* @param hash - The content-digest algorithm.
|
|
331
|
+
* @returns The recomputed content digest.
|
|
332
|
+
*/
|
|
333
|
+
function computeContentDigest(apk, apkSigBlockOffset, centralDirOffset, eocdOffset, hash) {
|
|
334
|
+
const beforeBlock = apk.subarray(0, apkSigBlockOffset);
|
|
335
|
+
const centralDir = apk.subarray(centralDirOffset, eocdOffset);
|
|
336
|
+
const eocd = Buffer.from(apk.subarray(eocdOffset));
|
|
337
|
+
eocd.writeUInt32LE(apkSigBlockOffset, EOCD_CD_OFFSET_FIELD);
|
|
338
|
+
const chunkDigests = [];
|
|
339
|
+
let count = 0;
|
|
340
|
+
count += digestSegment(beforeBlock, hash, chunkDigests);
|
|
341
|
+
count += digestSegment(centralDir, hash, chunkDigests);
|
|
342
|
+
count += digestSegment(eocd, hash, chunkDigests);
|
|
343
|
+
const top = (0, node_crypto_1.createHash)(hash);
|
|
344
|
+
const header = Buffer.allocUnsafe(5);
|
|
345
|
+
header.writeUInt8(TOP_LEVEL_PREFIX, 0);
|
|
346
|
+
header.writeUInt32LE(count, 1);
|
|
347
|
+
top.update(header);
|
|
348
|
+
for (const digest of chunkDigests) {
|
|
349
|
+
top.update(digest);
|
|
350
|
+
}
|
|
351
|
+
return top.digest();
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Verify an APK's v2/v3 signature and return its leaf signer certificate
|
|
355
|
+
* SHA-256.
|
|
356
|
+
*
|
|
357
|
+
* @param apkPath - Path to the APK on disk.
|
|
358
|
+
* @returns The verified scheme and lowercase-hex leaf certificate SHA-256.
|
|
359
|
+
* @throws {@link SnapshotHelperError} when the APK is malformed or the
|
|
360
|
+
* signature does not verify.
|
|
361
|
+
*/
|
|
362
|
+
async function verifyApkSignerSha256(apkPath) {
|
|
363
|
+
const apk = await (0, promises_1.readFile)(apkPath);
|
|
364
|
+
if (apk.length > MAX_APK_BYTES) {
|
|
365
|
+
invalid(`APK exceeds ${MAX_APK_BYTES} bytes`);
|
|
366
|
+
}
|
|
367
|
+
const { eocdOffset, centralDirOffset, centralDirSize } = findEocd(apk);
|
|
368
|
+
const { apkSigBlockOffset, pairs } = findSigningBlock(apk, centralDirOffset, eocdOffset, centralDirSize);
|
|
369
|
+
const { scheme, block } = selectSchemeBlock(pairs);
|
|
370
|
+
const signer = parseSigner(block, scheme);
|
|
371
|
+
if (signer.certificates.length < 1) {
|
|
372
|
+
invalid('Signer has no certificates');
|
|
373
|
+
}
|
|
374
|
+
const selected = signer.signatures
|
|
375
|
+
.map((signature) => ({ signature, algo: sigAlgo(signature.algoId) }))
|
|
376
|
+
.filter((entry) => entry.algo !== null)
|
|
377
|
+
.sort((a, b) => b.algo.strength - a.algo.strength)
|
|
378
|
+
.at(0);
|
|
379
|
+
if (!selected) {
|
|
380
|
+
invalid('No supported signature algorithm in signer');
|
|
381
|
+
}
|
|
382
|
+
const publicKey = (0, node_crypto_1.createPublicKey)({
|
|
383
|
+
key: signer.publicKey,
|
|
384
|
+
format: 'der',
|
|
385
|
+
type: 'spki',
|
|
386
|
+
});
|
|
387
|
+
if (!selected.algo.verify(signer.signedData, publicKey, selected.signature.payload)) {
|
|
388
|
+
invalid('Signature did not verify over signed-data');
|
|
389
|
+
}
|
|
390
|
+
const leaf = signer.certificates[0];
|
|
391
|
+
const certPublicKey = new node_crypto_1.X509Certificate(leaf).publicKey;
|
|
392
|
+
const certSpki = certPublicKey.export({ format: 'der', type: 'spki' });
|
|
393
|
+
if (!certSpki.equals(signer.publicKey)) {
|
|
394
|
+
invalid('Leaf certificate public key does not match the block public key');
|
|
395
|
+
}
|
|
396
|
+
const sigIds = signer.signatures.map((s) => s.algoId);
|
|
397
|
+
const digestIds = signer.digests.map((d) => d.algoId);
|
|
398
|
+
if (sigIds.length !== digestIds.length ||
|
|
399
|
+
sigIds.some((id, index) => id !== digestIds[index])) {
|
|
400
|
+
invalid('Signature and digest algorithm lists differ');
|
|
401
|
+
}
|
|
402
|
+
const expectedDigest = signer.digests.find((d) => d.algoId === selected.signature.algoId);
|
|
403
|
+
if (!expectedDigest) {
|
|
404
|
+
invalid('No content digest for the selected algorithm');
|
|
405
|
+
}
|
|
406
|
+
const computed = computeContentDigest(apk, apkSigBlockOffset, centralDirOffset, eocdOffset, selected.algo.hash);
|
|
407
|
+
if (!computed.equals(expectedDigest.payload)) {
|
|
408
|
+
invalid('Recomputed APK content digest does not match signed digest');
|
|
409
|
+
}
|
|
410
|
+
if (scheme === 'v3') {
|
|
411
|
+
if (signer.signerMinSdk !== signer.signedMinSdk ||
|
|
412
|
+
signer.signerMaxSdk !== signer.signedMaxSdk) {
|
|
413
|
+
invalid('v3 signer min/max SDK does not match signed-data');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const signerSha256 = (0, node_crypto_1.createHash)('sha256').update(leaf).digest('hex');
|
|
417
|
+
return { verified: true, signerSha256, scheme };
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Verify an APK and assert its signer matches an expected pinned SHA-256.
|
|
421
|
+
*
|
|
422
|
+
* @param apkPath - Path to the APK on disk.
|
|
423
|
+
* @param expectedSignerSha256 - The pinned lowercase-hex signer SHA-256.
|
|
424
|
+
* @throws {@link UntrustedHelperError} when the (validly signed) APK's signer
|
|
425
|
+
* does not match the pin.
|
|
426
|
+
* @throws {@link SnapshotHelperError} when the APK is malformed or unverifiable.
|
|
427
|
+
*/
|
|
428
|
+
async function assertApkSignerSha256(apkPath, expectedSignerSha256) {
|
|
429
|
+
const { signerSha256 } = await verifyApkSignerSha256(apkPath);
|
|
430
|
+
const expected = expectedSignerSha256.toLowerCase();
|
|
431
|
+
if (signerSha256 !== expected) {
|
|
432
|
+
throw new errors_js_1.UntrustedHelperError('Installed snapshot helper is signed by an unexpected certificate', { expectedSignerSha256: expected, actualSignerSha256: signerSha256 });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
//# sourceMappingURL=signer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.cjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/signer.ts"],"names":[],"mappings":"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","sourcesContent":["// Implemented from the public AOSP APK Signature Scheme v2/v3 spec.\n/**\n * Pure-JS verifier for APK Signature Scheme v2 and v3.\n *\n * This proves an APK was cryptographically SIGNED by a given key — it verifies\n * the signature over the recomputed APK content digest, then binds the leaf\n * certificate to that key. It is NOT a cert-presence check: reading a\n * certificate out of an APK proves nothing (certificates are public), so this\n * module recomputes the content digest and verifies the signature before\n * trusting the signer. Used to gate `am instrument` against a helper that could\n * otherwise be a package-name squatter.\n *\n * No external dependencies, no Android SDK, no shelling out — `node:crypto` and\n * `node:fs` only, so it runs on any consumer machine that runs the MCP server.\n */\n\nimport {\n createHash,\n createPublicKey,\n verify as cryptoVerify,\n X509Certificate,\n} from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\n\nimport { SnapshotHelperError, UntrustedHelperError } from './errors.js';\n\nexport type ApkSignerVerification = {\n verified: true;\n signerSha256: string;\n scheme: 'v2' | 'v3';\n};\n\nconst MAX_APK_BYTES = 200 * 1024 * 1024;\n\nconst EOCD_SIGNATURE = 0x0605_4b50;\nconst EOCD_MIN_SIZE = 22;\nconst EOCD_CD_OFFSET_FIELD = 16;\nconst EOCD_COMMENT_LENGTH_FIELD = 20;\n\nconst SIG_BLOCK_MAGIC = Buffer.from('APK Sig Block 42', 'latin1');\nconst SIG_BLOCK_FOOTER_SIZE = 24;\n\nconst V2_BLOCK_ID = 0x7109_871a;\nconst V3_BLOCK_ID = 0xf053_68c0;\n\nconst CHUNK_SIZE = 1024 * 1024;\nconst CHUNK_PREFIX = 0xa5;\nconst TOP_LEVEL_PREFIX = 0x5a;\n\ntype ContentDigestAlgo = 'sha256' | 'sha512';\n\ntype SigAlgo = {\n hash: ContentDigestAlgo;\n verify: (\n data: Buffer,\n key: ReturnType<typeof createPublicKey>,\n signature: Buffer,\n ) => boolean;\n strength: number;\n};\n\nconst RSA_PKCS1_PSS_PADDING = 6;\n\n/**\n * Map an APK signature-algorithm ID to its hash and verification strategy.\n * IDs and their crypto meanings are fixed by the AOSP spec.\n *\n * @param id - The 32-bit signature algorithm identifier.\n * @returns The algorithm descriptor, or null when unsupported (e.g. fs-verity).\n */\nfunction sigAlgo(id: number): SigAlgo | null {\n switch (id) {\n case 0x0101:\n return {\n hash: 'sha256',\n strength: 1,\n verify: (data, key, signature) =>\n cryptoVerify(\n 'sha256',\n data,\n { key, padding: RSA_PKCS1_PSS_PADDING, saltLength: 32 },\n signature,\n ),\n };\n case 0x0102:\n return {\n hash: 'sha512',\n strength: 3,\n verify: (data, key, signature) =>\n cryptoVerify(\n 'sha512',\n data,\n { key, padding: RSA_PKCS1_PSS_PADDING, saltLength: 64 },\n signature,\n ),\n };\n case 0x0103:\n return {\n hash: 'sha256',\n strength: 0,\n verify: (data, key, signature) =>\n cryptoVerify('sha256', data, key, signature),\n };\n case 0x0104:\n return {\n hash: 'sha512',\n strength: 2,\n verify: (data, key, signature) =>\n cryptoVerify('sha512', data, key, signature),\n };\n case 0x0201:\n return {\n hash: 'sha256',\n strength: 1,\n verify: (data, key, signature) =>\n cryptoVerify('sha256', data, key, signature),\n };\n case 0x0202:\n return {\n hash: 'sha512',\n strength: 3,\n verify: (data, key, signature) =>\n cryptoVerify('sha512', data, key, signature),\n };\n case 0x0301:\n return {\n hash: 'sha256',\n strength: 1,\n verify: (data, key, signature) =>\n cryptoVerify('sha256', data, key, signature),\n };\n default:\n return null;\n }\n}\n\n/**\n * Raise an {@link SnapshotHelperError} for any malformed or unverifiable APK.\n *\n * @param message - What went wrong.\n * @throws Always.\n */\nfunction invalid(message: string): never {\n throw new SnapshotHelperError('ARTIFACT_INVALID', message);\n}\n\n/**\n * Find the ZIP End Of Central Directory record by scanning backwards.\n *\n * @param apk - The whole APK file.\n * @returns The EOCD start offset plus the central-directory offset and size it\n * declares.\n */\nfunction findEocd(apk: Buffer): {\n eocdOffset: number;\n centralDirOffset: number;\n centralDirSize: number;\n} {\n const minStart = Math.max(0, apk.length - EOCD_MIN_SIZE - 0xffff);\n for (let i = apk.length - EOCD_MIN_SIZE; i >= minStart; i -= 1) {\n if (apk.readUInt32LE(i) !== EOCD_SIGNATURE) {\n continue;\n }\n const commentLength = apk.readUInt16LE(i + EOCD_COMMENT_LENGTH_FIELD);\n if (i + EOCD_MIN_SIZE + commentLength !== apk.length) {\n continue;\n }\n return {\n eocdOffset: i,\n centralDirOffset: apk.readUInt32LE(i + EOCD_CD_OFFSET_FIELD),\n centralDirSize: apk.readUInt32LE(i + 12),\n };\n }\n return invalid('APK is missing a ZIP End Of Central Directory record');\n}\n\n/**\n * Locate the APK Signing Block that sits immediately before the central\n * directory.\n *\n * @param apk - The whole APK file.\n * @param centralDirOffset - The central-directory offset from the EOCD.\n * @param eocdOffset - The EOCD start offset.\n * @param centralDirSize - The central-directory size from the EOCD.\n * @returns The signing-block offset and its payload (between the two size\n * fields, excluding the trailing size and magic).\n */\nfunction findSigningBlock(\n apk: Buffer,\n centralDirOffset: number,\n eocdOffset: number,\n centralDirSize: number,\n): { apkSigBlockOffset: number; pairs: Buffer } {\n if (centralDirOffset + centralDirSize !== eocdOffset) {\n invalid('Central directory is not immediately followed by the EOCD');\n }\n if (centralDirOffset < SIG_BLOCK_FOOTER_SIZE) {\n invalid('No APK Signing Block before the central directory');\n }\n const footer = apk.subarray(\n centralDirOffset - SIG_BLOCK_FOOTER_SIZE,\n centralDirOffset,\n );\n if (!footer.subarray(8, 24).equals(SIG_BLOCK_MAGIC)) {\n invalid('APK Signing Block magic not found (APK is not v2/v3 signed)');\n }\n const trailerSize = footer.readBigUInt64LE(0);\n const totalSize = Number(trailerSize) + 8;\n const apkSigBlockOffset = centralDirOffset - totalSize;\n if (apkSigBlockOffset < 0) {\n invalid('APK Signing Block size is out of range');\n }\n const headerSize = apk.readBigUInt64LE(apkSigBlockOffset);\n if (headerSize !== trailerSize) {\n invalid('APK Signing Block header/footer size mismatch');\n }\n const pairs = apk.subarray(\n apkSigBlockOffset + 8,\n centralDirOffset - SIG_BLOCK_FOOTER_SIZE,\n );\n return { apkSigBlockOffset, pairs };\n}\n\n/**\n * Extract a scheme block (v2 or v3) from the signing block's ID/value pairs.\n *\n * @param pairs - The concatenated ID/value pairs.\n * @returns The v3 block if present, else the v2 block, with its scheme label.\n */\nfunction selectSchemeBlock(pairs: Buffer): {\n scheme: 'v2' | 'v3';\n block: Buffer;\n} {\n const blocks = new Map<number, Buffer>();\n let cursor = 0;\n while (cursor + 8 <= pairs.length) {\n const pairSize = Number(pairs.readBigUInt64LE(cursor));\n cursor += 8;\n if (pairSize < 4 || cursor + pairSize > pairs.length) {\n break;\n }\n const id = pairs.readUInt32LE(cursor);\n const value = pairs.subarray(cursor + 4, cursor + pairSize);\n blocks.set(id, value);\n cursor += pairSize;\n }\n const v3 = blocks.get(V3_BLOCK_ID);\n if (v3) {\n return { scheme: 'v3', block: v3 };\n }\n const v2 = blocks.get(V2_BLOCK_ID);\n if (v2) {\n return { scheme: 'v2', block: v2 };\n }\n return invalid('No v2 or v3 signature block in the APK Signing Block');\n}\n\n/**\n * Read a `uint32`-length-prefixed slice, advancing the cursor past it.\n *\n * @param bytes - The buffer to read from.\n * @param cursor - The current read offset.\n * @returns The inner bytes and the offset just past them.\n */\nfunction readLenPrefixed(\n bytes: Buffer,\n cursor: number,\n): { value: Buffer; next: number } {\n if (cursor + 4 > bytes.length) {\n invalid('Truncated length-prefixed field');\n }\n const len = bytes.readUInt32LE(cursor);\n const start = cursor + 4;\n const end = start + len;\n if (end > bytes.length) {\n invalid('Length-prefixed field runs past end of block');\n }\n return { value: bytes.subarray(start, end), next: end };\n}\n\n/**\n * Split a length-prefixed sequence into its length-prefixed elements.\n *\n * @param seq - The sequence bytes (already unwrapped from its own prefix).\n * @returns The element buffers in order.\n */\nfunction readSequence(seq: Buffer): Buffer[] {\n const items: Buffer[] = [];\n let cursor = 0;\n while (cursor < seq.length) {\n const { value, next } = readLenPrefixed(seq, cursor);\n items.push(value);\n cursor = next;\n }\n return items;\n}\n\ntype SignedDataElement = { algoId: number; payload: Buffer };\n\n/**\n * Parse the `{ uint32 algoId; len-prefixed payload }` records used by both the\n * signed-data digests list and the signatures list.\n *\n * @param seq - The sequence bytes.\n * @returns The parsed algorithm-id/payload records in order.\n */\nfunction readAlgoRecords(seq: Buffer): SignedDataElement[] {\n return readSequence(seq).map((element) => {\n if (element.length < 4) {\n invalid('Algorithm record too short');\n }\n return {\n algoId: element.readUInt32LE(0),\n payload: readLenPrefixed(element, 4).value,\n };\n });\n}\n\ntype ParsedSigner = {\n signedData: Buffer;\n digests: SignedDataElement[];\n certificates: Buffer[];\n signatures: SignedDataElement[];\n publicKey: Buffer;\n signedMinSdk?: number;\n signedMaxSdk?: number;\n signerMinSdk?: number;\n signerMaxSdk?: number;\n};\n\n/**\n * Parse a single signer out of a v2/v3 scheme block.\n *\n * @param block - The scheme block value (a length-prefixed signers sequence).\n * @param scheme - Which scheme is being parsed (v3 carries extra SDK fields).\n * @returns The parsed signer.\n */\nfunction parseSigner(block: Buffer, scheme: 'v2' | 'v3'): ParsedSigner {\n const signers = readSequence(readLenPrefixed(block, 0).value);\n if (signers.length !== 1) {\n invalid(`Expected exactly one signer, found ${signers.length}`);\n }\n const signer = signers[0];\n\n let cursor = 0;\n const signedDataRead = readLenPrefixed(signer, cursor);\n const signedData = signedDataRead.value;\n cursor = signedDataRead.next;\n\n let signerMinSdk: number | undefined;\n let signerMaxSdk: number | undefined;\n if (scheme === 'v3') {\n if (cursor + 8 > signer.length) {\n invalid('v3 signer missing min/max SDK');\n }\n signerMinSdk = signer.readUInt32LE(cursor);\n signerMaxSdk = signer.readUInt32LE(cursor + 4);\n cursor += 8;\n }\n\n const signaturesRead = readLenPrefixed(signer, cursor);\n cursor = signaturesRead.next;\n\n const publicKeyRead = readLenPrefixed(signer, cursor);\n\n let sdCursor = 0;\n const digestsRead = readLenPrefixed(signedData, sdCursor);\n sdCursor = digestsRead.next;\n const certificatesRead = readLenPrefixed(signedData, sdCursor);\n sdCursor = certificatesRead.next;\n\n let signedMinSdk: number | undefined;\n let signedMaxSdk: number | undefined;\n if (scheme === 'v3') {\n if (sdCursor + 8 > signedData.length) {\n invalid('v3 signed-data missing min/max SDK');\n }\n signedMinSdk = signedData.readUInt32LE(sdCursor);\n signedMaxSdk = signedData.readUInt32LE(sdCursor + 4);\n }\n\n return {\n signedData,\n digests: readAlgoRecords(digestsRead.value),\n certificates: readSequence(certificatesRead.value),\n signatures: readAlgoRecords(signaturesRead.value),\n publicKey: publicKeyRead.value,\n signedMinSdk,\n signedMaxSdk,\n signerMinSdk,\n signerMaxSdk,\n };\n}\n\n/**\n * Compute the chunked content digest of one segment, appending each chunk's\n * digest to `out`.\n *\n * @param segment - The segment bytes.\n * @param hash - The digest algorithm.\n * @param out - Accumulator for per-chunk digests.\n * @returns The number of chunks produced.\n */\nfunction digestSegment(\n segment: Buffer,\n hash: ContentDigestAlgo,\n out: Buffer[],\n): number {\n let chunks = 0;\n for (let offset = 0; offset < segment.length; offset += CHUNK_SIZE) {\n const chunk = segment.subarray(offset, offset + CHUNK_SIZE);\n const prefix = Buffer.allocUnsafe(5);\n prefix.writeUInt8(CHUNK_PREFIX, 0);\n prefix.writeUInt32LE(chunk.length, 1);\n out.push(createHash(hash).update(prefix).update(chunk).digest());\n chunks += 1;\n }\n return chunks;\n}\n\n/**\n * Recompute the APK content digest per the v2/v3 spec: chunk the pre-signing\n * bytes, the central directory, and the EOCD (patched so its central-directory\n * offset points at the signing block), then hash the concatenated chunk\n * digests.\n *\n * @param apk - The whole APK file.\n * @param apkSigBlockOffset - Offset of the APK Signing Block.\n * @param centralDirOffset - Central-directory offset.\n * @param eocdOffset - EOCD start offset.\n * @param hash - The content-digest algorithm.\n * @returns The recomputed content digest.\n */\nfunction computeContentDigest(\n apk: Buffer,\n apkSigBlockOffset: number,\n centralDirOffset: number,\n eocdOffset: number,\n hash: ContentDigestAlgo,\n): Buffer {\n const beforeBlock = apk.subarray(0, apkSigBlockOffset);\n const centralDir = apk.subarray(centralDirOffset, eocdOffset);\n const eocd = Buffer.from(apk.subarray(eocdOffset));\n eocd.writeUInt32LE(apkSigBlockOffset, EOCD_CD_OFFSET_FIELD);\n\n const chunkDigests: Buffer[] = [];\n let count = 0;\n count += digestSegment(beforeBlock, hash, chunkDigests);\n count += digestSegment(centralDir, hash, chunkDigests);\n count += digestSegment(eocd, hash, chunkDigests);\n\n const top = createHash(hash);\n const header = Buffer.allocUnsafe(5);\n header.writeUInt8(TOP_LEVEL_PREFIX, 0);\n header.writeUInt32LE(count, 1);\n top.update(header);\n for (const digest of chunkDigests) {\n top.update(digest);\n }\n return top.digest();\n}\n\n/**\n * Verify an APK's v2/v3 signature and return its leaf signer certificate\n * SHA-256.\n *\n * @param apkPath - Path to the APK on disk.\n * @returns The verified scheme and lowercase-hex leaf certificate SHA-256.\n * @throws {@link SnapshotHelperError} when the APK is malformed or the\n * signature does not verify.\n */\nexport async function verifyApkSignerSha256(\n apkPath: string,\n): Promise<ApkSignerVerification> {\n const apk = await readFile(apkPath);\n if (apk.length > MAX_APK_BYTES) {\n invalid(`APK exceeds ${MAX_APK_BYTES} bytes`);\n }\n\n const { eocdOffset, centralDirOffset, centralDirSize } = findEocd(apk);\n const { apkSigBlockOffset, pairs } = findSigningBlock(\n apk,\n centralDirOffset,\n eocdOffset,\n centralDirSize,\n );\n const { scheme, block } = selectSchemeBlock(pairs);\n const signer = parseSigner(block, scheme);\n\n if (signer.certificates.length < 1) {\n invalid('Signer has no certificates');\n }\n\n const selected = signer.signatures\n .map((signature) => ({ signature, algo: sigAlgo(signature.algoId) }))\n .filter(\n (entry): entry is { signature: SignedDataElement; algo: SigAlgo } =>\n entry.algo !== null,\n )\n .sort((a, b) => b.algo.strength - a.algo.strength)\n .at(0);\n if (!selected) {\n invalid('No supported signature algorithm in signer');\n }\n\n const publicKey = createPublicKey({\n key: signer.publicKey,\n format: 'der',\n type: 'spki',\n });\n if (\n !selected.algo.verify(\n signer.signedData,\n publicKey,\n selected.signature.payload,\n )\n ) {\n invalid('Signature did not verify over signed-data');\n }\n\n const leaf = signer.certificates[0];\n const certPublicKey = new X509Certificate(leaf).publicKey;\n const certSpki = certPublicKey.export({ format: 'der', type: 'spki' });\n if (!certSpki.equals(signer.publicKey)) {\n invalid('Leaf certificate public key does not match the block public key');\n }\n\n const sigIds = signer.signatures.map((s) => s.algoId);\n const digestIds = signer.digests.map((d) => d.algoId);\n if (\n sigIds.length !== digestIds.length ||\n sigIds.some((id, index) => id !== digestIds[index])\n ) {\n invalid('Signature and digest algorithm lists differ');\n }\n\n const expectedDigest = signer.digests.find(\n (d) => d.algoId === selected.signature.algoId,\n );\n if (!expectedDigest) {\n invalid('No content digest for the selected algorithm');\n }\n const computed = computeContentDigest(\n apk,\n apkSigBlockOffset,\n centralDirOffset,\n eocdOffset,\n selected.algo.hash,\n );\n if (!computed.equals(expectedDigest.payload)) {\n invalid('Recomputed APK content digest does not match signed digest');\n }\n\n if (scheme === 'v3') {\n if (\n signer.signerMinSdk !== signer.signedMinSdk ||\n signer.signerMaxSdk !== signer.signedMaxSdk\n ) {\n invalid('v3 signer min/max SDK does not match signed-data');\n }\n }\n\n const signerSha256 = createHash('sha256').update(leaf).digest('hex');\n return { verified: true, signerSha256, scheme };\n}\n\n/**\n * Verify an APK and assert its signer matches an expected pinned SHA-256.\n *\n * @param apkPath - Path to the APK on disk.\n * @param expectedSignerSha256 - The pinned lowercase-hex signer SHA-256.\n * @throws {@link UntrustedHelperError} when the (validly signed) APK's signer\n * does not match the pin.\n * @throws {@link SnapshotHelperError} when the APK is malformed or unverifiable.\n */\nexport async function assertApkSignerSha256(\n apkPath: string,\n expectedSignerSha256: string,\n): Promise<void> {\n const { signerSha256 } = await verifyApkSignerSha256(apkPath);\n const expected = expectedSignerSha256.toLowerCase();\n if (signerSha256 !== expected) {\n throw new UntrustedHelperError(\n 'Installed snapshot helper is signed by an unexpected certificate',\n { expectedSignerSha256: expected, actualSignerSha256: signerSha256 },\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS verifier for APK Signature Scheme v2 and v3.
|
|
3
|
+
*
|
|
4
|
+
* This proves an APK was cryptographically SIGNED by a given key — it verifies
|
|
5
|
+
* the signature over the recomputed APK content digest, then binds the leaf
|
|
6
|
+
* certificate to that key. It is NOT a cert-presence check: reading a
|
|
7
|
+
* certificate out of an APK proves nothing (certificates are public), so this
|
|
8
|
+
* module recomputes the content digest and verifies the signature before
|
|
9
|
+
* trusting the signer. Used to gate `am instrument` against a helper that could
|
|
10
|
+
* otherwise be a package-name squatter.
|
|
11
|
+
*
|
|
12
|
+
* No external dependencies, no Android SDK, no shelling out — `node:crypto` and
|
|
13
|
+
* `node:fs` only, so it runs on any consumer machine that runs the MCP server.
|
|
14
|
+
*/
|
|
15
|
+
export type ApkSignerVerification = {
|
|
16
|
+
verified: true;
|
|
17
|
+
signerSha256: string;
|
|
18
|
+
scheme: 'v2' | 'v3';
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Verify an APK's v2/v3 signature and return its leaf signer certificate
|
|
22
|
+
* SHA-256.
|
|
23
|
+
*
|
|
24
|
+
* @param apkPath - Path to the APK on disk.
|
|
25
|
+
* @returns The verified scheme and lowercase-hex leaf certificate SHA-256.
|
|
26
|
+
* @throws {@link SnapshotHelperError} when the APK is malformed or the
|
|
27
|
+
* signature does not verify.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyApkSignerSha256(apkPath: string): Promise<ApkSignerVerification>;
|
|
30
|
+
/**
|
|
31
|
+
* Verify an APK and assert its signer matches an expected pinned SHA-256.
|
|
32
|
+
*
|
|
33
|
+
* @param apkPath - Path to the APK on disk.
|
|
34
|
+
* @param expectedSignerSha256 - The pinned lowercase-hex signer SHA-256.
|
|
35
|
+
* @throws {@link UntrustedHelperError} when the (validly signed) APK's signer
|
|
36
|
+
* does not match the pin.
|
|
37
|
+
* @throws {@link SnapshotHelperError} when the APK is malformed or unverifiable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertApkSignerSha256(apkPath: string, expectedSignerSha256: string): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=signer.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.cts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/signer.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;GAaG;AAYH,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;CACrB,CAAC;AAgbF;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,CAAC,CA2FhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS verifier for APK Signature Scheme v2 and v3.
|
|
3
|
+
*
|
|
4
|
+
* This proves an APK was cryptographically SIGNED by a given key — it verifies
|
|
5
|
+
* the signature over the recomputed APK content digest, then binds the leaf
|
|
6
|
+
* certificate to that key. It is NOT a cert-presence check: reading a
|
|
7
|
+
* certificate out of an APK proves nothing (certificates are public), so this
|
|
8
|
+
* module recomputes the content digest and verifies the signature before
|
|
9
|
+
* trusting the signer. Used to gate `am instrument` against a helper that could
|
|
10
|
+
* otherwise be a package-name squatter.
|
|
11
|
+
*
|
|
12
|
+
* No external dependencies, no Android SDK, no shelling out — `node:crypto` and
|
|
13
|
+
* `node:fs` only, so it runs on any consumer machine that runs the MCP server.
|
|
14
|
+
*/
|
|
15
|
+
export type ApkSignerVerification = {
|
|
16
|
+
verified: true;
|
|
17
|
+
signerSha256: string;
|
|
18
|
+
scheme: 'v2' | 'v3';
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Verify an APK's v2/v3 signature and return its leaf signer certificate
|
|
22
|
+
* SHA-256.
|
|
23
|
+
*
|
|
24
|
+
* @param apkPath - Path to the APK on disk.
|
|
25
|
+
* @returns The verified scheme and lowercase-hex leaf certificate SHA-256.
|
|
26
|
+
* @throws {@link SnapshotHelperError} when the APK is malformed or the
|
|
27
|
+
* signature does not verify.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyApkSignerSha256(apkPath: string): Promise<ApkSignerVerification>;
|
|
30
|
+
/**
|
|
31
|
+
* Verify an APK and assert its signer matches an expected pinned SHA-256.
|
|
32
|
+
*
|
|
33
|
+
* @param apkPath - Path to the APK on disk.
|
|
34
|
+
* @param expectedSignerSha256 - The pinned lowercase-hex signer SHA-256.
|
|
35
|
+
* @throws {@link UntrustedHelperError} when the (validly signed) APK's signer
|
|
36
|
+
* does not match the pin.
|
|
37
|
+
* @throws {@link SnapshotHelperError} when the APK is malformed or unverifiable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertApkSignerSha256(apkPath: string, expectedSignerSha256: string): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=signer.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.mts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/signer.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;GAaG;AAYH,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,IAAI,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;CACrB,CAAC;AAgbF;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,CAAC,CA2FhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CAAC,IAAI,CAAC,CASf"}
|