[GH-ISSUE #6722] [Bug] Android application graph omits nested META-INF signing entries #4088

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

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

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

project-android-application-graph omits readable META-INF/keys/CERT.EC and META-INF/keys/CERT.SF entries from normalized_result.components.signing. inspect-artifact retains those paths, while inspect-apk-signature recognizes their v1 pair and resolves its certificate with decode_error: null. The otherwise identical direct-child control includes both entries in the application graph. All CLI commands succeed.

Reproduction

The locked dependencies and compiled runtime were already prepared at the source revision below. From that checkout's root with Node.js on PATH, run the following. Python 3 constructs two small local ZIP archives, using the repository's existing OpenSSL-produced CMS fixture unchanged. HOME and XDG directories are isolated. The script saves each actual CLI response and prints a comparison:

python3 <<'PYTHON'
import base64, json, os, pathlib, re, subprocess, tempfile, zipfile
repo=pathlib.Path.cwd()
work=pathlib.Path(tempfile.mkdtemp(prefix='agent-work.',dir='/tmp'))
source=(repo/'tests/fixtures/apkSignature/v1SignatureBlock.ts').read_text()
block=base64.b64decode(re.search(r'export const OPENSSL_BLOCK_ISSUER_SERIAL = fromBase64\(\s*"([^"]+)"',source)[1])
env=os.environ.copy()
for key,leaf in [('HOME','home'),('USERPROFILE','home'),('XDG_CONFIG_HOME','config'),('XDG_DATA_HOME','data'),('XDG_CACHE_HOME','cache'),('XDG_STATE_HOME','state')]:
 p=work/leaf; p.mkdir(exist_ok=True); env[key]=str(p)
node='node'
def cli(case,operation,*args):
 p=subprocess.run([node,str(repo/'scripts/rea.mjs'),operation,*map(str,args),'--json'],env=env,cwd=work,capture_output=True,text=True)
 (work/f'{case}.{operation}.stdout.json').write_text(p.stdout)
 (work/f'{case}.{operation}.stderr.txt').write_text(p.stderr)
 if p.returncode: raise RuntimeError((p.returncode,p.stderr,p.stdout[:500]))
 return json.loads(p.stdout)
def occurrences(value):
 if isinstance(value,dict):
  if 'logical_path' in value: yield value
  for v in value.values(): yield from occurrences(v)
 elif isinstance(value,list):
  for v in value: yield from occurrences(v)
summary=[]
for label,base in [('direct','META-INF/CERT'),('nested','META-INF/keys/CERT')]:
 apk=work/(label+'.apk')
 with zipfile.ZipFile(apk,'w',compression=zipfile.ZIP_STORED) as z:
  z.writestr('AndroidManifest.xml','<manifest package="example.fixture"/>')
  z.writestr('META-INF/MANIFEST.MF','Manifest-Version: 1.0\r\n\r\n')
  z.writestr(base+'.SF','Signature-Version: 1.0\n')
  z.writestr(base+'.EC',block)
 inspected=cli(label,'inspect-artifact',apk)
 request=work/(label+'.input.json'); request.write_text(json.dumps({'inventory_evidence':[inspected]}))
 graph=cli(label,'project-android-application-graph',request)['normalized_result']
 signature=cli(label,'inspect-apk-signature',apk)['normalized_result']['schemes']['v1']
 summary.append({'case':label,'inventoried':sorted({r['logical_path'] for r in occurrences(inspected) if r['logical_path'].startswith('META-INF/')}),'signing':[r['path'] for r in graph['components']['signing']],'v1_present':signature['present'],'signer_blocks':[{'block_file':s['block_file'],'fingerprint':s['certificate']['sha256_fingerprint'] if s['certificate'] else None,'decode_error':s['decode_error']} for s in signature['signers']],'coverage':graph['coverage'],'unavailable_components':graph['unavailable_components']})
print(json.dumps(summary,indent=2))
print('Evidence directory:',work)
(work/'nested-signing-summary.json').write_text(json.dumps(summary,indent=2)+'\n')
PYTHON

The direct/nested comparison reproduced twice through the compiled CLI, including the runnable script above. No signing tool, device, APK execution, or external provider is needed.

Expected behavior

The signing component list should retain the nested .SF and .EC paths with the artifact IDs and hashes already present in inventory, just as it retains the direct-child pair. This concerns path classification, without asserting signature verification or Android installation acceptance.

The application graph documentation promises exact component paths and hashes from file/slice occurrences. The existing v1 metadata contract explicitly selects signature files under META-INF/ at any depth and gives META-INF/x/CERT.RSA with META-INF/x/CERT.SF as its example. The executed signature-reader result independently confirms that these are recognized signing entries in this input.

Impact and scope

P2: the application graph loses two known signing component identities for this bounded path layout. The original archives and their inspection Evidence remain intact. A demonstrated workaround is to read the paths and identities from inspect-artifact, or obtain signer metadata separately with inspect-apk-signature. Both nested attempts omitted the pair; both direct-child controls retained it. Frequency in real APKs and MCP execution were not tested.

REA version

Source checkout 3c5c887a2cc7371a94fe0fd8b6c478dbf2af60db; manifest version 6.7.0. This tests the built main source, not the published npm release tarball with the same version number.

Runtime and platform

Node.js v24.22.1; Linux x86_64. No external analysis engine was used.

Target and artifact context

Two self-generated ZIP_STORED archives named .apk, each containing AndroidManifest.xml, META-INF/MANIFEST.MF, and a paired .SF/.EC entry. The unchanged CMS block comes from the repository's public OPENSSL_BLOCK_ISSUER_SERIAL fixture. Only the pair's paths change between controls. These are static metadata fixtures; they do not establish an installable Android application or complete valid JAR signing.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Observed reduced projection of the saved CLI responses:

[
  {
    "case": "direct",
    "inventoried": ["META-INF/CERT.EC", "META-INF/CERT.SF", "META-INF/MANIFEST.MF"],
    "signing": ["META-INF/CERT.EC", "META-INF/CERT.SF", "META-INF/MANIFEST.MF"],
    "v1_present": true,
    "signer_blocks": [{"block_file": "META-INF/CERT.EC", "fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88", "decode_error": null}]
  },
  {
    "case": "nested",
    "inventoried": ["META-INF/MANIFEST.MF", "META-INF/keys/CERT.EC", "META-INF/keys/CERT.SF"],
    "signing": ["META-INF/MANIFEST.MF"],
    "v1_present": true,
    "signer_blocks": [{"block_file": "META-INF/keys/CERT.EC", "fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88", "decode_error": null}]
  }
]

Both graphs additionally return coverage: {"status":"complete-within-inventory","inventory_complete":true} and unavailable_components: []. Coverage is context for the omission, not an assertion of signature authenticity. Every executed command exits 0. Local verification: reproduced; evidence provenance: observed; reproduction completeness: complete for this static path-classification workflow.

Typed outcome or recovery expected

Include META-INF/keys/CERT.EC and META-INF/keys/CERT.SF in components.signing, preserving their inventoried identities. Keep the existing limitations about signature semantics and APK Signing Blocks outside ZIP entries.

Analysis or suspected cause

Source-derived: the Android classifier uses ^META-INF/[^/]+\.(?:MF|RSA|DSA|EC|SF)$, excluding every nested entry. In contrast, the v1 selector accepts the META-INF/ prefix and checks the suffix without that one-level restriction. The original inventory retains the entries, and the signature-reader control decodes the same block, distinguishing a classification omission from unreadable content or a semantic-verification exclusion.

Additional context

No source or runtime was changed. Last known good version: NOT_ENOUGH_INFO; no earlier revision was tested. Related #5413 concerns omitted suffixes in the separate Harmony classifier; #1485 concerns non-entry APK Signing Block bytes. This report concerns nested, readable ZIP entries in the Android classifier. Proposed restoration check: the same nested input retains all three META-INF signing paths and their original inventory identities, while the direct-child control remains unchanged.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The compiled public graph command consistently omits two readable signing component paths and identities for the demonstrated nested layout.
  • Workaround status: available
  • Workaround basis: The executed inspect-artifact result retains those exact paths and identities, and inspect-apk-signature independently reports the nested signer.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6722 ### 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 `project-android-application-graph` omits readable `META-INF/keys/CERT.EC` and `META-INF/keys/CERT.SF` entries from `normalized_result.components.signing`. `inspect-artifact` retains those paths, while `inspect-apk-signature` recognizes their v1 pair and resolves its certificate with `decode_error: null`. The otherwise identical direct-child control includes both entries in the application graph. All CLI commands succeed. ### Reproduction The locked dependencies and compiled runtime were already prepared at the source revision below. From that checkout's root with Node.js on PATH, run the following. Python 3 constructs two small local ZIP archives, using the repository's existing OpenSSL-produced CMS fixture unchanged. HOME and XDG directories are isolated. The script saves each actual CLI response and prints a comparison: ```sh python3 <<'PYTHON' import base64, json, os, pathlib, re, subprocess, tempfile, zipfile repo=pathlib.Path.cwd() work=pathlib.Path(tempfile.mkdtemp(prefix='agent-work.',dir='/tmp')) source=(repo/'tests/fixtures/apkSignature/v1SignatureBlock.ts').read_text() block=base64.b64decode(re.search(r'export const OPENSSL_BLOCK_ISSUER_SERIAL = fromBase64\(\s*"([^"]+)"',source)[1]) env=os.environ.copy() for key,leaf in [('HOME','home'),('USERPROFILE','home'),('XDG_CONFIG_HOME','config'),('XDG_DATA_HOME','data'),('XDG_CACHE_HOME','cache'),('XDG_STATE_HOME','state')]: p=work/leaf; p.mkdir(exist_ok=True); env[key]=str(p) node='node' def cli(case,operation,*args): p=subprocess.run([node,str(repo/'scripts/rea.mjs'),operation,*map(str,args),'--json'],env=env,cwd=work,capture_output=True,text=True) (work/f'{case}.{operation}.stdout.json').write_text(p.stdout) (work/f'{case}.{operation}.stderr.txt').write_text(p.stderr) if p.returncode: raise RuntimeError((p.returncode,p.stderr,p.stdout[:500])) return json.loads(p.stdout) def occurrences(value): if isinstance(value,dict): if 'logical_path' in value: yield value for v in value.values(): yield from occurrences(v) elif isinstance(value,list): for v in value: yield from occurrences(v) summary=[] for label,base in [('direct','META-INF/CERT'),('nested','META-INF/keys/CERT')]: apk=work/(label+'.apk') with zipfile.ZipFile(apk,'w',compression=zipfile.ZIP_STORED) as z: z.writestr('AndroidManifest.xml','<manifest package="example.fixture"/>') z.writestr('META-INF/MANIFEST.MF','Manifest-Version: 1.0\r\n\r\n') z.writestr(base+'.SF','Signature-Version: 1.0\n') z.writestr(base+'.EC',block) inspected=cli(label,'inspect-artifact',apk) request=work/(label+'.input.json'); request.write_text(json.dumps({'inventory_evidence':[inspected]})) graph=cli(label,'project-android-application-graph',request)['normalized_result'] signature=cli(label,'inspect-apk-signature',apk)['normalized_result']['schemes']['v1'] summary.append({'case':label,'inventoried':sorted({r['logical_path'] for r in occurrences(inspected) if r['logical_path'].startswith('META-INF/')}),'signing':[r['path'] for r in graph['components']['signing']],'v1_present':signature['present'],'signer_blocks':[{'block_file':s['block_file'],'fingerprint':s['certificate']['sha256_fingerprint'] if s['certificate'] else None,'decode_error':s['decode_error']} for s in signature['signers']],'coverage':graph['coverage'],'unavailable_components':graph['unavailable_components']}) print(json.dumps(summary,indent=2)) print('Evidence directory:',work) (work/'nested-signing-summary.json').write_text(json.dumps(summary,indent=2)+'\n') PYTHON ``` The direct/nested comparison reproduced twice through the compiled CLI, including the runnable script above. No signing tool, device, APK execution, or external provider is needed. ### Expected behavior The signing component list should retain the nested `.SF` and `.EC` paths with the artifact IDs and hashes already present in inventory, just as it retains the direct-child pair. This concerns path classification, without asserting signature verification or Android installation acceptance. The [application graph documentation][graph-doc] promises exact component paths and hashes from file/slice occurrences. The existing [v1 metadata contract][v1-doc] explicitly selects signature files under `META-INF/` at any depth and gives `META-INF/x/CERT.RSA` with `META-INF/x/CERT.SF` as its example. The executed signature-reader result independently confirms that these are recognized signing entries in this input. ### Impact and scope P2: the application graph loses two known signing component identities for this bounded path layout. The original archives and their inspection Evidence remain intact. A demonstrated workaround is to read the paths and identities from `inspect-artifact`, or obtain signer metadata separately with `inspect-apk-signature`. Both nested attempts omitted the pair; both direct-child controls retained it. Frequency in real APKs and MCP execution were not tested. ### REA version Source checkout `3c5c887a2cc7371a94fe0fd8b6c478dbf2af60db`; manifest version 6.7.0. This tests the built main source, not the published npm release tarball with the same version number. ### Runtime and platform Node.js v24.22.1; Linux x86_64. No external analysis engine was used. ### Target and artifact context Two self-generated ZIP_STORED archives named `.apk`, each containing `AndroidManifest.xml`, `META-INF/MANIFEST.MF`, and a paired `.SF`/`.EC` entry. The unchanged CMS block comes from the repository's public `OPENSSL_BLOCK_ISSUER_SERIAL` fixture. Only the pair's paths change between controls. These are static metadata fixtures; they do not establish an installable Android application or complete valid JAR signing. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Observed reduced projection of the saved CLI responses: ```json [ { "case": "direct", "inventoried": ["META-INF/CERT.EC", "META-INF/CERT.SF", "META-INF/MANIFEST.MF"], "signing": ["META-INF/CERT.EC", "META-INF/CERT.SF", "META-INF/MANIFEST.MF"], "v1_present": true, "signer_blocks": [{"block_file": "META-INF/CERT.EC", "fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88", "decode_error": null}] }, { "case": "nested", "inventoried": ["META-INF/MANIFEST.MF", "META-INF/keys/CERT.EC", "META-INF/keys/CERT.SF"], "signing": ["META-INF/MANIFEST.MF"], "v1_present": true, "signer_blocks": [{"block_file": "META-INF/keys/CERT.EC", "fingerprint": "9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88", "decode_error": null}] } ] ``` Both graphs additionally return `coverage: {"status":"complete-within-inventory","inventory_complete":true}` and `unavailable_components: []`. Coverage is context for the omission, not an assertion of signature authenticity. Every executed command exits 0. Local verification: reproduced; evidence provenance: observed; reproduction completeness: complete for this static path-classification workflow. ### Typed outcome or recovery expected Include `META-INF/keys/CERT.EC` and `META-INF/keys/CERT.SF` in `components.signing`, preserving their inventoried identities. Keep the existing limitations about signature semantics and APK Signing Blocks outside ZIP entries. ### Analysis or suspected cause Source-derived: the [Android classifier][classifier] uses `^META-INF/[^/]+\.(?:MF|RSA|DSA|EC|SF)$`, excluding every nested entry. In contrast, the [v1 selector][selector] accepts the `META-INF/` prefix and checks the suffix without that one-level restriction. The original inventory retains the entries, and the signature-reader control decodes the same block, distinguishing a classification omission from unreadable content or a semantic-verification exclusion. ### Additional context No source or runtime was changed. Last known good version: NOT_ENOUGH_INFO; no earlier revision was tested. Related [#5413](https://github.com/morluto/rea/issues/5413) concerns omitted suffixes in the separate Harmony classifier; [#1485](https://github.com/morluto/rea/issues/1485) concerns non-entry APK Signing Block bytes. This report concerns nested, readable ZIP entries in the Android classifier. Proposed restoration check: the same nested input retains all three META-INF signing paths and their original inventory identities, while the direct-child control remains unchanged. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The compiled public graph command consistently omits two readable signing component paths and identities for the demonstrated nested layout. - Workaround status: available - Workaround basis: The executed inspect-artifact result retains those exact paths and identities, and inspect-apk-signature independently reports the nested signer. [graph-doc]: https://github.com/morluto/rea/blob/3c5c887a2cc7371a94fe0fd8b6c478dbf2af60db/docs/android-analysis.md#L249 [v1-doc]: https://github.com/morluto/rea/blob/3c5c887a2cc7371a94fe0fd8b6c478dbf2af60db/docs/apk-signature.md#L85 [classifier]: https://github.com/morluto/rea/blob/3c5c887a2cc7371a94fe0fd8b6c478dbf2af60db/src/domain/android/androidApplication.ts#L176 [selector]: https://github.com/morluto/rea/blob/3c5c887a2cc7371a94fe0fd8b6c478dbf2af60db/src/domain/apkSignature/apkV1Signature.ts#L373
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#4088
No description provided.