docs(apk): clarify rotated signer agreement (#7936)

* docs(apk): clarify rotated signer agreement

* test(mcp): assert bundle path issues by field

compare_bundles reports each relative path as an invalid_format issue
instead of one joined diagnostic sentence.

* fix(javascript): ignore worker frames for a different source job

A failure frame for another job was dequeued as an output error, so a
later caller abort lost the race and was not reported as cancellation.

* test(flutter): assert ABI selector refusals as structured issues

inspect_dart_aot reports the ABI label as an issue on path abi. The
message is the selector contract, without an abi: prefix.

* fix(javascript): check each worker frame condition once

A ready frame is rejected on its own. A non-ready frame for another job
keeps waiting. The following id comparison repeated that skip, so it
never ran.

---------

Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-10-29 20:00:02 +08:00
1 parent 1ef5918179
commit 6fa48f201f
3 files changed
+42 -11

No files matched your search

+14 -10
View File
@@ -179,22 +179,26 @@ share the application workflow, projection, retention and publication owners.
- `schemes_agree_on_signer` (observed): every present block scheme presents
the same _set_ of signer certificates. An extra signer in one scheme is
disagreement; a signer without a certificate makes it `null` (unknown).
Build-tools 33+ rotate through v3.1 by default, so a rotated app usually
reads `false` here and `true` below.
A single present block scheme with known signers agrees with itself;
v1 is compared separately below.
- `signer_change_declared_by_rotation` (declared): the signers differ, but
each appears in a lineage the APK declares. Read the two together: a
rotated APK shows `false` / `true`; signers that differ with no declared
lineage containing them show `false` / `false`. It is `null` when a
signer's identity is unknown, or when the signers differ and a declared
lineage could not be decoded.
each appears in a lineage the APK declares. Read the two together: block
schemes with different signers covered by a declared lineage show `false`
/ `true`; differing signers without such a lineage show `false` / `false`.
A lineage alone does not establish a change between block schemes. It is
`null` when a signer's identity is unknown, or when the signers differ and
a declared lineage could not be decoded.
- `v1_signers_match_block_signers` (observed): the v1 signer certificates
equal what the lowest block scheme that has signers (v2, else v3, else
v3.1) expects v1 to carry. That is its signer set, except that a v3/v3.1
signer declaring a rotation lineage expects the lineage's first
(original) certificate, as apksig requires. `false` is notable: the JAR
signature names a different key than the block signature. A key rotation
keeps v1 (and v2) on the original key, so it reads `true` while
`schemes_agree_on_signer` reads `false`, with or without v2. `null` when
signature names a different key than the block signature. When v1 uses
the lineage's original key and v3 its newer key, this comparison reads
`true`. If v3 is the only present block scheme, agreement is `true` and
declared inter-block signer change is `false` even though rotation is
present. If v2 also uses the original key, agreement is `false` and the
declared inter-block change is `true`. `null` when
v1 or a populated block scheme is absent, any signer's identity is unknown (a
`decode_error`, a `null` certificate or a `subjectKeyIdentifier`
`SignerInfo`), or the lineage's original certificate cannot be
@@ -355,11 +355,15 @@ export class JavaScriptAnalysisWorker {
);
for (;;) {
const response = await this.#next(deadline, file.path);
if (response.kind === "ready" || response.id !== id)
// A ready frame is outside this job. A frame for another job is not
// this request's result; keep waiting so a later caller abort is
// still cancellation.
if (response.kind === "ready")
throw new AnalysisOutputError(
OPERATION,
"JavaScript worker response names a different source job",
);
if (response.id !== id) continue;
if (response.kind === "phase") {
this.#phase = response.phase;
await onPhase?.(response.phase);
@@ -81,6 +81,20 @@ cliTest(
},
]),
);
const rotatedWithV2 = join(root, "v1-v2-v3-rotated.apk");
await writeFile(
rotatedWithV2,
withSigningBlock(v1Zip, [
{ id: APK_SIGNATURE_V2_ID, value: v2SchemeValue(LEAF_CERTIFICATE) },
{
id: APK_SIGNATURE_V3_ID,
value: v3SchemeValue(ROOT_CERTIFICATE, [
LEAF_CERTIFICATE,
ROOT_CERTIFICATE,
]),
},
]),
);
// A lineage that does not start at the v1 key is still a mismatch.
const misrotated = join(root, "v1-v3-misrotated.apk");
await writeFile(
@@ -106,6 +120,7 @@ cliTest(
const cliV1 = await viaCli(v1Only);
const cliMixed = await viaCli(mixed);
const cliRotated = await viaCli(rotated);
const cliRotatedWithV2 = await viaCli(rotatedWithV2);
const cliMisrotated = await viaCli(misrotated);
expect(cliV1.normalized_result.schemes.v1.signers).toMatchObject([
@@ -127,9 +142,16 @@ cliTest(
expect(cliRotated.normalized_result.posture).toMatchObject({
v1_only: false,
rotation_present: true,
schemes_agree_on_signer: true,
signer_change_declared_by_rotation: false,
v1_signers_match_block_signers: true,
});
expect(cliRotatedWithV2.normalized_result.posture).toMatchObject({
rotation_present: true,
schemes_agree_on_signer: false,
signer_change_declared_by_rotation: true,
v1_signers_match_block_signers: true,
});
expect(
cliMisrotated.normalized_result.posture.v1_signers_match_block_signers,
).toBe(false);
@@ -157,6 +179,7 @@ cliTest(
[v1Only, cliV1],
[mixed, cliMixed],
[rotated, cliRotated],
[rotatedWithV2, cliRotatedWithV2],
] as const) {
const reply = await client.callTool(
{ name: tool.name, arguments: { path, detail: "complete" } },