El APK Signing Block
Antes conviene leer «Contenedor ZIP» · «Esquemas de firma v1, v2 y v3»
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 · Censo real del muestrario» mide en el «muestrario» 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 · Layout global». 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 · Identificadores de bloque conocidos» |
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 muestrario, 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 |
v3/V3SchemeConstants.java, etiqueta android-17.0.0_r1 |
Firma v3.2, híbrida |
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 · El bloque de relleno») |
0x504b4453 |
— | No está en apksig |
Metadatos de dependencias de Google Play |
0x2146444e |
— | No está en apksig |
«Frosting» de Google Play («sección 11 · El bloque desconocido de Google Keep: 0x2146444e») |
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/.
0x70e1c89f es el bloque de la firma v3.2 de Android 17, con MIN_SDK_WITH_V32_SUPPORT = AndroidSdkVersion.C, que vale 37.
Lo documenta source.android.com («APK Signature Scheme v3.2») y está en la etiqueta
android-17.0.0_r1 de apksig; no en la rama main, que se congeló en marzo de 2025. Su
formato y sus dos firmantes, uno clásico y otro poscuántico, están en
«Esquemas de firma → §7 · v3.2: la firma híbrida». No aparece en ningún APK del
muestrario.
0x504b4453, 0x2146444e y 0x71777777 no están en apksig: buscados en hexadecimal y en
decimal en src/main de platform/tools/apksig en android-17.0.0_r1, no aparecen. Ninguno es
un esquema de firma de la plataforma. 0x504b4453 lo añade el Android Gradle Plugin después de
firmar: es DEPENDENCY_INFO_BLOCK_ID en signflinger/src/com/android/signflinger/SignedApk.java
de platform/tools/base (líneas 57-58 y 254-256 en la etiqueta studio-2026.1.2), y lleva los
datos de dependencias que, según la documentación de Android Studio, van «encrypted by a Google
Play signing key». 0x71777777 es el APK_CHANNEL_BLOCK_ID de Walle, el empaquetador de
canales de Meituan (payload_reader/.../ApkUtil.java, línea 40). 0x2146444e no tiene fuente
primaria pública: el nombre de «frosting» de Google Play sale del enumerado BlockId de
avast/apkverifier, con el que se han cotejado los tres valores. Los dos primeros se han
observado en el muestrario («sección 10 · Censo real del muestrario»); 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 (SIGNER_TARGETS_DEV_RELEASE_ATTR_ID desde Android 17) |
Firmantes v3.1 y v3.2 | El firmante apunta a una release en 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 |
Firmantes v3.0 y v3.1 | SDK mínimo del bloque v3.2, int32 little-endian: si el atributo está y el bloque híbrido falta o no coincide, el APK se rechaza |
0x9f06b79c |
HYBRID_MAX_SDK_VERSION_ATTR_ID |
Firmantes v3.0 y v3.1 | SDK máximo del bloque v3.2. Opcional: si falta no se compara; si está sin el mínimo, el APK se rechaza. apksigner 37.0.0 no lo escribe |
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
minSdkVersionymaxSdkVersion—solo en v3 y v3.1— permiten descartar un firmante fuera de rango sin abrir elsigned data, y la decisión real usa las de dentro. Pero no son un atajo informativo: tienen que ser idénticas a las firmadas. La especificación las define como «duplicate of the signed data value, must match», y el paso 3.3 de verificación ordena comprobarlo. Una discrepancia no es un aviso, es un fallo:V3SchemeVerifier.parseSigneremiteV3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORDcomo error, y en AOSPApkSignatureSchemeV3Verifier.verifySignerlanzaSecurityException("minSdkVersion mismatch between signed and unsigned in v3 signer block."). Un verificador propio que se salte esa comparación acepta bloques que Android rechaza.
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 · Las dos exclusiones y por qué son necesarias».
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, y su digest final es la raíz del árbol más el tamaño del contenido. El
árbol se parece al de la firma v4, descrito en
«Firma v4 y verificación → §3 · El árbol de Merkle y fs-verity», pero no es el mismo:
aquí cada hash lleva una sal de 8 bytes a cero y el árbol cubre el fichero sin el
APK Signing Block, mientras que el de v4 va sin sal, cubre el fichero entero y su digest es
solo la raíz.
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 · Alineación» comprueba esa correlación sobre el muestrario, 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 · Los algoritmos de digest de contenido» 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 |
0x0501 |
ML_DSA |
ML-DSA (ML-DSA-65 o ML-DSA-87; apksig también acepta ML-DSA-44, que la plataforma no admite) |
CHUNKED_SHA512 |
37 |
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 muestrario solo aparecen tres de los doce: 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 · Los algoritmos de digest de contenido»). Se
consigue insertando hasta 4.095 bytes a cero entre la última entrada del
ZIPy el bloque (generateApkSigningBlockPadding), no dentro del 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 · El EOCD de un APK real» 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 muestrario, ejecutada el 12 de agosto de 2026:
MUESTRAS=~/muestras-apk
python3 dump_sigblock.py $MUESTRAS/com.termux_1002.apk \
$MUESTRAS/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 · Los algoritmos de digest de contenido» no aplica. Y el
0x2146444e de Keep es el bloque que la «sección 11 · El bloque desconocido de Google Keep: 0x2146444e» identifica.
10. Censo real del muestrario
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 muestrario, 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 muestrario, 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
APKsueltos que publica el propio proyecto —Bitwarden y Orbot—, no Play. Es el bloque que añade el Android Gradle Plugin al compilar en release; que en el muestrario casi no aparezca se explica porque los proyectos de F-Droid suelen desactivarlo condependenciesInfo.
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 · Los algoritmos de digest de contenido»:
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 sin identificar. 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 esquema .proto no es público, pero la
estructura exterior que publica avast/apkverifier (signingblock/frosting.go) se ha
reproducido sobre los tres base.apk de Play disponibles —los de los .xapk de Keep,
Calculator y Authenticator—, y buena parte del resto se identifica cotejando con datos
conocidos:
- El valor es
varint(n),nbytes firmados y la firma. Losnbytes sonvarint(m), el mensaje protobuf dembytes y una lista de metadatos con el SHA-256 del fichero. La firma es unECDSA-Sig-ValueDER de 256 bits, y verifica con la clave pública de Play quefrosting.gocopia de la tienda («from com.android.vending 11.2.14»); el SHA-256, recalculado con el algoritmo de ese mismo fichero, coincide. Con un bit cambiado, la firma ya no verifica. - En el protobuf,
5.9.1.1es elversionCode, y5.8.1.1y5.12.1.5, elminSdkVersion: coinciden en los tres con el manifiesto. 5.12.2.2.1es el SHA-256 del propioAPKbase antes de recibir el frosting: quitando el par0x2146444ey recalculando el relleno a 4096, coincide en los tres. Cada5.12.2.3es un split, con su SHA-256 y su calificador, y casan con los splits del.xapk: 6 de 6 en Calculator, 26 de 26 en Keep y 27 de 32 en Authenticator.- El campo 4 tiene el tamaño de una marca de tiempo en milisegundos, y el análisis público lo lee como la fecha de la firma. No puede serlo: así leído cae entre el 27 de septiembre y el 11 de noviembre de 2026, entre 79 y 90 días después de la fecha de los propios ficheros. Su significado queda abierto.
Su presencia ya es un indicador útil, sin necesidad de verificarla. En el muestrario aparece en 25
APK, todos base.apk de aplicaciones de Play: los de las aplicaciones de Google (Keep, Maps,
YouTube, Photos, Drive), 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, y tampoco los split
de las de Play ni el APK suelto que llegó por APKPure: su ausencia no descarta Play.
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 · Layout byte a byte», la
cita sobre la magic, el algoritmo de digest por chunks de la «sección 6.2 · El troceado» con sus prefijos
0xa5y0x5a, la estructura interna de la «sección 5 · Estructura interna de un bloque v2 o v3» y la tabla de algoritmos de firma de la «sección 7 · Algoritmos de firma» con los rangos de tamaño de clave. - AOSP —
tools/apksig/.../internal/apk/ApkSigningBlockUtils.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java Consultado el 25 de septiembre 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 bloque de relleno», el deChunkDigestsde la «sección 6.2 · El troceado» y las dos comprobaciones de alineación de la «sección 6.3 · Los algoritmos de digest de contenido». - AOSP —
tools/apksig/.../internal/apk/SignatureAlgorithm.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java Consultado el 25 de septiembre de 2026. De aquí sale íntegra la tabla de la «sección 7 · Algoritmos de firma», con sus identificadores, sus parámetros PSS y susminSdkVersion. - AOSP —
tools/apksig/.../internal/apk/ContentDigestAlgorithm.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java Consultado el 25 de septiembre de 2026. De aquí sale la tabla de la «sección 6.3 · Los algoritmos de digest de contenido». - AOSP —
tools/apksig/.../internal/apk/v2/V2SchemeConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java Consultado el 25 de septiembre de 2026. De aquí salen0x7109871ay0xbeeff00d. - AOSP —
tools/apksig/.../internal/apk/v3/V3SchemeConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java Consultado el 25 de septiembre de 2026. De aquí salen0xf05368c0,0x1b93ad61,0x3ba06f8c,0x559f8b02y0xc2a6b3ba. - AOSP —
tools/apksig/.../internal/apk/stamp/SourceStampConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java Consultado el 25 de septiembre 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 · El bloque desconocido de Google Keep: 0x2146444e». 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 · El bloque desconocido de Google Keep: 0x2146444e»: 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. De aquí salieronAPK_SIGNATURE_SCHEME_V32_BLOCK_IDyHYBRID_MIN_SDK_VERSION_ATTR_IDde las secciones «4» y «4.1», antes de encontrarlos en la fuente 12.- AOSP —
V3SchemeConstants.javaen la etiquetaandroid-17.0.0_r1, y la página «APK Signature Scheme v3.2» — https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java y https://source.android.com/docs/security/features/apksigning/v3-2 Consultadas el 24 de septiembre de 2026. De aquí salen0x70e1c89f,0x9f06b79cy lo que hacen los dos atributos híbridos (secciones «4» y «4.1»), y el nombre nuevo de0xc2a6b3ba. - AOSP —
ApkSigningBlockUtils.java,DefaultApkSignerEngine.javayV4SchemeSigner.javaen la etiquetaandroid-17.0.0_r1— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java y https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java Consultado el 25 de septiembre de 2026. De aquí salen las diferencias entre el árbol deVERITY_CHUNKED_SHA256(computeApkVerityDigest) y el de v4 (generateV4Signature) de la «sección 6.3 · Los algoritmos de digest de contenido», y el relleno previo al bloque degenerateApkSigningBlockPaddingde la «sección 8 · El bloque de relleno». - AOSP —
tools/base,signflinger/.../SignedApk.java, etiquetastudio-2026.1.2— https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2026.1.2/signflinger/src/com/android/signflinger/SignedApk.java Consultado el 25 de septiembre de 2026. De aquí sale que0x504b4453es elDEPENDENCY_INFO_BLOCK_IDque añade el Android Gradle Plugin («sección 4 · Identificadores de bloque conocidos»). - Add build dependencies — Android Studio, «Dependency information for Play Console» — https://developer.android.com/build/dependencies Consultado el 25 de septiembre de 2026. De aquí sale que esos datos van cifrados con una clave de Google Play en el bloque de firma («sección 4 · Identificadores de bloque conocidos»).
Meituan-Dianping/walle,ApkUtil.java, commitf78edcf— https://github.com/Meituan-Dianping/walle/blob/f78edcf1117a0aa858a3d04bb24d86bf9ad51bb2/payload_reader/src/main/java/com/meituan/android/walle/ApkUtil.java#L40 Consultado el 25 de septiembre de 2026. De aquí saleAPK_CHANNEL_BLOCK_ID = 0x71777777(«sección 4 · Identificadores de bloque conocidos»).avast/apkverifier—signingblock/frosting.go, commitd0e1a791— https://github.com/avast/apkverifier/blob/d0e1a791cd5ab5b84eb6f271d59ac3b2a9771071/signingblock/frosting.go Consultado el 25 de septiembre de 2026 y reimplementado en Python sobre los tresbase.apkde Play disponibles, con la verificación de la firma hecha conopenssl dgst -sha256 -verify. De aquí salen la estructura exterior del frosting, su firma y su SHA-256, y el cotejo de los campos del protobuf de la «sección 11 · El bloque desconocido de Google Keep: 0x2146444e».