El APK Signing Block

Antes conviene leer «Contenedor ZIP» · «Esquemas de firma v1, v2 y v3»

referencia técnica · Actualizado el 25 de septiembre de 2026

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 ZIP que 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:

  1. La firma se calcula sobre los bytes del signed data completo, tal cual aparecen en el fichero. No sobre una reserialización: hay que quedarse con el rango de bytes original.
  2. La lista de algoritmos de digests y la de signatures debe coincidir. La spec lo exige explícitamente; si no coinciden, el APK se rechaza.
  3. El primer certificado de la secuencia es el del firmante, y su clave pública debe ser idéntica al campo public key del signer. Comprobar solo uno de los dos deja pasar un APK con la clave cambiada.
  4. Las copias sin firmar de minSdkVersion y maxSdkVersion —solo en v3 y v3.1— permiten descartar un firmante fuera de rango sin abrir el signed 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.parseSigner emite V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD como error, y en AOSP ApkSignatureSchemeV3Verifier.verifySigner lanza SecurityException("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 ZIP y 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:

  1. El bloque v2 está en el 100 % de las muestras. Ni un solo APK del muestrario, ni siquiera los splits de solo idioma de menos de 50 KB, se firma únicamente con v1. La obligación de targetSdk ≥ 30 se nota.
  2. 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) y org.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.
  3. El source stamp no es una rareza: 86 % de las muestras. Es lo que produce bundletool, y por eso domina en los contenedores de splits.
  4. v3.1 aparece en un tercio del muestrario, siempre acompañado de v3, nunca solo. La regla V31_BLOCK_FOUND_WITHOUT_V3_BLOCK se cumple en el 100 % de los casos observados.
  5. El bloque de dependencias de Google Play solo aparece en 2 de 717, y los dos son APK sueltos 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 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 · 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), n bytes firmados y la firma. Los n bytes son varint(m), el mensaje protobuf de m bytes y una lista de metadatos con el SHA-256 del fichero. La firma es un ECDSA-Sig-Value DER de 256 bits, y verifica con la clave pública de Play que frosting.go copia 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.1 es el versionCode, y 5.8.1.1 y 5.12.1.5, el minSdkVersion: coinciden en los tres con el manifiesto.
  • 5.12.2.2.1 es el SHA-256 del propio APK base antes de recibir el frosting: quitando el par 0x2146444e y recalculando el relleno a 4096, coincide en los tres. Cada 5.12.2.3 es 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

  1. 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 0xa5 y 0x5a, 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.
  2. 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í salen APK_SIGNING_BLOCK_MAGIC, VERITY_PADDING_BLOCK_ID = 0x42726577, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, el código citado de generateApkSigningBlock de la «sección 8 · El bloque de relleno», el de ChunkDigests de 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».
  3. 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 sus minSdkVersion.
  4. 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».
  5. 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í salen 0x7109871a y 0xbeeff00d.
  6. 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í salen 0xf05368c0, 0x1b93ad61, 0x3ba06f8c, 0x559f8b02 y 0xc2a6b3ba.
  7. 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í salen 0x2b09189e, 0x6dff800d, 0x9d6303f7 y 0xe43c5946, y la descripción del formato del atributo de marca de tiempo.
  8. 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 en apksig: BlockIdDependencyMetadata = 0x504b4453, BlockIdMeituanMetadata = 0x71777777 y blockIdFrosting = 0x2146444e de 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.
  9. 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 bloque dependenciesInfo que lo desactiva.
  10. 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 0x2146444e de 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.
  11. lib/apksigner.jar de build-tools 37.0.0 — inspeccionado con javap -constants el 12 de agosto de 2026. De aquí salieron APK_SIGNATURE_SCHEME_V32_BLOCK_ID y HYBRID_MIN_SDK_VERSION_ATTR_ID de las secciones «4» y «4.1», antes de encontrarlos en la fuente 12.
  12. AOSP — V3SchemeConstants.java en la etiqueta android-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í salen 0x70e1c89f, 0x9f06b79c y lo que hacen los dos atributos híbridos (secciones «4» y «4.1»), y el nombre nuevo de 0xc2a6b3ba.
  13. AOSP — ApkSigningBlockUtils.java, DefaultApkSignerEngine.java y V4SchemeSigner.java en la etiqueta android-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 de VERITY_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 de generateApkSigningBlockPadding de la «sección 8 · El bloque de relleno».
  14. AOSP — tools/base, signflinger/.../SignedApk.java, etiqueta studio-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 que 0x504b4453 es el DEPENDENCY_INFO_BLOCK_ID que añade el Android Gradle Plugin («sección 4 · Identificadores de bloque conocidos»).
  15. 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»).
  16. Meituan-Dianping/walle, ApkUtil.java, commit f78edcf — 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í sale APK_CHANNEL_BLOCK_ID = 0x71777777 («sección 4 · Identificadores de bloque conocidos»).
  17. avast/apkverifier — signingblock/frosting.go, commit d0e1a791 — https://github.com/avast/apkverifier/blob/d0e1a791cd5ab5b84eb6f271d59ac3b2a9771071/signingblock/frosting.go Consultado el 25 de septiembre de 2026 y reimplementado en Python sobre los tres base.apk de Play disponibles, con la verificación de la firma hecha con openssl 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».