El APK Signing Block

referencia técnica · Revisado el 12 de agosto 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 Blockglosario— 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 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. 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:

  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 sirven para descartar rápido un firmante fuera de rango. La decisión real usa las de dentro del signed 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 Blockno 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 0xa5longitud del chunk (uint32 LE) ‖ datos.
  • Digest final: se digiere 0x5anú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:

  1. El bloque v2 está en el 100 % de las muestras. Ni un solo APK del corpus, 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 corpus, 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 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

  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, la cita sobre la magic, el algoritmo de digest por chunks de la sección 6.2 con sus prefijos 0xa5 y 0x5a, 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.
  2. 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í 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 de ChunkDigests de la sección 6.2 y las dos comprobaciones de alineación de la sección 6.3.
  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 sus minSdkVersion.
  4. 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.
  5. 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í salen 0x7109871a y 0xbeeff00d.
  6. 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í salen 0xf05368c0, 0x1b93ad61, 0x3ba06f8c, 0x559f8b02 y 0xc2a6b3ba.
  7. 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í salen 0x2b09189e, 0x6dff800d, 0x9d6303f7 y 0xe43c5946, y la descripción del formato del atributo de marca de tiempo.
  8. avast/apkverifiersigningblock/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. 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: 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 en esta máquina. De aquí salen APK_SIGNATURE_SCHEME_V32_BLOCK_ID y HYBRID_MIN_SDK_VERSION_ATTR_ID de las secciones 4 y 4.1.
  12. Corpus de verificación~/corpus-apk Medido 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.