[GH-ISSUE #6440] [Bug] inspect-apk-signature skips v3 signer comparison when v2 has no signers #3853

Closed
opened 2026-10-31 00:44:39 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

inspect-apk-signature returns posture.v1_signers_match_block_signers: null when a present v2 block has no signers, even though v1 and v3 have the same known certificate and v3 declares no rotation. Removing only the empty v2 pair makes the comparison true. Both commands exit successfully.

Reproduction

From a fresh checkout at 9eb829aca34be36ff112826337996f6139f82eff, prepare the runtime with npm ci && npm run build:cached. Then generate two synthetic APK/ZIP inputs inline using that revision's fixture helpers:

export WORK=$(mktemp -d)
mkdir -p "$WORK/home"
node --import tsx --input-type=module <<'JS'
import { writeFile } from 'node:fs/promises';
import {
  zipWith, withSigningBlock, v3SchemeValue, LEAF_CERTIFICATE,
  OPENSSL_BLOCK_ISSUER_SERIAL, APK_SIGNATURE_V2_ID, APK_SIGNATURE_V3_ID,
} from './tests/fixtures/apkSignature/v1SignatureBlock.ts';

const zip = await zipWith([
  { name: 'classes.dex', data: 'dex' },
  { name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0' },
  { name: 'META-INF/CERT.SF', data: 'Signature-Version: 1.0' },
  { name: 'META-INF/CERT.EC', data: OPENSSL_BLOCK_ISSUER_SERIAL, stored: true },
]);
const v3 = { id: APK_SIGNATURE_V3_ID, value: v3SchemeValue(LEAF_CERTIFICATE) };
for (const [name, pairs] of [
  ['empty-v2', [{ id: APK_SIGNATURE_V2_ID, value: Buffer.alloc(4) }, v3]],
  ['v3-only', [v3]],
]) {
  await writeFile(`${process.env.WORK}/${name}.apk`, withSigningBlock(zip, pairs));
}
JS
HOME="$WORK/home" node scripts/rea.mjs inspect-apk-signature "$WORK/empty-v2.apk" --format json
HOME="$WORK/home" node scripts/rea.mjs inspect-apk-signature "$WORK/v3-only.apk" --format json

Inspect normalized_result.schemes and normalized_result.posture in the complete Evidence. The empty-v2 case reproduced in all three CLI runs. The v3-only matching control returned true in both runs. These observations used the supplied built runtime at the baseline; the fresh installation/build step above is preparation guidance.

Expected behavior

Compare v1 with the first present block scheme containing signers: v3 in this input. The matching case should return true.

The documented rule selects the lowest scheme with signers; the schema comment repeats it. The inspection scope explicitly excludes cryptographic verification, so this expectation concerns declared certificate identities.

Impact and scope

P2: a bounded incorrect derived result. The complete Evidence preserves both certificate fingerprints, allowing callers to compare them directly for this no-rotation input. Frequency in real APKs is unknown.

The same fallback also loses a known mismatch: substituting ROOT_CERTIFICATE for the v3 certificate returned false without v2 and null with empty v2, once each. No security boundary crossing was observed, and no claim of signature authenticity or Android installation acceptance is made.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. No external analysis engine or device was used. The checkout's exact Node.js 24.18.0 pin was not tested.

Target and artifact context

Synthetic local ZIP/APK with a PKCS#7 v1 block containing leaf and root certificates and one SignerInfo selecting the leaf by issuer and serial; an empty v2 signer sequence; and a v3 signer naming the same leaf without rotation. Block signatures are opaque fixture values and the declared content digest is a placeholder. This is a structural inspection input, not a claim of valid cryptographic signing.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The empty-v2 command returned exit 0 with these facts:

{
  "v1_fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88",
  "v2": { "present": true, "signers": [], "uninterpreted_bytes": 0 },
  "v3_fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88",
  "v3_rotation_present": false,
  "v1_signers_match_block_signers": null
}

This is a reduced projection of the CLI Evidence: fingerprints come from each scheme's signers[0].certificate.sha256_fingerprint, v3 rotation from signers[0].rotation.present, and the comparison from posture. The v1 signer has decode_error: null. No limitation explains an unknown comparison. The otherwise identical v3-only control returns true.

Typed outcome or recovery expected

posture.v1_signers_match_block_signers: true for the matching fixture, and false when the first scheme with signers names a different certificate. An empty lower scheme should not suppress the comparison.

Analysis or suspected cause

Source-derived: decodeSigners accepts an empty sequence, and decodeScheme marks the pair present. The posture derivation passes present[0], selecting empty v2; the comparison guard immediately returns null instead of considering v3.

Additional context

schemes_agree_on_signer: false and self_consistent: false are expected for these fixture values and are outside this report. Open and closed issue searches found no matching report; #4537 describes the general inspection feature and #717 the Android roadmap. Last known good version: NOT_ENOUGH_INFO; no earlier revision was tested.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6440 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior `inspect-apk-signature` returns `posture.v1_signers_match_block_signers: null` when a present v2 block has no signers, even though v1 and v3 have the same known certificate and v3 declares no rotation. Removing only the empty v2 pair makes the comparison `true`. Both commands exit successfully. ### Reproduction From a fresh checkout at `9eb829aca34be36ff112826337996f6139f82eff`, prepare the runtime with `npm ci && npm run build:cached`. Then generate two synthetic APK/ZIP inputs inline using that revision's fixture helpers: ```sh export WORK=$(mktemp -d) mkdir -p "$WORK/home" node --import tsx --input-type=module <<'JS' import { writeFile } from 'node:fs/promises'; import { zipWith, withSigningBlock, v3SchemeValue, LEAF_CERTIFICATE, OPENSSL_BLOCK_ISSUER_SERIAL, APK_SIGNATURE_V2_ID, APK_SIGNATURE_V3_ID, } from './tests/fixtures/apkSignature/v1SignatureBlock.ts'; const zip = await zipWith([ { name: 'classes.dex', data: 'dex' }, { name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0' }, { name: 'META-INF/CERT.SF', data: 'Signature-Version: 1.0' }, { name: 'META-INF/CERT.EC', data: OPENSSL_BLOCK_ISSUER_SERIAL, stored: true }, ]); const v3 = { id: APK_SIGNATURE_V3_ID, value: v3SchemeValue(LEAF_CERTIFICATE) }; for (const [name, pairs] of [ ['empty-v2', [{ id: APK_SIGNATURE_V2_ID, value: Buffer.alloc(4) }, v3]], ['v3-only', [v3]], ]) { await writeFile(`${process.env.WORK}/${name}.apk`, withSigningBlock(zip, pairs)); } JS HOME="$WORK/home" node scripts/rea.mjs inspect-apk-signature "$WORK/empty-v2.apk" --format json HOME="$WORK/home" node scripts/rea.mjs inspect-apk-signature "$WORK/v3-only.apk" --format json ``` Inspect `normalized_result.schemes` and `normalized_result.posture` in the complete Evidence. The empty-v2 case reproduced in all three CLI runs. The v3-only matching control returned `true` in both runs. These observations used the supplied built runtime at the baseline; the fresh installation/build step above is preparation guidance. ### Expected behavior Compare v1 with the first present block scheme containing signers: v3 in this input. The matching case should return `true`. The [documented rule](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/apk-signature.md#L180) selects the lowest scheme with signers; the [schema comment](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/apkSignature/apkSignatureReport.ts#L193) repeats it. The [inspection scope](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/apk-signature.md#L197) explicitly excludes cryptographic verification, so this expectation concerns declared certificate identities. ### Impact and scope P2: a bounded incorrect derived result. The complete Evidence preserves both certificate fingerprints, allowing callers to compare them directly for this no-rotation input. Frequency in real APKs is unknown. The same fallback also loses a known mismatch: substituting `ROOT_CERTIFICATE` for the v3 certificate returned `false` without v2 and `null` with empty v2, once each. No security boundary crossing was observed, and no claim of signature authenticity or Android installation acceptance is made. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. No external analysis engine or device was used. The checkout's exact Node.js 24.18.0 pin was not tested. ### Target and artifact context Synthetic local ZIP/APK with a PKCS#7 v1 block containing leaf and root certificates and one SignerInfo selecting the leaf by issuer and serial; an empty v2 signer sequence; and a v3 signer naming the same leaf without rotation. Block signatures are opaque fixture values and the declared content digest is a placeholder. This is a structural inspection input, not a claim of valid cryptographic signing. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The empty-v2 command returned exit 0 with these facts: ```json { "v1_fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88", "v2": { "present": true, "signers": [], "uninterpreted_bytes": 0 }, "v3_fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88", "v3_rotation_present": false, "v1_signers_match_block_signers": null } ``` This is a reduced projection of the CLI Evidence: fingerprints come from each scheme's `signers[0].certificate.sha256_fingerprint`, v3 rotation from `signers[0].rotation.present`, and the comparison from `posture`. The v1 signer has `decode_error: null`. No limitation explains an unknown comparison. The otherwise identical v3-only control returns `true`. ### Typed outcome or recovery expected `posture.v1_signers_match_block_signers: true` for the matching fixture, and `false` when the first scheme with signers names a different certificate. An empty lower scheme should not suppress the comparison. ### Analysis or suspected cause Source-derived: [decodeSigners](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/apkSignature/apkSignatureSchemes.ts#L248) accepts an empty sequence, and [decodeScheme](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/apkSignature/ApkSignatureProvider.ts#L605) marks the pair present. The [posture derivation](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/apkSignature/apkSignatureReport.ts#L414) passes `present[0]`, selecting empty v2; the [comparison guard](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/apkSignature/apkSignatureReport.ts#L349) immediately returns `null` instead of considering v3. ### Additional context `schemes_agree_on_signer: false` and `self_consistent: false` are expected for these fixture values and are outside this report. Open and closed issue searches found no matching report; #4537 describes the general inspection feature and #717 the Android roadmap. Last known good version: NOT_ENOUGH_INFO; no earlier revision was tested.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

At d87e73f2db50be72d33a4b3c7199af35518b441c, posture derivation now selects the first present block scheme with signers (source), so an empty v2 signer list no longer prevents v1/v3 comparison. The regression explicitly keeps v2 present and empty while asserting v3 match, mismatch and unknown outcomes.

I ran the cited regression file on this revision; all passed as part of the focused 28-file, 603-test run. Provider-facing checks here use controlled fixtures; this does not claim a run with the real analysis engine. Closing as completed on main.

<!-- gh-comment-id:6380641856 --> @coygeek commented on GitHub (Oct 24, 2026): At `d87e73f2db50be72d33a4b3c7199af35518b441c`, posture derivation now selects the first present block scheme with signers ([source](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/apkSignature/apkSignatureReport.ts#L414)), so an empty v2 signer list no longer prevents v1/v3 comparison. The [regression](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/apkSignature/apkSignatureReport.test.ts#L284) explicitly keeps v2 present and empty while asserting v3 match, mismatch and unknown outcomes. I ran the cited regression file on this revision; all passed as part of the focused 28-file, 603-test run. Provider-facing checks here use controlled fixtures; this does not claim a run with the real analysis engine. Closing as completed on main.
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#3853
No description provided.