[GH-ISSUE #7331] [Bug] APK signing guide promises signer disagreement for a declared v1+v3 rotation without v2 #4358

Closed
opened 2026-10-31 00:49:10 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 26, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7331

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Documentation

Observed behavior

The APK signing guide says a rotation makes schemes_agree_on_signer false “with or without v2” and describes a rotated APK as showing false / true for agreement and declared signer change. A structurally readable v1+v3 fixture, with the original certificate in v1 and a different v3 certificate declaring both certificates in its lineage, instead returns true / false. This agrees with the explicitly documented block-scheme-only comparison: only v3 participates here. The guide's unconditional rotation interpretation is incorrect; this report does not request a change to the fields' comparison scope.

Reproduction

Use the baseline checkout with its locked dependencies and compiled CLI already prepared, and Node.js 24. I ran each of three fixture cases twice through the built CLI, all six calls exit 0. The following inline fixture and inspection procedure reproduces those inputs and observations from the repository root. Preparation/build commands were not run during this verification.

export WORK=$(mktemp -d)
mkdir -p "$WORK/home" "$WORK/tmp"
node --import tsx --input-type=module <<'JS'
import { writeFileSync } from 'node:fs';
import {
  zipWith, signedDataBlock, signerInfo, withSigningBlock,
  v2SchemeValue, v3SchemeValue, APK_SIGNATURE_V2_ID, APK_SIGNATURE_V3_ID,
  ROOT_CERTIFICATE as oldCert, LEAF_CERTIFICATE as newCert,
} from './tests/fixtures/apkSignature/v1SignatureBlock.ts';
const archive = async cert => zipWith([
  { name: 'classes.dex', data: 'dex' },
  { name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0\r\n\r\n' },
  { name: 'META-INF/CERT.SF', data: 'Signature-Version: 1.0\r\n\r\n' },
  { name: 'META-INF/CERT.RSA', data: signedDataBlock({
    certificates: [cert], signerInfos: [signerInfo({ names: cert })],
  }), stored: true },
]);
const rotated = {
  id: APK_SIGNATURE_V3_ID,
  value: v3SchemeValue(newCert, [oldCert, newCert]),
};
for (const [name, cert, pairs] of [
  ['lineage-v1-v3', oldCert, [rotated]],
  ['lineage-v1-v2-v3', oldCert, [
    { id: APK_SIGNATURE_V2_ID, value: v2SchemeValue(oldCert) }, rotated,
  ]],
  ['same-v1-v3', newCert, [
    { id: APK_SIGNATURE_V3_ID, value: v3SchemeValue(newCert) },
  ]],
]) {
  writeFileSync(`${process.env.WORK}/${name}.apk`,
    withSigningBlock(await archive(cert), pairs));
}
JS
for name in lineage-v1-v3 lineage-v1-v2-v3 same-v1-v3; do
  HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/config" \
  XDG_CACHE_HOME="$WORK/home/cache" XDG_DATA_HOME="$WORK/home/data" \
  TMPDIR="$WORK/tmp" node scripts/rea.mjs inspect-apk-signature \
    "$WORK/$name.apk" --format json > "$WORK/$name.json"
done
node --input-type=module <<'JS'
import { readFileSync } from 'node:fs';
for (const name of ['lineage-v1-v3', 'lineage-v1-v2-v3', 'same-v1-v3']) {
  const r = JSON.parse(readFileSync(`${process.env.WORK}/${name}.json`, 'utf8'))
    .normalized_result;
  console.log(JSON.stringify({
    name,
    v1: r.schemes.v1.signers.map(s => s.certificate?.sha256_fingerprint),
    v2_present: r.schemes.v2.present,
    v3: r.schemes.v3.signers.map(s => s.certificate?.sha256_fingerprint),
    lineage: r.schemes.v3.signers[0].rotation.lineage
      .map(n => n.certificate?.sha256_fingerprint),
    agreement: r.posture.schemes_agree_on_signer,
    declared_change: r.posture.signer_change_declared_by_rotation,
    v1_match: r.posture.v1_signers_match_block_signers,
    rotation_present: r.posture.rotation_present,
  }));
}
JS

Expected behavior

The guide's rotation examples should agree with its block-only definition and the public CLI. A decoded lineage can be present when there is only one populated block scheme; agreement can then be true and declared inter-block signer change false. The v1 comparison separately checks the lineage origin. No cryptographic or platform-acceptance conclusion follows from these facts.

Impact and scope

P3 documentation correctness defect: the guide supplies the wrong interpretation for a supported structural inspection layout. It can confuse readers comparing the described rotation pattern with the report. No incorrect certificate extraction, lost lineage, blocked workflow, or consequential downstream behavior was observed. The report exposes rotation_present, complete lineage and per-scheme fingerprints, and its explicit block-only definition gives a practical way to interpret these fixtures. Real APK prevalence and effects on downstream callers are NOT_ENOUGH_INFO; neither was sampled.

REA version

Source revision ace4dbf36e; checkout package version 6.8.0; supplied compiled runtime.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 arm64. Isolated temporary HOME and XDG directories; local built CLI only; no Android SDK, device or external provider used.

Target and artifact context

Three synthetic APK/ZIP inputs made with the checkout's fixture helpers. The lineage cases declare the original certificate in v1 and the newer certificate in v3, with a decoded two-node lineage; the second additionally declares the original certificate in v2. The same-key control has no lineage. Opaque signature bytes and placeholder content digests are intentional structural fixtures, with no installability or cryptographic-validity claim.

Interface

CLI

Earliest failure stage

Unknown

Relevant output or evidence

The failure is in the guide's interpretation, rather than a demonstrated runtime failure stage. Reduced observations from the complete public CLI responses:

Input v2 present Agreement Declared change v1 match Rotation present
lineage-v1-v3 false true false true true
lineage-v1-v2-v3 true false true true true
same-v1-v3 false true false true false

The original certificate fingerprint is b8470a4468a232423983c0b4b0414c43af60ae66ea167fdd05a06a3511ebb04d; the newer certificate fingerprint is 9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88. Both lineage cases returned those two fingerprints in that order, with no v1 or lineage decode error. v3.1 was absent in all three. Each returned artifact SHA-256 matched a separately computed SHA-256 of its input bytes.

  • Expected source: src/domain/apkSignature/apkSignatureReport.ts:179–191 explicitly defines agreement over v2/v3/v3.1 block schemes; docs/apk-signature.md:179–183 repeats that scope. The derivation at src/domain/apkSignature/apkSignatureReport.ts:384–440 applies both comparison fields to those block signer sets, while the v1 comparison has its own lineage-origin rule.
  • Failure source: docs/apk-signature.md:184–197 describes the rotation pair without the necessary inter-block-disagreement condition, and explicitly predicts false agreement “with or without v2.” The CLI observations above contradict that prediction for the lineage-v1-v3 fixture.
  • Evidence provenance: observed public CLI output and directly read documentation/source; the supplied hunter report was treated only as a lead.
  • Local verification: reproduced twice per fixture through the supplied built CLI, all six calls exit 0; no source edits or rebuild.
  • Reproduction completeness: complete for the documentation/output inconsistency on structural inputs.

Additional context

Restoration check: the guide must correctly explain all three rows above under the existing block-scheme comparison definition, including a present decoded lineage with agreement true and declared change false when v3 is the only block scheme. The v1+v2+v3 control distinguishes that case from actual disagreement between block signer sets. This report does not establish a need to change CLI results.

Duplicate search covered the supplied open/closed issue index and full relevant bodies. #6440 concerns empty-v2 suppression of the separate v1 comparison; closed PR #6813 addresses that fallback and explicitly excludes the agreement field. Open PR #7205 concerns serial INTEGER identity matching. None covers this guide's no-v2 rotation interpretation. Last known good documentation revision is NOT_ENOUGH_INFO; earlier versions were not examined.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: Incorrect rotation guidance conflicts with the defined comparison scope and observed CLI output, without an observed behavior failure or loss of metadata.
  • Workaround status: available
  • Workaround basis: The same report retains rotation presence, decoded lineage and per-scheme identities; the guide and schema explicitly define agreement over block schemes, which correctly explains all three observed rows.
Originally created by @coygeek on GitHub (Oct 26, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7331 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Documentation ## Observed behavior The APK signing guide says a rotation makes `schemes_agree_on_signer` false “with or without v2” and describes a rotated APK as showing `false / true` for agreement and declared signer change. A structurally readable v1+v3 fixture, with the original certificate in v1 and a different v3 certificate declaring both certificates in its lineage, instead returns `true / false`. This agrees with the explicitly documented block-scheme-only comparison: only v3 participates here. The guide's unconditional rotation interpretation is incorrect; this report does not request a change to the fields' comparison scope. ## Reproduction Use the baseline checkout with its locked dependencies and compiled CLI already prepared, and Node.js 24. I ran each of three fixture cases twice through the built CLI, all six calls exit 0. The following inline fixture and inspection procedure reproduces those inputs and observations from the repository root. Preparation/build commands were not run during this verification. ```sh export WORK=$(mktemp -d) mkdir -p "$WORK/home" "$WORK/tmp" node --import tsx --input-type=module <<'JS' import { writeFileSync } from 'node:fs'; import { zipWith, signedDataBlock, signerInfo, withSigningBlock, v2SchemeValue, v3SchemeValue, APK_SIGNATURE_V2_ID, APK_SIGNATURE_V3_ID, ROOT_CERTIFICATE as oldCert, LEAF_CERTIFICATE as newCert, } from './tests/fixtures/apkSignature/v1SignatureBlock.ts'; const archive = async cert => zipWith([ { name: 'classes.dex', data: 'dex' }, { name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0\r\n\r\n' }, { name: 'META-INF/CERT.SF', data: 'Signature-Version: 1.0\r\n\r\n' }, { name: 'META-INF/CERT.RSA', data: signedDataBlock({ certificates: [cert], signerInfos: [signerInfo({ names: cert })], }), stored: true }, ]); const rotated = { id: APK_SIGNATURE_V3_ID, value: v3SchemeValue(newCert, [oldCert, newCert]), }; for (const [name, cert, pairs] of [ ['lineage-v1-v3', oldCert, [rotated]], ['lineage-v1-v2-v3', oldCert, [ { id: APK_SIGNATURE_V2_ID, value: v2SchemeValue(oldCert) }, rotated, ]], ['same-v1-v3', newCert, [ { id: APK_SIGNATURE_V3_ID, value: v3SchemeValue(newCert) }, ]], ]) { writeFileSync(`${process.env.WORK}/${name}.apk`, withSigningBlock(await archive(cert), pairs)); } JS for name in lineage-v1-v3 lineage-v1-v2-v3 same-v1-v3; do HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/config" \ XDG_CACHE_HOME="$WORK/home/cache" XDG_DATA_HOME="$WORK/home/data" \ TMPDIR="$WORK/tmp" node scripts/rea.mjs inspect-apk-signature \ "$WORK/$name.apk" --format json > "$WORK/$name.json" done node --input-type=module <<'JS' import { readFileSync } from 'node:fs'; for (const name of ['lineage-v1-v3', 'lineage-v1-v2-v3', 'same-v1-v3']) { const r = JSON.parse(readFileSync(`${process.env.WORK}/${name}.json`, 'utf8')) .normalized_result; console.log(JSON.stringify({ name, v1: r.schemes.v1.signers.map(s => s.certificate?.sha256_fingerprint), v2_present: r.schemes.v2.present, v3: r.schemes.v3.signers.map(s => s.certificate?.sha256_fingerprint), lineage: r.schemes.v3.signers[0].rotation.lineage .map(n => n.certificate?.sha256_fingerprint), agreement: r.posture.schemes_agree_on_signer, declared_change: r.posture.signer_change_declared_by_rotation, v1_match: r.posture.v1_signers_match_block_signers, rotation_present: r.posture.rotation_present, })); } JS ``` ## Expected behavior The guide's rotation examples should agree with its block-only definition and the public CLI. A decoded lineage can be present when there is only one populated block scheme; agreement can then be true and declared inter-block signer change false. The v1 comparison separately checks the lineage origin. No cryptographic or platform-acceptance conclusion follows from these facts. ## Impact and scope P3 documentation correctness defect: the guide supplies the wrong interpretation for a supported structural inspection layout. It can confuse readers comparing the described rotation pattern with the report. No incorrect certificate extraction, lost lineage, blocked workflow, or consequential downstream behavior was observed. The report exposes `rotation_present`, complete lineage and per-scheme fingerprints, and its explicit block-only definition gives a practical way to interpret these fixtures. Real APK prevalence and effects on downstream callers are NOT_ENOUGH_INFO; neither was sampled. ## REA version Source revision ace4dbf36e8c8f36a0e192aff9a11484ed428358; checkout package version 6.8.0; supplied compiled runtime. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1 arm64. Isolated temporary HOME and XDG directories; local built CLI only; no Android SDK, device or external provider used. ## Target and artifact context Three synthetic APK/ZIP inputs made with the checkout's fixture helpers. The lineage cases declare the original certificate in v1 and the newer certificate in v3, with a decoded two-node lineage; the second additionally declares the original certificate in v2. The same-key control has no lineage. Opaque signature bytes and placeholder content digests are intentional structural fixtures, with no installability or cryptographic-validity claim. ## Interface CLI ## Earliest failure stage Unknown ## Relevant output or evidence The failure is in the guide's interpretation, rather than a demonstrated runtime failure stage. Reduced observations from the complete public CLI responses: | Input | v2 present | Agreement | Declared change | v1 match | Rotation present | | --- | --- | --- | --- | --- | --- | | lineage-v1-v3 | false | true | false | true | true | | lineage-v1-v2-v3 | true | false | true | true | true | | same-v1-v3 | false | true | false | true | false | The original certificate fingerprint is `b8470a4468a232423983c0b4b0414c43af60ae66ea167fdd05a06a3511ebb04d`; the newer certificate fingerprint is `9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88`. Both lineage cases returned those two fingerprints in that order, with no v1 or lineage decode error. v3.1 was absent in all three. Each returned artifact SHA-256 matched a separately computed SHA-256 of its input bytes. - Expected source: `src/domain/apkSignature/apkSignatureReport.ts:179–191` explicitly defines agreement over v2/v3/v3.1 block schemes; `docs/apk-signature.md:179–183` repeats that scope. The derivation at `src/domain/apkSignature/apkSignatureReport.ts:384–440` applies both comparison fields to those block signer sets, while the v1 comparison has its own lineage-origin rule. - Failure source: `docs/apk-signature.md:184–197` describes the rotation pair without the necessary inter-block-disagreement condition, and explicitly predicts false agreement “with or without v2.” The CLI observations above contradict that prediction for the lineage-v1-v3 fixture. - Evidence provenance: observed public CLI output and directly read documentation/source; the supplied hunter report was treated only as a lead. - Local verification: reproduced twice per fixture through the supplied built CLI, all six calls exit 0; no source edits or rebuild. - Reproduction completeness: complete for the documentation/output inconsistency on structural inputs. ## Additional context Restoration check: the guide must correctly explain all three rows above under the existing block-scheme comparison definition, including a present decoded lineage with agreement true and declared change false when v3 is the only block scheme. The v1+v2+v3 control distinguishes that case from actual disagreement between block signer sets. This report does not establish a need to change CLI results. Duplicate search covered the supplied open/closed issue index and full relevant bodies. #6440 concerns empty-v2 suppression of the separate v1 comparison; closed PR #6813 addresses that fallback and explicitly excludes the agreement field. Open PR #7205 concerns serial INTEGER identity matching. None covers this guide's no-v2 rotation interpretation. Last known good documentation revision is NOT_ENOUGH_INFO; earlier versions were not examined. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: Incorrect rotation guidance conflicts with the defined comparison scope and observed CLI output, without an observed behavior failure or loss of metadata. - Workaround status: available - Workaround basis: The same report retains rotation presence, decoded lineage and per-scheme identities; the guide and schema explicitly define agreement over block schemes, which correctly explains all three observed rows.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4358
No description provided.