[GH-ISSUE #6439] [Bug] inspect-apk-signature loses v1 signer identity when issuer string encoding changes #3855

Closed
opened 2026-10-31 00:44:39 +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/6439

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

node scripts/rea.mjs inspect-apk-signature "$WORK/printable.apk" --format json returns a null v1 certificate and null signer_infos[0].certificate_sha256, although the block carries the certificate named by that SignerInfo's issuer and serial. Its issuer has the same common name, organization, attribute order and serial as the certificate; only the issuer's DirectoryString tags differ: PrintableString in SignerInfo versus UTF8String in the certificate. The carried certificate is listed and decode_error is null.

The unchanged-encoding control resolves the certificate. A different issuer or serial correctly stays unresolved. The encoding-only failure reproduced in all five runs: three on Node v24.22.1 and two on v26.12.1.

Reproduction

From a fresh checkout at 9eb829aca34be36ff112826337996f6139f82eff, install and build with npm ci && npm run build:cached. Then run the following from its root. Python 3 is used only to build reduced local ZIP fixtures. The certificate comes from the baseline's public fixture; each block contains one certificate and one SignerInfo with an opaque signature, because this command reports declared identity without verifying signatures.

export WORK=$(mktemp -d)
mkdir -p "$WORK/home"
python3 - <<'PY'
import base64, os, pathlib, re, zipfile
work = pathlib.Path(os.environ['WORK'])
source = pathlib.Path('tests/fixtures/apkSignature/v1SignatureBlock.ts').read_text()
cert = base64.b64decode(re.search(
    r'export const LEAF_CERTIFICATE = fromBase64\(\s*"([^"]+)"', source)[1])
def der(tag, *parts):
    body = b''.join(parts)
    n = len(body)
    size = n.to_bytes(max(1, (n.bit_length()+7)//8), 'big')
    length = bytes([n]) if n < 128 else bytes([128+len(size)]) + size
    return bytes([tag]) + length + body
def name(tag, cn):
    return der(48,
        der(49, der(48, bytes.fromhex('0603550403'), der(tag, cn))),
        der(49, der(48, bytes.fromhex('060355040a'), der(tag, b'REA'))))
sha = der(48, bytes.fromhex('0609608648016503040201'))
ecdsa = der(48, bytes.fromhex('06082a8648ce3d040302'))
cases = [('control', 12, b'REA Test Root', b'\x12\x34'),
         ('printable', 19, b'REA Test Root', b'\x12\x34'),
         ('other-issuer', 12, b'REA Test Nope', b'\x12\x34'),
         ('other-serial', 12, b'REA Test Root', b'\x12\x35')]
for label, tag, cn, serial in cases:
    sid = der(48, name(tag, cn), der(2, serial))
    info = der(48, der(2, b'\x01'), sid, sha, ecdsa, der(4, b'\x01'))
    signed = der(48, der(2, b'\x01'), der(49, sha),
        der(48, bytes.fromhex('06092a864886f70d010701')),
        der(160, cert), der(49, info))
    block = der(48, bytes.fromhex('06092a864886f70d010702'), der(160, signed))
    with zipfile.ZipFile(work / (label+'.apk'), 'w') as archive:
        archive.writestr('classes.dex', b'dex\n035\0')
        archive.writestr('META-INF/MANIFEST.MF', b'Manifest-Version: 1.0\r\n\r\n')
        archive.writestr('META-INF/CERT.SF', b'Signature-Version: 1.0\r\n\r\n')
        archive.writestr('META-INF/CERT.EC', block)
PY
for case in control printable other-issuer other-serial; do
  HOME="$WORK/home" node scripts/rea.mjs inspect-apk-signature "$WORK/$case.apk" --format json > "$WORK/$case.json"
done
python3 - <<'PY'
import json, os, pathlib
work = pathlib.Path(os.environ['WORK'])
for case in ['control', 'printable', 'other-issuer', 'other-serial']:
    signer = json.loads((work/(case+'.json')).read_text())['normalized_result']['schemes']['v1']['signers'][0]
    print(case, signer['signer_infos'][0]['certificate_sha256'], signer['decode_error'])
PY

Expected behavior

Both control and printable should identify the carried certificate with fingerprint 9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88. other-issuer and other-serial should remain unknown.

The documented v1 contract identifies the carried certificate by issuerAndSerialNumber. RFC 5280 section 7.1 compares PrintableString and UTF8String name attributes by their prepared values. These fixtures preserve those values, OIDs, RDN order and serial. The declared-identity scope explicitly includes archives whose signatures are not cryptographically valid.

Impact and scope

P2: this issuer representation makes the declared v1 signer unavailable for fingerprinting or clustering through the signer fields. The certificate inventory survives; in this reduced single-certificate case its fingerprint remains readable there. Real-world frequency and a general workaround are NOT_ENOUGH_INFO: no production APK corpus or external signing-tool workaround was tested. No authenticity or trust boundary is involved.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node v24.22.1 and v26.12.1; macOS 27.0.1; arm64. The exact pinned Node 24.18.0 was not tested. No Android SDK, device or analysis engine was used.

Target and artifact context

Synthetic local ZIP containers named .apk, with classes.dex, MANIFEST.MF, a matching .SF/.EC pair, and a PKCS#7 SignedData carrying the public test leaf certificate. These fixtures isolate declared certificate selection; they are not evidence of installability or valid JAR signatures.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed projection of the four CLI results:

control 9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88 None
printable None None
other-issuer None None
other-serial None None

The columns are fixture, signer_infos[0].certificate_sha256, and decode_error (None is Python's rendering of JSON null). In every result, certificates contains the unchanged leaf fingerprint. For printable, the top-level per-v1-signer certificate is also null. Every CLI call exits 0.

Typed outcome or recovery expected

For the equivalent issuer name, return the carried certificate facts in certificate and its fingerprint in signer_infos[0].certificate_sha256, with decode_error: null. Preserve unknown identity for genuinely different issuers or serials.

Analysis or suspected cause

Source-derived explanation: certificateIdentity retains the issuer's raw encoding, and signerInfo compares it byte-for-byte. Equivalent name values with different ASN.1 string tags therefore do not match. The adapter projects the resulting null index into both missing signer fields while retaining the certificate inventory.

Additional context

Open and closed upstream searches found no matching defect. The related signature-inspection feature request #4537 does not describe this encoding case. No last-known-good revision, v2 comparison, or apksigner verification result was established.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6439 ### 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 `node scripts/rea.mjs inspect-apk-signature "$WORK/printable.apk" --format json` returns a null v1 `certificate` and null `signer_infos[0].certificate_sha256`, although the block carries the certificate named by that SignerInfo's issuer and serial. Its issuer has the same common name, organization, attribute order and serial as the certificate; only the issuer's DirectoryString tags differ: PrintableString in SignerInfo versus UTF8String in the certificate. The carried certificate is listed and `decode_error` is null. The unchanged-encoding control resolves the certificate. A different issuer or serial correctly stays unresolved. The encoding-only failure reproduced in all five runs: three on Node v24.22.1 and two on v26.12.1. ### Reproduction From a fresh checkout at `9eb829aca34be36ff112826337996f6139f82eff`, install and build with `npm ci && npm run build:cached`. Then run the following from its root. Python 3 is used only to build reduced local ZIP fixtures. The certificate comes from the baseline's public fixture; each block contains one certificate and one SignerInfo with an opaque signature, because this command reports declared identity without verifying signatures. ```sh export WORK=$(mktemp -d) mkdir -p "$WORK/home" python3 - <<'PY' import base64, os, pathlib, re, zipfile work = pathlib.Path(os.environ['WORK']) source = pathlib.Path('tests/fixtures/apkSignature/v1SignatureBlock.ts').read_text() cert = base64.b64decode(re.search( r'export const LEAF_CERTIFICATE = fromBase64\(\s*"([^"]+)"', source)[1]) def der(tag, *parts): body = b''.join(parts) n = len(body) size = n.to_bytes(max(1, (n.bit_length()+7)//8), 'big') length = bytes([n]) if n < 128 else bytes([128+len(size)]) + size return bytes([tag]) + length + body def name(tag, cn): return der(48, der(49, der(48, bytes.fromhex('0603550403'), der(tag, cn))), der(49, der(48, bytes.fromhex('060355040a'), der(tag, b'REA')))) sha = der(48, bytes.fromhex('0609608648016503040201')) ecdsa = der(48, bytes.fromhex('06082a8648ce3d040302')) cases = [('control', 12, b'REA Test Root', b'\x12\x34'), ('printable', 19, b'REA Test Root', b'\x12\x34'), ('other-issuer', 12, b'REA Test Nope', b'\x12\x34'), ('other-serial', 12, b'REA Test Root', b'\x12\x35')] for label, tag, cn, serial in cases: sid = der(48, name(tag, cn), der(2, serial)) info = der(48, der(2, b'\x01'), sid, sha, ecdsa, der(4, b'\x01')) signed = der(48, der(2, b'\x01'), der(49, sha), der(48, bytes.fromhex('06092a864886f70d010701')), der(160, cert), der(49, info)) block = der(48, bytes.fromhex('06092a864886f70d010702'), der(160, signed)) with zipfile.ZipFile(work / (label+'.apk'), 'w') as archive: archive.writestr('classes.dex', b'dex\n035\0') archive.writestr('META-INF/MANIFEST.MF', b'Manifest-Version: 1.0\r\n\r\n') archive.writestr('META-INF/CERT.SF', b'Signature-Version: 1.0\r\n\r\n') archive.writestr('META-INF/CERT.EC', block) PY for case in control printable other-issuer other-serial; do HOME="$WORK/home" node scripts/rea.mjs inspect-apk-signature "$WORK/$case.apk" --format json > "$WORK/$case.json" done python3 - <<'PY' import json, os, pathlib work = pathlib.Path(os.environ['WORK']) for case in ['control', 'printable', 'other-issuer', 'other-serial']: signer = json.loads((work/(case+'.json')).read_text())['normalized_result']['schemes']['v1']['signers'][0] print(case, signer['signer_infos'][0]['certificate_sha256'], signer['decode_error']) PY ``` ### Expected behavior Both `control` and `printable` should identify the carried certificate with fingerprint `9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88`. `other-issuer` and `other-serial` should remain unknown. The [documented v1 contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/apk-signature.md#L91) identifies the carried certificate by issuerAndSerialNumber. [RFC 5280 section 7.1](https://www.rfc-editor.org/rfc/rfc5280#section-7.1) compares PrintableString and UTF8String name attributes by their prepared values. These fixtures preserve those values, OIDs, RDN order and serial. The [declared-identity scope](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/apk-signature.md#L195) explicitly includes archives whose signatures are not cryptographically valid. ### Impact and scope P2: this issuer representation makes the declared v1 signer unavailable for fingerprinting or clustering through the signer fields. The certificate inventory survives; in this reduced single-certificate case its fingerprint remains readable there. Real-world frequency and a general workaround are NOT_ENOUGH_INFO: no production APK corpus or external signing-tool workaround was tested. No authenticity or trust boundary is involved. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node v24.22.1 and v26.12.1; macOS 27.0.1; arm64. The exact pinned Node 24.18.0 was not tested. No Android SDK, device or analysis engine was used. ### Target and artifact context Synthetic local ZIP containers named `.apk`, with classes.dex, MANIFEST.MF, a matching .SF/.EC pair, and a PKCS#7 SignedData carrying the public test leaf certificate. These fixtures isolate declared certificate selection; they are not evidence of installability or valid JAR signatures. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed projection of the four CLI results: ```text control 9224e30fd7981ebeb903d2593075845316d5c4f546c2f3e4675c03ef3302ef88 None printable None None other-issuer None None other-serial None None ``` The columns are fixture, `signer_infos[0].certificate_sha256`, and `decode_error` (`None` is Python's rendering of JSON null). In every result, `certificates` contains the unchanged leaf fingerprint. For `printable`, the top-level per-v1-signer `certificate` is also null. Every CLI call exits 0. ### Typed outcome or recovery expected For the equivalent issuer name, return the carried certificate facts in `certificate` and its fingerprint in `signer_infos[0].certificate_sha256`, with `decode_error: null`. Preserve unknown identity for genuinely different issuers or serials. ### Analysis or suspected cause Source-derived explanation: [certificateIdentity retains the issuer's raw encoding](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/apkSignature/apkV1SignatureBlock.ts#L270), and [signerInfo compares it byte-for-byte](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/apkSignature/apkV1SignatureBlock.ts#L294). Equivalent name values with different ASN.1 string tags therefore do not match. [The adapter projects the resulting null index](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/apkSignature/apkV1Signers.ts#L180) into both missing signer fields while retaining the certificate inventory. ### Additional context Open and closed upstream searches found no matching defect. The related [signature-inspection feature request #4537](https://github.com/morluto/rea/issues/4537) does not describe this encoding case. No last-known-good revision, v2 comparison, or apksigner verification result 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#3855
No description provided.