El APK Signing Block
1. Qué es y por qué existe
La firma v2 tenía que cubrir todos los bytes del APK sin dejar de ser un ZIP que cualquier
lector abre. Esas dos exigencias parecen incompatibles: si el digest va dentro de una entrada
del ZIP, el Central Directory que la describe queda fuera de lo firmado; si va fuera del
ZIP, ningún lector lo encuentra.
El APK Signing Block —glosario— es la salida a ese dilema, y
aprovecha una propiedad del formato: el EOCD apunta al Central Directory
por offset absoluto, así que todo lo que haya entre el último dato y ese offset es un hueco
que ningún lector de ZIP mira. Ahí se inserta el APK Signing Block: un contenedor propio
de Android, autodescriptivo, extensible y completamente invisible para unzip.
El resultado es una pieza de infraestructura que va mucho más allá de la firma. Como el
formato es una secuencia de pares ID-valor y todo lector debe saltarse los identificadores
que no conoce, el bloque se ha convertido en el sitio donde el ecosistema deja metadatos:
las firmas v2, v3 y v3.1, el source stamp, el árbol de dependencias que lee Google Play, el
sello de distribución de Play y el relleno de alineación conviven en el mismo bloque sin
estorbarse. La sección 10 mide en el corpus cuánto de eso ocurre de verdad.
Este documento describe el contenedor y el algoritmo de digest. Qué garantiza cada esquema está en Esquemas de firma v1, v2 y v3; el orden de verificación, en Firma v4 y verificación.
Endianness: todas las estructuras de este documento son little-endian, igual que las del
ZIPque las rodea.
2. Dónde vive
Entre el final de los datos de la última entrada y el inicio del Central Directory, tal y
como describe la sección 2 de Contenedor ZIP. No
se repite aquí el layout del ZIP; lo que importa es cómo se localiza el bloque, y siempre
es de atrás hacia delante:
1. buscar el EOCD (firma 0x06054b50) desde el final del fichero
2. leer su campo de offset 0x10 → offset absoluto del Central Directory (cd_off)
3. mirar los 16 bytes en [cd_off - 16, cd_off)
¿son "APK Sig Block 42"? no → el APK no tiene APK Signing Block
sí → seguir
4. leer el uint64 en cd_off - 24 → size_al_final
5. inicio_del_bloque = cd_off - 8 - size_al_final
6. leer el uint64 en inicio_del_bloque → size_al_principio
7. exigir size_al_principio == size_al_final
El paso 3 es el que da nombre al campo mágico. La especificación es explícita sobre para qué
sirve: «The magic value provides a quick way to establish that what precedes Central
Directory is likely the APK signing block». Likely, no certainly: los pasos 4 a 7 son
los que confirman.
El paso 7 no es decorativo. Los dos campos de tamaño existen para poder recorrer el bloque en
los dos sentidos, y que discrepen es la señal de que el fichero está manipulado o de que
el EOCD elegido no es el bueno.
3. Layout byte a byte
◄── crece hacia arriba
┌──────────────────────────────────────────────────────────┐ ← inicio_del_bloque
│ size of block uint64 (excluye este campo) │
├──────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────┐ │
│ │ size of pair uint64 (excluye este campo) │ │
│ │ ID uint32 │ │ par 1
│ │ value (size of pair − 4) bytes │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ size / ID / value │ │ par 2
│ └──────────────────────────────────────────────────────┘ │
│ … │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ par de relleno ID = 0x42726577 (opcional) │ │
│ └──────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────┤
│ size of block uint64 (idéntico al de arriba) │
├──────────────────────────────────────────────────────────┤
│ magic "APK Sig Block 42" 16 bytes │
└──────────────────────────────────────────────────────────┘ ← cd_off
│ ZIP Central Directory │
3.1 Campos del bloque
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
size of block |
0x00 | 8 | uint64 |
Bytes desde el final de este campo hasta el final de la magic. Equivale a tamaño_total − 8 |
pairs |
0x08 | var | — | Secuencia de pares ID-valor, sin separador |
size of block |
— | 8 | uint64 |
Repetición exacta del campo de 0x00 |
magic |
— | 16 | char[16] |
41 50 4B 20 53 69 67 20 42 6C 6F 63 6B 20 34 32 = APK Sig Block 42 |
El tamaño total del bloque en el fichero es size of block + 8.
3.2 Campos de un par ID-valor
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
size of pair |
0x00 | 8 | uint64 |
Bytes desde el final de este campo hasta el final del valor. Incluye los 4 del ID |
ID |
0x08 | 4 | uint32 |
Identificador del bloque. Tabla en la sección 4 |
value |
0x0C | var | — | size of pair − 4 bytes de contenido opaco |
Los pares se encadenan sin relleno entre ellos: el siguiente empieza en
offset_actual + 8 + size_of_pair. El recorrido termina cuando se alcanza el segundo campo de
tamaño, es decir, cd_off − 24.
Un valor de size of pair menor que 4 es un fichero malformado, y uno que haga que el par
se salga de cd_off − 24 también. Son las dos comprobaciones que un lector tiene que hacer en
cada iteración para no entrar en un bucle infinito ni leer fuera de rango con un APK
manipulado.
El orden de los pares no está fijado. apksig escribe en el orden en que se los pasan y
ApkVerifier los busca por ID, no por posición. En el corpus, apksigner produce
v2 → v3.1 → v3 → source stamp → relleno, pero eso es una consecuencia del código, no una
garantía del formato.
4. Identificadores de bloque conocidos
Todos los valores de esta tabla se han verificado contra el código fuente indicado; ninguno viene de memoria.
| ID | Nombre en apksig |
Fichero de origen | Contenido |
|---|---|---|---|
0x7109871a |
APK_SIGNATURE_SCHEME_V2_BLOCK_ID |
v2/V2SchemeConstants.java |
Firma v2 |
0xf05368c0 |
APK_SIGNATURE_SCHEME_V3_BLOCK_ID |
v3/V3SchemeConstants.java |
Firma v3 |
0x1b93ad61 |
APK_SIGNATURE_SCHEME_V31_BLOCK_ID |
v3/V3SchemeConstants.java |
Firma v3.1 |
0x70e1c89f |
APK_SIGNATURE_SCHEME_V32_BLOCK_ID |
solo en el apksigner.jar local |
Firma v3.2 ⚠️ |
0x2b09189e |
V1_SOURCE_STAMP_BLOCK_ID |
stamp/SourceStampConstants.java |
Source stamp v1 (obsoleto) |
0x6dff800d |
V2_SOURCE_STAMP_BLOCK_ID |
stamp/SourceStampConstants.java |
Source stamp v2 |
0x42726577 |
VERITY_PADDING_BLOCK_ID |
ApkSigningBlockUtils.java |
Relleno de alineación (sección 8) |
0x504b4453 |
— | No está en apksig |
Metadatos de dependencias de Google Play |
0x2146444e |
— | No está en apksig |
«Frosting» de Google Play (sección 11) |
0x71777777 |
— | No está en apksig |
Metadatos de Meituan (canal de distribución chino) |
v4 no tiene identificador de bloque. Vive en un fichero .idsig aparte, y por eso
Firma v4 y verificación lo trata por separado. Tampoco lo
tiene v1: sus artefactos son entradas del ZIP bajo META-INF/.
⚠️ sin verificar: 0x70e1c89f sale de javap -constants sobre
build-tools/37.0.0/lib/apksigner.jar (apksigner 0.9), junto a
MIN_SDK_WITH_V32_SUPPORT = 37. No está en la rama main pública de
platform/tools/apksig ni tiene página en source.android.com, y no aparece en ningún
APK del corpus.
⚠️ sin verificar: 0x504b4453, 0x2146444e y 0x71777777 no están en apksig porque no son
de Android sino de Google Play y de terceros. Los tres valores se han cotejado con el
enumerado BlockId de avast/apkverifier, y los dos primeros se han observado en el
corpus (sección 10). El de Meituan no aparece en ninguna muestra.
4.1 Los atributos internos, que también son identificadores
No son bloques del APK Signing Block sino additional attributes dentro de un bloque de
firma, pero se confunden constantemente con los anteriores porque son uint32 del mismo
aspecto. Todos verificados en el código:
| ID | Nombre | Dónde aparece | Contenido |
|---|---|---|---|
0xbeeff00d |
STRIPPING_PROTECTION_ATTR_ID |
Bloque v2 | Número del esquema superior presente |
0x3ba06f8c |
PROOF_OF_ROTATION_ATTR_ID |
Bloque v3 / v3.1 | El lineage de rotación |
0x559f8b02 |
ROTATION_MIN_SDK_VERSION_ATTR_ID |
Bloque v3 | minSdkVersion que debe declarar el v3.1 |
0xc2a6b3ba |
ROTATION_ON_DEV_RELEASE_ATTR_ID |
Bloque v3.1 | La rotación apunta a una release de desarrollo |
0x9d6303f7 |
PROOF_OF_ROTATION_ATTR_ID (stamp) |
Source stamp | Lineage del certificado del stamp |
0xe43c5946 |
STAMP_TIME_ATTR_ID |
Source stamp | Época en segundos, uint64 little-endian |
0xbf940529 |
HYBRID_MIN_SDK_VERSION_ATTR_ID |
⚠️ solo en el jar local | Sin documentar |
5. Estructura interna de un bloque v2 o v3
El valor de los pares 0x7109871a, 0xf05368c0 y 0x1b93ad61 tiene la misma forma general.
Todo son secuencias con prefijo de longitud uint32: primero un uint32 con los bytes
que siguen, luego esos bytes. Anidado tres niveles.
valor del par
└── uint32 len ─ secuencia de signers
└── uint32 len ─ signer
├── uint32 len ─ signed data
│ ├── uint32 len ─ secuencia de digests
│ │ └── uint32 len ─ digest
│ │ ├── uint32 signature algorithm ID
│ │ └── uint32 len ─ bytes del digest
│ ├── uint32 len ─ secuencia de certificados
│ │ └── uint32 len ─ certificado X.509 en DER
│ ├── uint32 minSdkVersion ← solo v3 y v3.1
│ ├── uint32 maxSdkVersion ← solo v3 y v3.1
│ └── uint32 len ─ secuencia de additional attributes
│ └── uint32 len ─ attribute
│ ├── uint32 ID ← tabla de la sección 4.1
│ └── valor
├── uint32 minSdkVersion ← solo v3 y v3.1, copia SIN firmar
├── uint32 maxSdkVersion ← solo v3 y v3.1, copia SIN firmar
├── uint32 len ─ secuencia de signatures
│ └── uint32 len ─ signature
│ ├── uint32 signature algorithm ID
│ └── uint32 len ─ bytes de la firma
└── uint32 len ─ public key (SubjectPublicKeyInfo en DER)
Cuatro reglas que gobiernan la verificación de esta estructura:
- La firma se calcula sobre los bytes del
signed datacompleto, tal cual aparecen en el fichero. No sobre una reserialización: hay que quedarse con el rango de bytes original. - La lista de algoritmos de
digestsy la designaturesdebe coincidir. La spec lo exige explícitamente; si no coinciden, elAPKse rechaza. - El primer certificado de la secuencia es el del firmante, y su clave pública debe ser
idéntica al campo
public keydelsigner. Comprobar solo uno de los dos deja pasar unAPKcon la clave cambiada. - Las copias sin firmar de
minSdkVersionymaxSdkVersionsolo sirven para descartar rápido un firmante fuera de rango. La decisión real usa las de dentro delsigned data.
Volcado real del bloque v3.1 del base.apk de Google Keep, con el lineage desplegado:
bloque v3.1: valor=5589 B, secuencia de signers=5585 B
signer: minSdkVersion=33 maxSdkVersion=2147483647
signed data: digests=76 B certs=1488 B attrs=2903 B
digest alg=0x0104 RSA_PKCS1_V1_5_WITH_SHA512 64 B fd8bb3891bfa93c480ed6c49…
certificate X.509 DER: 1484 B
minSdkVersion=33 maxSdkVersion=2147483647 (copia firmada)
attribute 0x3ba06f8c PROOF_OF_ROTATION valor=2895 B
lineage version=1
nodo #1: cert=1095 B sigAlgId=0x0103 RSA_PKCS1_V1_5_WITH_SHA256 sig=0 B
flags=0x00000017 → INSTALLED_DATA|SHARED_USER_ID|PERMISSION|AUTH
nodo #2: cert=1484 B sigAlgId=0x0000 ? sig=256 B
flags=0x00000017 → INSTALLED_DATA|SHARED_USER_ID|PERMISSION|AUTH
signature alg=0x0104 RSA_PKCS1_V1_5_WITH_SHA512 512 B
public key SubjectPublicKeyInfo DER: 550 B
6. El algoritmo de digest por chunks
Es el detalle que hay que implementar exactamente, y el que más se equivoca. La descripción
oficial y el código de ApkSigningBlockUtils.java coinciden byte a byte.
6.1 Qué se trocea
Los tres tramos cubiertos, en este orden y concatenados como si fuesen un solo flujo:
tramo 1: bytes [0, inicio_del_APK_Signing_Block) contenido de las entradas ZIP
tramo 3: el Central Directory completo
tramo 4: el EOCD, con su campo de offset del CD sustituido por el tamaño del tramo 1
El tramo 2 —el propio APK Signing Block— no entra. Y la sustitución del tramo 4 es la
excepción necesaria que explica la sección 4.1 de
Esquemas de firma v1, v2 y v3.
Un detalle que cuesta caro descubrir tarde: los tres tramos no se concatenan antes de
trocear. Cada uno se trocea por separado y el último trozo de cada tramo puede ser más corto
de 1 MiB. El comentario de ChunkSupplier en apksig lo dice: «When bounds are met in a
supplied DataSource, the data from the next DataSource are NOT concatenated». Un
implementador que junte los tres tramos en un buffer y luego trocee obtiene chunks distintos y
digests distintos para el mismo fichero.
6.2 El troceado
- Tamaño de chunk: 1 MiB = 1.048.576 bytes. En el código,
CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024. - Digest de cada chunk: se digiere
0xa5‖longitud del chunk(uint32 LE) ‖datos. - Digest final: se digiere
0x5a‖número total de chunks(uint32 LE) ‖concatenación de todos los digests de chunk, en orden.
chunk i ──► H( 0xa5 ‖ len_i(u32 LE) ‖ datos_i ) ──► d_i
digest final = H( 0x5a ‖ n(u32 LE) ‖ d_0 ‖ d_1 ‖ … ‖ d_{n-1} )
El código construye el buffer del segundo nivel exactamente así, y el comentario no deja lugar a interpretación:
concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize];
// Fill the initial values of the concatenated digests of chunks, which is
// {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}.
concatOfDigestsOfChunks[0] = 0x5a;
Los dos prefijos existen por lo mismo: separar el dominio. Sin ellos, el digest de un
chunk y el digest final serían hashes del mismo espacio de entradas, y un atacante podría
construir un fichero cuyo contenido fuese, a la vez, una lista válida de digests. Con 0xa5
en un nivel y 0x5a en el otro, las dos entradas nunca colisionan. La longitud incluida en el
prefijo hace lo propio con el último chunk, que es más corto.
6.3 Los algoritmos de digest de contenido
ContentDigestAlgorithm.java define cuatro:
| Nombre | ID | Hash | Chunk | Uso |
|---|---|---|---|---|
CHUNKED_SHA256 |
1 | SHA-256 | 1 MiB | v2, v3, v3.1 |
CHUNKED_SHA512 |
2 | SHA-512 | 1 MiB | v2, v3, v3.1 con clave grande |
VERITY_CHUNKED_SHA256 |
3 | SHA-256 | 4 KiB | APK verity: árbol de Merkle, no lista plana |
SHA256 |
4 | SHA-256 | — | Sin trocear |
VERITY_CHUNKED_SHA256 no usa el algoritmo de esta sección: construye un árbol de Merkle
de bloques de 4 KiB, el mismo que la firma v4, y su digest final es la raíz del árbol más el
tamaño del contenido. Está descrito en
Firma v4 y verificación, sección 3.
Su presencia impone dos condiciones extra, que verifyIntegrity comprueba lanzando una
excepción:
if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) {
throw new RuntimeException("APK Signing Block is not aligned on 4k boundary: " + …);
}
…
if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
throw new RuntimeException("APK Signing Block size is not multiple of page size: " + …);
}
Es decir: si hay digest de verity, el bloque tiene que empezar en múltiplo de 4096 y medir un múltiplo de 4096. La sección 10.2 comprueba esa correlación sobre el corpus, y se cumple sin excepciones.
7. Algoritmos de firma
Sacados de SignatureAlgorithm.java. La columna «Digest de contenido» dice qué algoritmo de
la sección 6.3 acompaña obligatoriamente a cada firma.
| ID | Nombre en apksig |
Firma | Digest de contenido | minSdkVersion |
|---|---|---|---|---|
0x0101 |
RSA_PSS_WITH_SHA256 |
RSASSA-PSS, SHA-256, MGF1 SHA-256, sal de 32 B | CHUNKED_SHA256 |
24 |
0x0102 |
RSA_PSS_WITH_SHA512 |
RSASSA-PSS, SHA-512, MGF1 SHA-512, sal de 64 B | CHUNKED_SHA512 |
24 |
0x0103 |
RSA_PKCS1_V1_5_WITH_SHA256 |
RSASSA-PKCS1-v1_5, SHA-256 | CHUNKED_SHA256 |
24 |
0x0104 |
RSA_PKCS1_V1_5_WITH_SHA512 |
RSASSA-PKCS1-v1_5, SHA-512 | CHUNKED_SHA512 |
24 |
0x0201 |
ECDSA_WITH_SHA256 |
ECDSA, SHA-256 | CHUNKED_SHA256 |
24 |
0x0202 |
ECDSA_WITH_SHA512 |
ECDSA, SHA-512 | CHUNKED_SHA512 |
24 |
0x0301 |
DSA_WITH_SHA256 |
DSA, SHA-256 | CHUNKED_SHA256 |
24 |
0x0301 |
DETDSA_WITH_SHA256 |
DSA determinista, RFC 6979 | CHUNKED_SHA256 |
24 |
0x0421 |
VERITY_RSA_PKCS1_V1_5_WITH_SHA256 |
RSASSA-PKCS1-v1_5, SHA-256 | VERITY_CHUNKED_SHA256 |
28 |
0x0423 |
VERITY_ECDSA_WITH_SHA256 |
ECDSA, SHA-256 | VERITY_CHUNKED_SHA256 |
28 |
0x0425 |
VERITY_DSA_WITH_SHA256 |
DSA, SHA-256 | VERITY_CHUNKED_SHA256 |
28 |
0x0301 está duplicado a propósito: DSA_WITH_SHA256 y DETDSA_WITH_SHA256 comparten
identificador porque en el fichero son indistinguibles; la diferencia es solo cómo se generó
el nonce al firmar. Un parser que use un mapa ID → nombre tiene que decidir cuál gana, y da
igual cuál: la verificación es la misma.
Los rangos de clave que admite la especificación: RSA de 1024 a 16384 bits, EC sobre las curvas NIST P-256, P-384 y P-521, DSA de 1024 a 3072 bits.
En los 58 APK standalone del corpus solo aparecen tres de los once: 0x0103 en 98
firmantes, 0x0421 en 12 y 0x0104 en 2. Ninguno usa PSS ni curva elíptica.
8. El bloque de relleno
El par 0x42726577 —que en ASCII se lee Brew, leyendo el uint32 como big-endian— no lleva
información: es relleno para que el tamaño total del bloque sea múltiplo de 4096.
generateApkSigningBlock lo calcula así:
int resultSize = 8 /*size*/ + blocksSize + 8 /*size*/ + 16 /*magic*/;
if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) {
int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES -
(resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES);
if (padding < 12) { // minimum size of an ID-value pair
padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES;
}
…
}
Dos consecuencias que un implementador debe reproducir:
- El relleno cuenta como un par completo, con sus 8 bytes de longitud y sus 4 de ID. Por eso, si faltan menos de 12 bytes para llegar al múltiplo, se añade una página entera: no cabe un par válido en menos.
- Se alinea el tamaño, no la posición. Que el bloque empiece en un múltiplo de 4096 es una
condición distinta, y solo obligatoria cuando hay digest de verity (sección 6.3). Se
consigue rellenando la última entrada del
ZIP, no el bloque.
Que el tamaño del bloque sea múltiplo de 4096 tiene un efecto lateral visible: el offset del
Central Directory hereda la alineación del final de los datos. En com.termux_1002.apk
ese offset vale 0x06C8F000 = 113.831.936, múltiplo exacto de 4096, y el EOCD de la sección
6.3 de Contenedor ZIP lo enseña.
9. El volcador
Cabe en 60 líneas y es la herramienta que hace falta para todo lo demás. Recorre el bloque y enumera los pares.
#!/usr/bin/env python3
"""Vuelca los pares ID-value del APK Signing Block de un APK."""
import struct, sys
MAGIC = b"APK Sig Block 42"
NAMES = {
0x7109871A: "v2 (APK_SIGNATURE_SCHEME_V2_BLOCK_ID)",
0xF05368C0: "v3 (APK_SIGNATURE_SCHEME_V3_BLOCK_ID)",
0x1B93AD61: "v3.1 (APK_SIGNATURE_SCHEME_V31_BLOCK_ID)",
0x2B09189E: "source stamp v1 (V1_SOURCE_STAMP_BLOCK_ID)",
0x6DFF800D: "source stamp v2 (V2_SOURCE_STAMP_BLOCK_ID)",
0x42726577: "padding (VERITY_PADDING_BLOCK_ID)",
0x504B4453: "dependency info (Google Play)",
0x2146444E: "frosting (Google Play)",
}
def parse(path):
with open(path, "rb") as f:
buf = f.read()
# EOCD: se busca hacia atras en la ventana maxima de comentario
idx = buf.rfind(b"PK\x05\x06", max(0, len(buf) - (22 + 65535)))
if idx < 0:
raise SystemExit("EOCD no encontrado")
cd_off = struct.unpack_from("<I", buf, idx + 16)[0]
if buf[cd_off - 16:cd_off] != MAGIC:
return None, cd_off
size_at_end = struct.unpack_from("<Q", buf, cd_off - 24)[0]
blk_start = cd_off - 8 - size_at_end
size_at_start = struct.unpack_from("<Q", buf, blk_start)[0]
pairs, p, end = [], blk_start + 8, cd_off - 24
while p < end:
(plen,) = struct.unpack_from("<Q", buf, p)
if plen < 4 or p + 8 + plen > cd_off - 16: # par malformado
pairs.append(("MALFORMADO", plen, p))
break
(pid,) = struct.unpack_from("<I", buf, p + 8)
pairs.append((pid, plen - 4, buf[p + 12:p + 8 + plen]))
p += 8 + plen
return (blk_start, size_at_start, size_at_end, pairs), cd_off
for path in sys.argv[1:]:
res, cd_off = parse(path)
name = path.rsplit("/", 1)[-1]
if res is None:
print(f"{name}: SIN APK Signing Block (CD en {cd_off})")
continue
blk_start, s1, s2, pairs = res
print(f"== {name}")
print(f" inicio del bloque : {blk_start} (multiplo de 4096: {blk_start % 4096 == 0})")
print(f" size (cabeza/cola): {s1} / {s2} coinciden: {s1 == s2}")
print(f" offset del CD : {cd_off} total del bloque: {cd_off - blk_start}")
for pid, plen, val in pairs:
print(f" 0x{pid:08x} {plen:>9} bytes {NAMES.get(pid, 'DESCONOCIDO')}")
print()
Salida real sobre tres APK del corpus, ejecutada el 12 de agosto de 2026:
CORPUS=~/corpus-apk
python3 dump_sigblock.py $CORPUS/com.termux_1002.apk \
$CORPUS/com.x8bit.bitwarden_2026.5.0.apk \
/tmp/scratch/keep_x/com.google.android.keep.apk
== com.termux_1002.apk
inicio del bloque : 113827840 (multiplo de 4096: True)
size (cabeza/cola): 4088 / 4088 coinciden: True
offset del CD : 113831936 total del bloque: 4096
0x7109871a 1525 bytes v2 (APK_SIGNATURE_SCHEME_V2_BLOCK_ID)
0xf05368c0 1525 bytes v3 (APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
0x42726577 978 bytes padding (VERITY_PADDING_BLOCK_ID)
== com.x8bit.bitwarden_2026.5.0.apk
inicio del bloque : 78765228 (multiplo de 4096: False)
size (cabeza/cola): 16376 / 16376 coinciden: True
offset del CD : 78781612 total del bloque: 16384
0x7109871a 1427 bytes v2 (APK_SIGNATURE_SCHEME_V2_BLOCK_ID)
0x504b4453 11719 bytes dependency info (Google Play)
0x42726577 3170 bytes padding (VERITY_PADDING_BLOCK_ID)
== com.google.android.keep.apk
inicio del bloque : 16523264 (multiplo de 4096: True)
size (cabeza/cola): 16376 / 16376 coinciden: True
offset del CD : 16539648 total del bloque: 16384
0x7109871a 1751 bytes v2 (APK_SIGNATURE_SCHEME_V2_BLOCK_ID)
0x1b93ad61 5589 bytes v3.1 (APK_SIGNATURE_SCHEME_V31_BLOCK_ID)
0xf05368c0 1763 bytes v3 (APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
0x6dff800d 3057 bytes source stamp v2 (V2_SOURCE_STAMP_BLOCK_ID)
0x2146444e 3930 bytes DESCONOCIDO
0x42726577 190 bytes padding (VERITY_PADDING_BLOCK_ID)
Los tres bloques miden 4096 o 16384. Bitwarden empieza en offset no alineado y no pasa nada:
no tiene digest de verity, así que la condición de la sección 6.3 no aplica. Y el
0x2146444e de Keep es el bloque que la sección 11 identifica.
10. Censo real del corpus
Ejecutado sobre 717 APK: los 58 standalone más los 659 internos de los 28 contenedores,
leídos directamente del ZIP con zipfile sin extraerlos a disco.
10.1 Frecuencia por identificador
APK analizados: 717
Sin APK Signing Block (solo v1 o sin firmar): 0
Frecuencia por identificador de bloque:
0x7109871a v2 717 100.0 %
0x42726577 padding 715 99.7 %
0xf05368c0 v3 701 97.8 %
0x6dff800d source stamp v2 617 86.1 %
0x1b93ad61 v3.1 219 30.5 %
0x2146444e frosting (Play) 25 3.5 %
0x504b4453 dependency info 2 0.3 %
Combinaciones observadas:
381 v2 + v3 + source stamp v2 + padding
211 v2 + v3.1 + v3 + source stamp v2 + padding
84 v2 + v3 + padding
17 v2 + v3 + source stamp v2 + frosting (Play) + padding
12 v2 + padding
8 v2 + v3.1 + v3 + source stamp v2 + frosting (Play) + padding
2 v2 + dependency info + padding
2 v2
Cinco lecturas:
- El bloque v2 está en el 100 % de las muestras. Ni un solo
APKdel corpus, ni siquiera los splits de solo idioma de menos de 50 KB, se firma únicamente con v1. La obligación detargetSdk≥ 30 se nota. - El relleno falta en 2 de 717. Los dos son standalone de F-Droid:
org.briarproject.briar.android_10517.apk(bloque de 1.415 bytes) yorg.gnucash.android_2.4.0.apk(1.487 bytes). Ninguno es múltiplo de 4096, lo que indica que se firmaron con una cadena que no añade el par de relleno. - El
source stampno es una rareza: 86 % de las muestras. Es lo que producebundletool, y por eso domina en los contenedores de splits. - v3.1 aparece en un tercio del corpus, siempre acompañado de v3, nunca solo. La regla
V31_BLOCK_FOUND_WITHOUT_V3_BLOCKse cumple en el 100 % de los casos observados. - El bloque de dependencias de Google Play solo aparece en 2 de 717, y los dos son
aplicaciones distribuidas por F-Droid —Bitwarden y Orbot—, no por Play. Es el bloque que
añade el Android Gradle Plugin al compilar en release; que en el corpus casi no aparezca se
explica porque los proyectos de F-Droid suelen desactivarlo con
dependenciesInfo.
Reparto solo sobre los 58 standalone, para contraste: v2 en 58, relleno en 56, v3 en 42, dependency info en 2 y source stamp en 1. Ningún standalone lleva v3.1 ni frosting.
10.2 Alineación
tamaño del bloque múltiplo de 4096: {True: 715, False: 2}
inicio del bloque múltiplo de 4096: {True: 701, False: 16}
Los dos que fallan en tamaño son los dos sin relleno. Los 16 que no empiezan en múltiplo de 4096 son un universo distinto, y aquí está la correlación que confirma la sección 6.3:
no alineado / sin verity: 16 alineado / sin verity: 36
no alineado / con verity: 0 alineado / con verity: 6
Cero excepciones: todo APK con un digest VERITY_CHUNKED_SHA256 tiene su bloque
empezando en múltiplo de 4096, y los que no empiezan alineados no llevan verity. La condición
del código no es teórica.
11. El bloque desconocido de Google Keep: 0x2146444e
El base.apk de Google Keep lleva un bloque más que ninguna de las constantes de apksig
identifica. El volcador lo sitúa: 0x2146444e, 3.930 bytes, entre el source stamp y el
relleno.
Es el bloque de «frosting» de Google Play. No está en apksig porque no lo escribe la
cadena de firma de Android: lo añade Google Play al distribuir, después de que el
desarrollador haya firmado. Es una firma adicional, hecha con una clave de Google, que
permite a Play Protect determinar sin ambigüedad si un APK concreto salió de Play. El
enumerado BlockId de avast/apkverifier —la implementación que lo documenta— lo llama
blockIdFrosting = 0x2146444e.
Los primeros bytes de su valor confirman que es Protocol Buffers y no una firma con el formato de las de Android:
90 1e ea 1d 08 01 10 01 18 01 20 97 a4 ef df 9c 34 2a d4 1d 42 0c 0a 02 08 20 30 03 30 02
30 04 30 05 4a 0b 0a 09 08 fd da 9c 69 1a 02 08 20 52 20 0a 1e ff ff ff ff ff ff fd ff ff
Los campos 08 01 10 01 18 01 son varints de campos 1, 2 y 3 con valor 1; 12 22 0a 20 abre
un campo de 32 bytes, que es el tamaño de un SHA-256. El análisis público describe el
contenido como metadatos —marca de tiempo de firma, versionCode, minSdkVersion— junto al
SHA-256 del fichero, firmado con ECDSA-SHA256 contra una clave pública de Google.
⚠️ sin verificar: la interpretación campo a campo del protobuf no se ha reproducido aquí; solo
se ha confirmado la codificación protobuf y la presencia de digests de 32 bytes. El esquema
.proto no es público.
Su presencia es un indicador útil, no una firma que verificar. En el corpus aparece en 25
APK, y son exactamente los que vienen de Play: los base.apk de las aplicaciones de Google
(Keep, Maps, YouTube, Photos, Docs), Duolingo, Candy Crush, LinkedIn Learning. Los .apkm de
APKMirror lo conservan porque copian el APK tal cual, lo que confirma de paso que APKMirror
redistribuye binarios de Play sin recompilarlos. Ninguna aplicación de F-Droid lo tiene.
Para una herramienta de análisis, eso lo convierte en la evidencia estructural más barata de
que un APK pasó por Google Play, más fiable que el nombre del fichero o que el origen
declarado.
Fuentes
- APK signature scheme v2 — Android Open Source Project — https://source.android.com/docs/security/features/apksigning/v2
Consultado el 12 de agosto de 2026. De aquí salen el layout del bloque de la sección 3, la
cita sobre la magic, el algoritmo de digest por chunks de la sección 6.2 con sus prefijos
0xa5y0x5a, la estructura interna de la sección 5 y la tabla de algoritmos de firma de la sección 7 con los rangos de tamaño de clave. - AOSP —
tools/apksig/.../internal/apk/ApkSigningBlockUtils.java— https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java Consultado el 12 de agosto de 2026. De aquí salenAPK_SIGNING_BLOCK_MAGIC,VERITY_PADDING_BLOCK_ID = 0x42726577,CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, el código citado degenerateApkSigningBlockde la sección 8, el deChunkDigestsde la sección 6.2 y las dos comprobaciones de alineación de la sección 6.3. - AOSP —
tools/apksig/.../internal/apk/SignatureAlgorithm.java— https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java Consultado el 12 de agosto de 2026. De aquí sale íntegra la tabla de la sección 7, con sus identificadores, sus parámetros PSS y susminSdkVersion. - AOSP —
tools/apksig/.../internal/apk/ContentDigestAlgorithm.java— https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java Consultado el 12 de agosto de 2026. De aquí sale la tabla de la sección 6.3. - AOSP —
tools/apksig/.../internal/apk/v2/V2SchemeConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java Consultado el 12 de agosto de 2026. De aquí salen0x7109871ay0xbeeff00d. - AOSP —
tools/apksig/.../internal/apk/v3/V3SchemeConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java Consultado el 12 de agosto de 2026. De aquí salen0xf05368c0,0x1b93ad61,0x3ba06f8c,0x559f8b02y0xc2a6b3ba. - AOSP —
tools/apksig/.../internal/apk/stamp/SourceStampConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java Consultado el 12 de agosto de 2026. De aquí salen0x2b09189e,0x6dff800d,0x9d6303f7y0xe43c5946, y la descripción del formato del atributo de marca de tiempo. avast/apkverifier—signingblock/signingblock.go— https://github.com/avast/apkverifier/blob/master/signingblock/signingblock.go Consultado el 12 de agosto de 2026. De aquí salen los tres identificadores que no están enapksig:BlockIdDependencyMetadata = 0x504b4453,BlockIdMeituanMetadata = 0x71777777yblockIdFrosting = 0x2146444ede la sección 11. Se usa esta fuente porque no existe documentación oficial de Google sobre el bloque de frosting.- Android Gradle Plugin 4.0.0 release notes — Dependency metadata — https://developer.android.com/build/releases/past-releases/agp-4-0-0-release-notes
Consultado el 12 de agosto de 2026. De aquí sale la descripción del bloque
0x504b4453: «The data is compressed, encrypted by a Google Play signing key, and stored in the signing block of your release app», y el bloquedependenciesInfoque lo desactiva. - Easter Egg in APK Files: What Is Frosting — BI.ZONE — https://bi-zone.medium.com/easter-egg-in-apk-files-what-is-frosting-f356aa9f4d1
Consultado el 12 de agosto de 2026. De aquí sale la descripción funcional del bloque
0x2146444ede la sección 11: quién lo añade, con qué clave se firma y qué metadatos contiene. Es análisis de terceros, no documentación oficial, y por eso la interpretación detallada del protobuf queda marcada como no verificada. lib/apksigner.jardebuild-tools37.0.0 — inspeccionado conjavap -constantsel 12 de agosto de 2026 en esta máquina. De aquí salenAPK_SIGNATURE_SCHEME_V32_BLOCK_IDyHYBRID_MIN_SDK_VERSION_ATTR_IDde las secciones 4 y 4.1.- Corpus de verificación —
~/corpus-apkMedido el 12 de agosto de 2026 con los guiones de las secciones 9 y 10. De aquí sale íntegro el censo de la sección 10, los volcados de las secciones 5 y 9 y la identificación del bloque de Google Keep de la sección 11.