[GH-ISSUE #7411] [Bug] inspect-apk-signature omits the documented explanation for an unknown v1 comparison #4417

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

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

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

Inspecting a synthetic archive with an unreadable v1 signature block and a populated v2 signer returns posture.v1_signers_match_block_signers: null, but none of the five returned limitations explains that unknown comparison. The v1 signer row retains decode_error: "malformed PKCS#7: contentType is missing", so the reason remains available elsewhere in the complete response. The guide promises that a limitation explains the null result.

Reproduction

  1. Use the baseline checkout with its supplied dependencies and built runtime, with Node 24 on PATH. Start in an empty directory beside that checkout, with the checkout accessible as ../rea. All fixture, output and isolated HOME paths below are relative to the empty directory; no build or installation is part of this reproduction.
  2. Create the reduced archive with Python 3. It contains a matching .SF/.RSA pair and a framed v2 signer; certificate and signature bytes intentionally exercise structural reporting rather than cryptographic validity.
cat > make_fixture.py <<'PYFIX'
import io
import struct
import zipfile
from pathlib import Path

def lp(value):
    return struct.pack('<I', len(value)) + value

archive = io.BytesIO()
with zipfile.ZipFile(archive, 'w', compression=zipfile.ZIP_STORED) as z:
    z.writestr('AndroidManifest.xml', b'manifest')
    z.writestr('META-INF/MANIFEST.MF', b'Manifest-Version: 1.0\r\n\r\n')
    z.writestr('META-INF/CERT.SF', b'Signature-Version: 1.0\r\n\r\n')
    z.writestr('META-INF/CERT.RSA', b'\x30\x82\x00\x00')
raw = archive.getvalue()
eocd = len(raw) - 22
central_offset = struct.unpack_from('<I', raw, eocd + 16)[0]
signed_data = lp(b'') + lp(lp(b'\x30\x82\x00\x00')) + lp(b'')
signer = lp(signed_data) + lp(b'') + lp(b'')
scheme = lp(lp(signer))
pair = struct.pack('<QI', len(scheme) + 4, 0x7109871a) + scheme
block_size = len(pair) + 24
block = struct.pack('<Q', block_size) + pair + struct.pack('<Q', block_size) + b'APK Sig Block 42'
patched_eocd = bytearray(raw[eocd:])
struct.pack_into('<I', patched_eocd, 16, central_offset + len(block))
Path('sample.apk').write_bytes(raw[:central_offset] + block + raw[central_offset:eocd] + patched_eocd)
PYFIX
python3 make_fixture.py
mkdir -p home
  1. Execute the public CLI twice and print a reduced projection of each complete response. Four invocations in total reproduced the omission; the first two were direct CLI invocations, and the final two used this harness, which additionally recorded exit code 0. All four produced complete JSON and empty stderr.
python3 - <<'PYRUN'
import json, os, subprocess
from pathlib import Path
for n in (1, 2):
    cp = subprocess.run(['node', '../rea/scripts/rea.mjs', 'inspect-apk-signature', 'sample.apk', '--json'], env={**os.environ, 'HOME': str(Path('home').resolve())}, text=True, capture_output=True)
    Path(f'run-{n}.json').write_text(cp.stdout)
    Path(f'run-{n}.stderr').write_text(cp.stderr)
    evidence=json.loads(cp.stdout)
    r=evidence['normalized_result']
    result={'attempt': n, 'exit_code': cp.returncode, 'v1_present': r['schemes']['v1']['present'], 'v2_present': r['schemes']['v2']['present'], 'v1_decode_error': r['schemes']['v1']['signers'][0]['decode_error'], 'v1_signers_match_block_signers': r['posture']['v1_signers_match_block_signers'], 'limitations': evidence['limitations']}
    print(json.dumps(result, indent=2))
    Path(f'projection-{n}.json').write_text(json.dumps(result, indent=2)+'\n')
PYRUN

Expected behavior

In docs/apk-signature.md, under "What it reports" → "Posture", the v1_signers_match_block_signers description says: "null when v1 or a 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 identified; a limitation then says why." For this unreadable v1 identity, a limitation should explain why the comparison remains unknown. The text does not require any particular message wording. Alternatively, the guide needs to state accurately which null cases receive that explanation.

Impact and scope

The omission reproduced in all four CLI invocations of this malformed-v1/populated-v2 fixture. The comparison value is correctly conservative, and no lost signer fact or consequential downstream behavior was observed. Reading the retained v1 decode_error gives the caller an available explanation, but callers following the documented limitations contract do not find it there. Real APK frequency, other archive layouts and MCP delivery were not tested.

REA version

6.8.0 (source commit a905a71e31)

Runtime and platform

Node 24.22.1; macOS 27.0.1 arm64.

Target and artifact context

Synthetic stored ZIP/APK with four entries and a v2 APK Signing Block. Its malformed v1 ContentInfo has no contentType. The v2 certificate bytes are also synthetic and unreadable as X.509, but REA retains a non-null fingerprint over those declared bytes. No Android SDK, device or external analysis provider was used, and no signature validity or installation acceptance is claimed.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed first projection from the executed harness below; the second differed only in attempt. The array shown is the complete returned limitations array, with no reason specific to the unreadable v1 signer.

{
  "attempt": 1,
  "exit_code": 0,
  "v1_present": true,
  "v2_present": true,
  "v1_decode_error": "malformed PKCS#7: contentType is missing",
  "v1_signers_match_block_signers": null,
  "limitations": [
    "Static structural inspection only. No Android code executes, no device or adb is involved, and odex/vdex/oat/cdex are not read.",
    "No signature is verified: signer, rotation-lineage and source-stamp signatures are NOT cryptographically checked, and certificate chains, revocation and authenticity are NOT evaluated. Certificates and lineages are what the APK declares.",
    "matches_apk and self_consistent only show that the declared content digest equals the digest of the current bytes. This is not a signature verification: anyone who changes the bytes can recompute the digest and write it into the signed data. SHA-256 identifies the exact bytes, not the producer.",
    "v1 signers are what each signature block's PKCS#7 SignedData declares: its signatures, the .SF and MANIFEST.MF digests and entry coverage are NOT verified.",
    "Not read: v4 .apk.idsig, verity digests (matches_apk null), and the v1 X-Android-APK-Signed marker, so v1_only cannot distinguish a v1-only APK from one whose v2/v3 block was removed."
  ]
}

Expected source: docs/apk-signature.md:197 through line 201 contains the quoted null-result explanation promise. The same guide's per-v1-signer description at docs/apk-signature.md:110 through line 116 explicitly retains malformed blocks with decode_error, so partial inspection applies to this input.

Failure source, source-only (not separately executed): src/domain/apkSignature/apkSignatureReport.ts:361 returns a null match without a note when a signer fingerprint is null. src/apkSignature/apkV1Signers.ts:129 generates other v1 notes but no general decode-error note. src/apkSignature/ApkSignatureProvider.ts:498 combines the fixed limitations and those two note sources. The production CLI observations above establish that this path is reachable and that the explanation is absent from its output.

Typed outcome or recovery expected

Keep the unknown comparison value and retained signer error, and make the returned limitations agree with the documented explanation promise. No new field or exact error string is required.

Analysis or suspected cause

Source-derived: src/domain/apkSignature/apkSignatureReport.ts:354 supplies a note when the lineage origin cannot be determined, while the unknown-identity branch supplies none. The subjectKeyIdentifier case already gets a separate explanation from src/apkSignature/apkV1Signers.ts:129; this report does not claim every null comparison lacks a limitation. The observed gap is the malformed-v1 case above.

Additional context

Related reports have different acceptance criteria: closed #6440 corrects an inappropriate null result caused by an empty v2 scheme, while this input has a populated v2 signer and an appropriate null result without the promised explanation. Open #7331 concerns the guide's interpretation of rotation and the separate block-scheme agreement fields. Open #7316 concerns discovery prerequisites. None requests this missing limitation. The registry query npm view rea-agents version returned 6.8.0, matching the checkout's package version. No earlier known-good version was established.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7411 ### 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 Inspecting a synthetic archive with an unreadable v1 signature block and a populated v2 signer returns `posture.v1_signers_match_block_signers: null`, but none of the five returned `limitations` explains that unknown comparison. The v1 signer row retains `decode_error: "malformed PKCS#7: contentType is missing"`, so the reason remains available elsewhere in the complete response. The guide promises that a limitation explains the null result. ### Reproduction 1. Use the baseline checkout with its supplied dependencies and built runtime, with Node 24 on PATH. Start in an empty directory beside that checkout, with the checkout accessible as `../rea`. All fixture, output and isolated HOME paths below are relative to the empty directory; no build or installation is part of this reproduction. 2. Create the reduced archive with Python 3. It contains a matching `.SF`/`.RSA` pair and a framed v2 signer; certificate and signature bytes intentionally exercise structural reporting rather than cryptographic validity. ```sh cat > make_fixture.py <<'PYFIX' import io import struct import zipfile from pathlib import Path def lp(value): return struct.pack('<I', len(value)) + value archive = io.BytesIO() with zipfile.ZipFile(archive, 'w', compression=zipfile.ZIP_STORED) as z: z.writestr('AndroidManifest.xml', b'manifest') z.writestr('META-INF/MANIFEST.MF', b'Manifest-Version: 1.0\r\n\r\n') z.writestr('META-INF/CERT.SF', b'Signature-Version: 1.0\r\n\r\n') z.writestr('META-INF/CERT.RSA', b'\x30\x82\x00\x00') raw = archive.getvalue() eocd = len(raw) - 22 central_offset = struct.unpack_from('<I', raw, eocd + 16)[0] signed_data = lp(b'') + lp(lp(b'\x30\x82\x00\x00')) + lp(b'') signer = lp(signed_data) + lp(b'') + lp(b'') scheme = lp(lp(signer)) pair = struct.pack('<QI', len(scheme) + 4, 0x7109871a) + scheme block_size = len(pair) + 24 block = struct.pack('<Q', block_size) + pair + struct.pack('<Q', block_size) + b'APK Sig Block 42' patched_eocd = bytearray(raw[eocd:]) struct.pack_into('<I', patched_eocd, 16, central_offset + len(block)) Path('sample.apk').write_bytes(raw[:central_offset] + block + raw[central_offset:eocd] + patched_eocd) PYFIX python3 make_fixture.py mkdir -p home ``` 3. Execute the public CLI twice and print a reduced projection of each complete response. Four invocations in total reproduced the omission; the first two were direct CLI invocations, and the final two used this harness, which additionally recorded exit code 0. All four produced complete JSON and empty stderr. ```sh python3 - <<'PYRUN' import json, os, subprocess from pathlib import Path for n in (1, 2): cp = subprocess.run(['node', '../rea/scripts/rea.mjs', 'inspect-apk-signature', 'sample.apk', '--json'], env={**os.environ, 'HOME': str(Path('home').resolve())}, text=True, capture_output=True) Path(f'run-{n}.json').write_text(cp.stdout) Path(f'run-{n}.stderr').write_text(cp.stderr) evidence=json.loads(cp.stdout) r=evidence['normalized_result'] result={'attempt': n, 'exit_code': cp.returncode, 'v1_present': r['schemes']['v1']['present'], 'v2_present': r['schemes']['v2']['present'], 'v1_decode_error': r['schemes']['v1']['signers'][0]['decode_error'], 'v1_signers_match_block_signers': r['posture']['v1_signers_match_block_signers'], 'limitations': evidence['limitations']} print(json.dumps(result, indent=2)) Path(f'projection-{n}.json').write_text(json.dumps(result, indent=2)+'\n') PYRUN ``` ### Expected behavior In `docs/apk-signature.md`, under "What it reports" → "Posture", the `v1_signers_match_block_signers` description says: "`null` when v1 or a 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 identified; a limitation then says why." For this unreadable v1 identity, a limitation should explain why the comparison remains unknown. The text does not require any particular message wording. Alternatively, the guide needs to state accurately which null cases receive that explanation. ### Impact and scope The omission reproduced in all four CLI invocations of this malformed-v1/populated-v2 fixture. The comparison value is correctly conservative, and no lost signer fact or consequential downstream behavior was observed. Reading the retained v1 `decode_error` gives the caller an available explanation, but callers following the documented `limitations` contract do not find it there. Real APK frequency, other archive layouts and MCP delivery were not tested. ### REA version 6.8.0 (source commit a905a71e31f71457cb21fd056b234f872126974e) ### Runtime and platform Node 24.22.1; macOS 27.0.1 arm64. ### Target and artifact context Synthetic stored ZIP/APK with four entries and a v2 APK Signing Block. Its malformed v1 ContentInfo has no contentType. The v2 certificate bytes are also synthetic and unreadable as X.509, but REA retains a non-null fingerprint over those declared bytes. No Android SDK, device or external analysis provider was used, and no signature validity or installation acceptance is claimed. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed first projection from the executed harness below; the second differed only in `attempt`. The array shown is the complete returned limitations array, with no reason specific to the unreadable v1 signer. ```json { "attempt": 1, "exit_code": 0, "v1_present": true, "v2_present": true, "v1_decode_error": "malformed PKCS#7: contentType is missing", "v1_signers_match_block_signers": null, "limitations": [ "Static structural inspection only. No Android code executes, no device or adb is involved, and odex/vdex/oat/cdex are not read.", "No signature is verified: signer, rotation-lineage and source-stamp signatures are NOT cryptographically checked, and certificate chains, revocation and authenticity are NOT evaluated. Certificates and lineages are what the APK declares.", "matches_apk and self_consistent only show that the declared content digest equals the digest of the current bytes. This is not a signature verification: anyone who changes the bytes can recompute the digest and write it into the signed data. SHA-256 identifies the exact bytes, not the producer.", "v1 signers are what each signature block's PKCS#7 SignedData declares: its signatures, the .SF and MANIFEST.MF digests and entry coverage are NOT verified.", "Not read: v4 .apk.idsig, verity digests (matches_apk null), and the v1 X-Android-APK-Signed marker, so v1_only cannot distinguish a v1-only APK from one whose v2/v3 block was removed." ] } ``` Expected source: `docs/apk-signature.md:197` through line 201 contains the quoted null-result explanation promise. The same guide's per-v1-signer description at `docs/apk-signature.md:110` through line 116 explicitly retains malformed blocks with `decode_error`, so partial inspection applies to this input. Failure source, source-only (not separately executed): `src/domain/apkSignature/apkSignatureReport.ts:361` returns a null match without a note when a signer fingerprint is null. `src/apkSignature/apkV1Signers.ts:129` generates other v1 notes but no general decode-error note. `src/apkSignature/ApkSignatureProvider.ts:498` combines the fixed limitations and those two note sources. The production CLI observations above establish that this path is reachable and that the explanation is absent from its output. ### Typed outcome or recovery expected Keep the unknown comparison value and retained signer error, and make the returned limitations agree with the documented explanation promise. No new field or exact error string is required. ### Analysis or suspected cause Source-derived: `src/domain/apkSignature/apkSignatureReport.ts:354` supplies a note when the lineage origin cannot be determined, while the unknown-identity branch supplies none. The `subjectKeyIdentifier` case already gets a separate explanation from `src/apkSignature/apkV1Signers.ts:129`; this report does not claim every null comparison lacks a limitation. The observed gap is the malformed-v1 case above. ### Additional context Related reports have different acceptance criteria: closed [#6440](https://github.com/morluto/rea/issues/6440) corrects an inappropriate null result caused by an empty v2 scheme, while this input has a populated v2 signer and an appropriate null result without the promised explanation. Open [#7331](https://github.com/morluto/rea/issues/7331) concerns the guide's interpretation of rotation and the separate block-scheme agreement fields. Open [#7316](https://github.com/morluto/rea/issues/7316) concerns discovery prerequisites. None requests this missing limitation. The registry query `npm view rea-agents version` returned 6.8.0, matching the checkout's package version. No earlier known-good version was established.
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#4417
No description provided.