Firma v4, verificación y keystores

referencia técnica · Revisado el 12 de agosto de 2026

1. Qué es y por qué existe

Los esquemas v2 y v3 tienen una propiedad incómoda: para verificarlos hay que haber leído el fichero entero. El digest se calcula sobre todos los bytes, así que el instalador no puede decir «esto es auténtico» hasta que ha recibido el último. Con APK de cientos de megas —los juegos del corpus rondan los 100 MB— eso significa esperar a la descarga completa antes de poder ejecutar nada.

La instalación incrementalglosario— de Android 11 rompe esa espera: la aplicación arranca con una parte del APK transferida y el resto se va trayendo bajo demanda, verificando cada bloque en el momento en que se lee. Para eso hace falta un digest que se pueda comprobar por trozos sin tener el resto, y eso es exactamente un árbol de Merkle. El esquema v4 lo aporta.

La segunda mitad de este documento trata algo distinto pero inseparable: el orden en que el sistema verifica. Con cinco esquemas conviviendo, «¿verifica este APK?» no tiene una respuesta única, y las reglas de precedencia y de protección contra el downgrade son donde se concentran los errores de implementación. La sección 5 las recorre en el orden real del código.

2. El fichero .idsig

2.1 Por qué va fuera del APK

Un árbol de Merkle sobre un fichero de 100 MB con bloques de 4 KiB ocupa del orden de 800 KB. Meterlo dentro del APK Signing Block engordaría el APK para todos los dispositivos, y —lo decisivo— el árbol es un digest del fichero completo, así que meterlo dentro cambiaría el fichero del que se calculó. La solución es un fichero acompañante, <nombre>.apk.idsig, que viaja al lado del APK y no dentro: adb install --incremental lo consume y el APK instalado no lo contiene.

2.2 Estructura

De V4Signature.java. Todos los campos bytes van con prefijo uint32 de longitud, y todo es little-endian.

Campo Offset Tamaño Tipo Significado
version 0x00 4 uint32 CURRENT_VERSION = 2. «only version 2 is supported as of now»
hashingInfo length 0x04 4 uint32 Longitud del bloque siguiente
hashingInfo 0x08 var Ver tabla siguiente
signingInfos length 4 uint32
signingInfos var Ver tabla siguiente. Máximo MAX_SIGNING_INFOS_SIZE = 7168
verityTree length 4 uint32 Opcional. Sin él, la firma es stripped
verityTree var El árbol de Merkle serializado

hashingInfo:

Campo Offset Tamaño Tipo Significado
hashAlgorithm 0x00 4 uint32 HASHING_ALGORITHM_SHA256 = 1. «only 1 == SHA256 supported»
log2BlockSize 0x04 1 byte LOG2_BLOCK_SIZE_4096_BYTES = 12. «only 12 (block size 4096) supported now»
salt length 0x05 4 uint32 0 en todo lo que produce apksigner. Máximo 32
salt 0x09 var «used exactly as in fs-verity, 32 bytes max»
rawRootHash length 4 uint32 32 con SHA-256
rawRootHash var «salted digest of the first Merkle tree page»

signingInfo (el primero; detrás pueden ir bloques adicionales con su blockId):

Campo Tamaño Tipo Significado
apkDigest var bytes Digest de contenido del APKel mismo valor que el del bloque v3 o v2
certificate var bytes X.509 en DER
additionalData var bytes «a free-form binary data blob»
publicKey var bytes SubjectPublicKeyInfo en DER, debe corresponder al certificado
signatureAlgorithmId 4 uint32 De la tabla de la sección 7 de APK Signing Block
signature var bytes Firma sobre el signed data

Lo que se firma no es el apkDigest a secas. V4Signature.getSignedData(fileSize, hashingInfo, signingInfo) construye un bloque que incluye el tamaño del fichero y todo el hashingInfo —y por tanto la raíz del árbol— además del digest y el certificado. Firmar solo la raíz permitiría reutilizar la firma con otro tamaño de fichero.

2.3 Un .idsig real, campo a campo

Generado en la sección 10.2 sobre una copia de org.fossify.calculator_1.4.0.apk de 7.176.562 bytes. Primeros 64 bytes:

00000000: 0200 0000 2d00 0000 0100 0000 0c00 0000  ....-...........
00000010: 0020 0000 0040 4cd0 b2fa 6a32 58fe 42ac  . ...@L...j2X.B.
00000020: 6474 09b0 586f 9395 ae50 18a9 f1ab 5802  dt..Xo...P....X.
00000030: 4d06 0a60 f0cc 0500 0020 0000 00b7 628a  M..`..... ....b.

Descompuesto:

0x00  02 00 00 00                version = 2
0x04  2d 00 00 00                hashingInfo = 45 bytes
0x08  01 00 00 00                  hashAlgorithm = 1 (SHA-256)
0x0c  0c                           log2BlockSize = 12  → bloque de 4096
0x0d  00 00 00 00                  salt = 0 bytes
0x11  20 00 00 00                  rawRootHash = 32 bytes
0x15  40 4c d0 b2 … 0a 60 f0        404cd0b2fa6a3258fe42ac647409b0586f9395ae5018a9f1ab58024d060a60f0
0x35  cc 05 00 00                signingInfos = 1484 bytes
0x39  20 00 00 00                  apkDigest = 32 bytes
0x3d  b7 62 8a 52 …                 b7628a525424c06e29230c37fedf07f8b0e3fd19c2b3886ea23dee7b8ef10ee6
…
0x605 00 f0 00 00                verityTree = 61440 bytes

4 + 1 + 4 + 0 + 4 + 32 = 45: el hashingInfo cuadra al byte.

Y el apkDigest no es un número cualquiera. Al modificar un byte de ese mismo APK, el error que devuelve apksigner sobre el bloque v3 es:

ERROR: APK Signature Scheme v3 signer #1: APK integrity check failed. CHUNKED_SHA256 digest
mismatch. Expected: <b7628a525424c06e29230c37fedf07f8b0e3fd19c2b3886ea23dee7b8ef10ee6>, …

El mismo valor. El apkDigest del .idsig es literalmente el digest de contenido del bloque v3, copiado. Eso es lo que ata el .idsig a la firma que vive dentro del APK, y es el mecanismo de la sección 4.

3. El árbol de Merkle y fs-verity

3.1 Cómo se construye

VerityTreeBuilder.java implementa el árbol de fs-verity con CHUNK_SIZE = 4096 y JCA_ALGORITHM = "SHA-256". La documentación del kernel describe el mismo procedimiento:

«The file contents is divided into blocks, where the block size is configurable but is usually 4096 bytes. The end of the last block is zero-padded if needed. Each block is then hashed, producing the first level of hashes. Then, the hashes in this first level are grouped into blocksize-byte blocks (zero-padding the ends as needed) and these blocks are hashed, producing the second level of hashes. This proceeds up the tree until only a single block remains.»

datos del APK, 4096 B por bloque
 ┌────┬────┬────┬────┬────┬────┬────┬─ … ─┬────┐
 │ b0 │ b1 │ b2 │ b3 │ b4 │ b5 │ b6 │     │bn-1│   ← el ultimo, con relleno de ceros
 └─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─────┴─┬──┘
   ▼    ▼    ▼    ▼    ▼    ▼    ▼          ▼        SHA-256 de cada bloque
 ┌────┬────┬────┬────┬────┬────┬────┬─ … ─┬────┐
 │ h0 │ h1 │ h2 │ h3 │ h4 │ h5 │ h6 │     │hn-1│   nivel 0: 32 B por hash
 └────┴────┴────┴────┴────┴────┴────┴─────┴────┘
       agrupados de nuevo en bloques de 4096 B (128 hashes por bloque)
                       ▼
              ┌────┬────┬─ … ─┐
              │ H0 │ H1 │     │                    nivel 1
              └────┴────┴─────┘
                       ▼
                    ┌────┐
                    │ R  │                          un solo bloque
                    └────┘
                       ▼
              raw root hash = SHA-256 de ese bloque

Como el APK cubierto es el mismo que cubre v2 —tramo 1 + Central Directory + EOCD con el offset normalizado, ver sección 6.1 de APK Signing Block—, el árbol hereda la misma exclusión del propio APK Signing Block, y por eso generateVerityTreeRootHash empieza exigiendo:

if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) {
    throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE …);
}

3.2 El tamaño del árbol, comprobado

calculateLevelOffset da el tamaño de cada nivel: cada uno se redondea al alza a múltiplo de 4096. Sobre el APK de 7.176.562 bytes de la sección 2.3:

Nivel Entradas Bytes de hashes Redondeado a 4096
0 (hashes de los datos) ⌈7.176.562 / 4096⌉ = 1.753 1.753 × 32 = 56.096 57.344
1 ⌈56.096 / 4096⌉ = 14 14 × 32 = 448 4.096
Total 61.440

Y el campo verityTree length del .idsig real vale exactamente 61.440. El fichero mide 62.985 bytes: 1.545 de cabecera y firma, más 61.440 de árbol. Es un buen test de humo para cualquier implementación: el tamaño del árbol es determinista y se calcula del tamaño del APK.

3.3 Qué comparte con fs-verity y qué no

La documentación de v4 es explícita: el árbol «follows the structure of the fs-verity hash tree exactly (for example, zero-padding the salt and zero-padding the last block)». La razón es que en el dispositivo el árbol lo reconstruye el kernel, no la aplicación: fs-verity lo recalcula al instalar y lo guarda en el sistema de ficheros, y a partir de ahí cada lectura del APK instalado se verifica contra él de forma transparente. El comentario de VerityTreeBuilder lo dice: «The tree is currently stored only in memory and is never written out. Nevertheless, it is the actual verity tree format on disk, and is supposed to be re-generated on device».

Dos diferencias que conviene tener presentes:

  • fs-verity firma un fsverity_descriptor, no la raíz a secas. El kernel calcula el digest del fichero a partir de una estructura que incluye la raíz y los metadatos, «rather than the root hash alone, to resolve ambiguities in hashing». v4 hace lo análogo con su getSignedData, pero no es la misma estructura byte a byte.
  • La sal. fs-verity la admite y la rellena con ceros «to the closest multiple of the input size of the hash algorithm's compression function». apksigner no la usa: el .idsig generado aquí declara salt length = 0, y el comentario de V4SchemeSigner explica por qué: «Salt has to stay empty for fs-verity compatibility».

4. v4 no se sostiene sola

La especificación lo dice en una línea: «A v4 signature requires a complementary v2 or v3 signature». No es una recomendación, es una precondición que apksig comprueba antes de firmar:

BT=~/Library/Android/sdk/build-tools/37.0.0
$BT/apksigner sign --ks pruebas.jks --ks-key-alias pruebas --ks-pass pass:pruebas \
  --v2-signing-enabled false --v3-signing-enabled false --v4-signing-enabled true \
  --out solo_v4.apk copia.apk
Exception in thread "main" java.lang.IllegalStateException: APK Signature Scheme v4 signing
requires at least v2 or v3 signing to be enabled
	at com.android.apksig.ApkSigner$Builder.build(ApkSigner.java:1923)

Por qué es así. El .idsig es un fichero suelto que cualquiera puede sustituir: no está firmado por nada que viaje dentro del APK. Si v4 fuese autónoma, un atacante podría entregar un APK cualquiera con un .idsig fabricado con su propia clave y el sistema no tendría con qué contrastarlo. El anclaje es el apkDigest de la sección 2.3: el .idsig copia el digest del bloque v2 o v3, así que verificar v4 obliga a que ese digest coincida con el que está firmado dentro del APK por la clave del desarrollador. v4 aporta la granularidad por bloques; la confianza sigue viniendo de v2/v3.

El código elige el digest en ese orden exacto — v3 si existe, v2 si no:

byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3)
        ? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3)
            : apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2);

5. El algoritmo completo de verificación

Reconstruido leyendo ApkVerifier.verify(), que es la implementación que Android usa y contra la que se compara todo lo demás.

5.1 El flujo

        entrada: APK, minSdk, maxSdk, [fichero .idsig]
                            │
     ┌──────────────────────▼───────────────────────────┐
     │ localizar EOCD → Central Directory → Signing Blk │
     └──────────────────────┬───────────────────────────┘
                            │
     ┌──────────────────────▼───────────────────────────┐
     │ si maxSdk >= 24, en este orden:                  │
     │   maxSdk >= 33  → bloque v3.1  0x1b93ad61        │
     │   maxSdk >= 28  → bloque v3    0xf05368c0        │──┐
     │   minSdk <  28  → bloque v2    0x7109871a        │  │
     │   o nada hallado antes                           │  │
     │   .idsig dado   → esquema v4                     │  │
     │ v3.1 presente sin v3 → error                     │  │
     └──────────────────────┬───────────────────────────┘  │
                            │                              │
     ┌──────────────────────▼───────────────────────────┐  │  bloque
     │ si minSdk < 24 o nada hallado:                   │  │  presente
     │   verificar v1 sobre META-INF/                   │──┤  que no
     └──────────────────────┬───────────────────────────┘  │  valida
                            │                              │
     ┌──────────────────────▼───────────────────────────┐  │
     │ si existe la entrada stamp-cert-sha256:          │  │
     │   verificar el source stamp  0x6dff800d          │  │
     ├──────────────────────────────────────────────────┤  │
     │ si v1 y v2 verificaron: mismo firmante           │──┤
     └──────────────────────┬───────────────────────────┘  │
                            ▼                              ▼
                        VERIFICA                       RECHAZO

5.2 Las reglas, en orden

1. De arriba hacia abajo, y el primero que se encuentra manda. El comentario del código: «Android N and newer attempts to verify APKs using the APK Signing Block, which can include v2 and/or v3 signatures. If none is found, it falls back to JAR signature verification. If the signature is found but does not verify, the APK is rejected.»

2. Un esquema superior presente obliga a que valide. Esta es la regla que más se malentiende. No es «prueba v3 y si falla prueba v2». Es «si hay bloque v3, tiene que validar; si no valida, se acabó». En el código, cada esquema termina con:

if (result.containsErrors()) {
    return result;
}

No hay camino de vuelta. La caída a un esquema inferior solo ocurre por ausencia, nunca por fallo.

3. Los inferiores se saltan si un superior ya cubrió el rango. El bloque v2 solo se verifica si minSdkVersion < 28 o si no se encontró nada; v1 solo si minSdkVersion < 24 o si no se encontró nada. De ahí la salida v2: false sobre un APK que sí tiene bloque v2 (sección 8 de Esquemas de firma v1, v2 y v3).

4. La protección contra el downgrade cierra el hueco. Las reglas 1 a 3 dejarían un ataque obvio: quitar el bloque superior para que el sistema use el inferior, más débil o firmado con otra clave. Tres candados cierran esa vía, y los tres están firmados:

Candado Dónde vive Qué declara
X-Android-APK-Signed Sección principal del .SF de v1 Lista de esquemas superiores presentes
0xbeeff00d STRIPPING_PROTECTION Atributo del bloque v2 Número del esquema superior presente
0x559f8b02 ROTATION_MIN_SDK_VERSION Atributo del bloque v3 minSdkVersion que debe declarar el v3.1

Más la regla simétrica sin atributo: un bloque v3.1 sin bloque v3 es un error, V31_BLOCK_FOUND_WITHOUT_V3_BLOCK. La demostración práctica del primer candado está en la sección 9.3 de Esquemas de firma v1, v2 y v3.

5. Si v1 y v2 verifican los dos, los firmantes tienen que coincidir. Un APK con v1 firmado por A y v2 firmado por B se rechaza. Sin esa comprobación, un atacante con una clave cualquiera podría añadir un bloque v2 propio a un APK firmado por otro y hacer que los dispositivos modernos aceptasen su versión.

6. targetSandboxVersion > 1 exige APK Signing Block. Desde Android 8.0, un APK que declare android:targetSandboxVersion mayor que 1 y no tenga ningún esquema v2+ se rechaza (NO_SIG_FOR_TARGET_SANDBOX_VERSION).

5.3 Qué se verifica del Central Directory y del EOCD

No hay un paso separado: están dentro del digest. Los tramos 3 y 4 de la sección 6.1 de APK Signing Block son el Central Directory completo y el EOCD, así que cualquier cambio en un nombre, un offset, un tamaño o el número de entradas rompe el digest igual que si se hubiera tocado un .dex.

La única pieza con tratamiento especial es el campo offset of start of central directory del EOCD, sustituido por el tamaño del tramo 1 antes de digerir. Ese campo queda determinado, no libre: su valor correcto se deduce de datos que sí están cubiertos.

Lo que no se verifica en esta capa es la coherencia interna del ZIP: que los Local File Header concuerden con el Central Directory, que no haya nombres duplicados o que no sobren bytes entre entradas. Eso es competencia del lector de ZIP (libziparchive en el dispositivo), y su divergencia con el verificador es la raíz de las dos vulnerabilidades históricas de la sección 12 de Contenedor ZIP. Una firma válida no implica un ZIP bien formado.

5.4 Un .idsig que no corresponde no es un error

Comportamiento observado con apksigner 0.9: al pasar un .idsig generado para otro APK, la salida es

Verifies
…
Verified using v4 scheme (APK Signature Scheme v4): false

con código de salida 0. Lo mismo con un .idsig truncado. El fichero se descarta en silencio y el veredicto global sigue siendo positivo, a diferencia de un digest v2/v3 que no cuadra, que produce DOES NOT VERIFY. ⚠️ sin verificar: no se ha encontrado en la documentación oficial una afirmación de que ese sea el comportamiento previsto ni de que el instalador de Android haga lo mismo; es lo que hace la herramienta local sobre los ficheros construidos en la sección 10.

6. Cadenas de certificados: qué se verifica y qué no

Este punto se malinterpreta constantemente, así que conviene decirlo sin rodeos.

Android no usa una PKI para las firmas de aplicación. No hay lista de CA de confianza, no se comprueba una cadena hasta una raíz, no se consultan CRL ni OCSP, y no se comprueba la fecha de caducidad del certificado en el momento de instalar. El certificado de un APK es casi siempre autofirmado: el subject y el issuer son la misma cadena, como se ve en el .RSA de F-Droid de la sección 3.3 de Esquemas de firma v1, v2 y v3.

Lo que sí se verifica:

Se verifica No se verifica
Que la firma es válida con la clave pública del bloque Que el certificado lo emita una CA reconocida
Que la clave pública corresponde al certificado del firmante Que la cadena llegue a una raíz de confianza
Que el digest del contenido coincide Que el certificado no esté revocado
Que la clave sea la misma que la de la instalación previa, o que esté acreditada por el lineage Que el certificado esté en vigor
Que los firmantes de v1 y v2 coincidan entre sí Que el CN diga la verdad sobre quién es

La identidad de una aplicación en Android es el hash de su certificado de firma. Nada más. Un CN=Android, O=Google Inc. en un APK no acredita que lo haya hecho Google: cualquiera puede generar ese dname con keytool en diez segundos. Lo que hace único al de Google es que el hash de su certificado es el que el dispositivo ya tiene asociado a ese packageName.

De ahí se siguen dos consecuencias prácticas:

  • Comparar por CN es un error de análisis. Se compara por SHA-256 del certificado, que es lo que imprime apksigner verify --print-certs.
  • Un certificado caducado sigue instalando. El requisito de validez hasta después del 22 de octubre de 2033 es una regla de Google Play, no de la plataforma: «If you plan to publish your apps on Google Play, the key you use to sign your app must have a validity period ending after 22 October 2033».

7. Leer un lineage de rotación

La estructura binaria del lineage, sus capacidades y su formato están en la sección 5.2 de Esquemas de firma v1, v2 y v3. Lo que interesa aquí es cómo se lee y qué decide.

Cómo se lee. Se recorre del primer nodo al último. Cada nodo lleva el certificado y las capacidades que se le conceden a ese certificado ya rotado; la firma de cada nodo la hizo la clave del nodo anterior, y el algoritmo con el que se hizo lo declara el nodo anterior en su signed data. El verificador comprueba, para cada nodo a partir del segundo, que sig(nodo_i) valida con la clave pública de nodo_{i-1}, y que el algoritmo declarado por el padre coincide con el que dice el hijo.

Qué decide. Al instalar una actualización, el sistema tiene un certificado asociado al packageName instalado. Si coincide con el firmante actual, no hay nada que decidir. Si no coincide, busca el certificado instalado dentro del lineage: si está, y el firmante actual es el último nodo, la sucesión es legítima y la actualización se acepta con las capacidades que el lineage conceda al certificado antiguo.

apksigner lineage --print-certs lo enseña sin escribir código:

$BT/apksigner lineage --print-certs --in $SCR/keep_x/com.google.android.keep.apk
Signer #1 in lineage certificate DN: CN=Android, OU=Android, O=Google Inc., L=Mountain View, …
Signer #1 in lineage certificate SHA-256 digest: f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83
Has installed data capability: true
Has shared UID capability    : true
Has permission capability    : true
Has rollback capability      : false
Has auth capability          : true
Signer #2 in lineage certificate DN: CN=Android, OU=Android, O=Google Inc., L=Mountain View, …
Signer #2 in lineage certificate SHA-256 digest: 7ce83c1b71f3d572fed04c8d40c5cb10ff75e6d87d9df6fbd53f0468c2905053
…las mismas cinco capacidades, idénticas…

Y los dos SHA-256 se corresponden con los dos firmantes del verify de la sección 9.1 de Esquemas de firma v1, v2 y v3: f0fd6c5b… es el firmante V3.0 con rango [24, 32] y 7ce83c1b… el firmante V3.1 con rango [33, INT_MAX]. El lineage y los rangos de SDK cuentan la misma historia desde dos sitios distintos del fichero, y que coincidan es parte de lo que se verifica.

rollback: false en los dos nodos: Google no permite volver a la clave antigua. Es el valor por defecto y el correcto.

8. El source stamp

Un source stamp acredita quién distribuyó el APK, de forma independiente a quién lo firmó. Son dos piezas que tienen que existir a la vez:

  1. Una entrada del ZIP llamada stamp-cert-sha256, en la raíz, con 32 bytes: el SHA-256 del certificado del sello.
  2. Un par 0x6dff800d en el APK Signing Block, con el certificado completo, la firma y sus atributos.

La entrada del ZIP está cubierta por el digest de v2/v3, así que no se puede cambiar sin romper la firma de distribución. Esa es la unión entre ambas piezas: el sello no puede sustituirse por otro sin invalidar la firma del desarrollador.

Comprobado sobre el base.apk de Google Keep:

unzip -p $SCR/keep_x/com.google.android.keep.apk stamp-cert-sha256 | xxd
00000000: 3257 d599 a49d 2c96 1a47 1ca9 843f 59d3  2W....,..G...?Y.
00000010: 41a4 0588 4583 fc08 7df4 237b 733b bd6d  A...E...}.#{s;.m

Y lo que informa apksigner:

Source Stamp Signer: certificate SHA-256 digest: 3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d
Source Stamp Signer: key size (bits): 4096
Source Stamp Timestamp: 1786040112

Byte a byte el mismo valor. El Source Stamp Timestamp es el atributo 0xe43c5946 (STAMP_TIME_ATTR_ID), «an 8-byte little-endian encoded long representing the epoch time in seconds when the stamp block was signed».

En qué se diferencia de la firma de distribución:

Firma v2/v3 Source stamp
Quién la pone El desarrollador (o Play App Signing por él) La cadena de compilación o distribución
Qué acredita Continuidad de la clave y que los bytes no han cambiado Origen: de dónde salió este binario
Qué pasa si falta El APK no instala Nada, es opcional
Qué pasa si falla El APK se rechaza apksigner informa y sigue
Presencia en el corpus 100 % 86,1 %

En el corpus, el source stamp aparece en 617 de 717 APK, y prácticamente todos son los splits producidos por bundletool, que lo añade por defecto. Un source stamp presente no es señal de nada especial; su ausencia en un split que debería tenerlo sí lo es.

9. Keystores

La clave privada y su certificado viven en un almacén cifrado. Android admite dos formatos, y la diferencia importa a la hora de leerlos.

JKS PKCS#12
Origen Propietario de Sun/Oracle Estándar del sector, RFC 7292
Extensión habitual .jks, .keystore .p12, .pfx
Contraseña de almacén y de clave Distintas: cada entrada tiene la suya Una sola en la práctica
Estado Obsoleto; keytool avisa al crear uno Por defecto desde Java 9
En el ecosistema Muy presente en keystores antiguos de firma Lo que produce keytool hoy

La distinción de la tercera fila es la que rompe herramientas: apksigner tiene --ks-pass para el almacén y --key-pass para la clave, y con un PKCS#12 la segunda sobra. Un lector que asuma que son la misma falla sobre los JKS antiguos, que son justo los que siguen usándose para firmar aplicaciones publicadas hace años.

9.1 El debug keystore

Android Studio crea automáticamente un almacén de depuración con parámetros conocidos y públicos:

Parámetro Valor
Ruta (macOS y Linux) ~/.android/debug.keystore
Ruta (Windows) C:\Users\<usuario>\.android\debug.keystore
Contraseña del almacén android
Alias androiddebugkey
Contraseña de la clave android

La documentación de Google los publica: «The default password for the debug keystore is android», con el alias androiddebugkey.

Por qué no debe usarse en producción, en orden de gravedad:

  1. La contraseña es pública y el fichero se filtra solo. Cualquiera que consiga el debug.keystore —de un repositorio, de una imagen de CI— firma actualizaciones de esa aplicación.
  2. Las tiendas lo rechazan. Literal de developer.android.com: «Because the debug certificate is created by the build tools and is insecure by design, most app stores (including the Google Play Store) do not accept apps signed with a debug certificate for publishing».
  3. No es la misma clave en dos máquinas, así que ni siquiera sirve para actualizar una aplicación desde otro ordenador.

⚠️ En esta máquina no existe ~/.android/debug.keystore —nunca se ha ejecutado Android Studio—, así que la tabla de arriba procede de la documentación de Google y no se ha podido comprobar en local. Tampoco se ha verificado el periodo de validez que le asigna la versión actual de las herramientas.

10. Recetas

Herramientas: apksigner 0.9 y zipalign de build-tools 37.0.0, keytool de OpenJDK 21.0.10, xxd y python3 3.12. Ejecutado el 12 de agosto de 2026. Todo sobre copias en el scratchpad; el corpus se lee y no se toca. Para el uso general de estas herramientas, ver Firma y empaquetado.

10.1 Crear un keystore de prueba

SCR=/tmp/scratch/firma
keytool -genkeypair -v -keystore $SCR/pruebas.jks -storetype JKS -alias pruebas \
  -keyalg RSA -keysize 2048 -validity 10000 \
  -storepass pruebas -keypass pruebas \
  -dname "CN=Ejemplo Test, OU=Doc, O=Ejemplo, L=Madrid, ST=Madrid, C=ES"
Generando par de claves RSA de 2.048 bits para certificado autofirmado (SHA384withRSA) con
una validez de 10.000 días
	para: CN=Ejemplo Test, OU=Doc, O=Ejemplo, L=Madrid, ST=Madrid, C=ES
[Almacenando /tmp/scratch/firma/pruebas.jks]

Warning:
El almacén de claves JKS utiliza un formato propietario. Se recomienda migrar a PKCS12, que
es un formato estándar del sector que utiliza "keytool -importkeystore -srckeystore
pruebas.jks -destkeystore pruebas.jks -deststoretype pkcs12".

Con -storetype PKCS12 no aparece el aviso y -keypass sobra. Los 10.000 días de validez llevan el certificado hasta 2053, muy por encima del umbral de Play. keytool sale en el idioma del sistema; aquí, en español.

10.2 Firmar una copia y verificarla

CORPUS=~/corpus-apk
cp $CORPUS/org.fossify.calculator_1.4.0.apk $SCR/copia.apk
$BT/apksigner sign --ks $SCR/pruebas.jks --ks-key-alias pruebas \
  --ks-pass pass:pruebas --key-pass pass:pruebas \
  --v4-signing-enabled true --out $SCR/firmado.apk $SCR/copia.apk
ls -la $SCR/firmado.apk*
$BT/apksigner verify --verbose --print-certs $SCR/firmado.apk | head -12
-rw-r--r--@ 1 jj  wheel  7176562 12 ago.  12:25 firmado.apk
-rw-r--r--@ 1 jj  wheel    62985 12 ago.  12:25 firmado.apk.idsig

Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
…v3.1 y v3.2: false…
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
V3.0 Signer: certificate DN: CN=Ejemplo Test, OU=Doc, O=Ejemplo, L=Madrid, ST=Madrid, C=ES
V3.0 Signer: certificate SHA-256 digest: d14efe3056cabee10e58c84110c4d0d1df324544bc57645d3de8fc53fa895caa

v4: false con el .idsig recién generado al lado. No es un fallo: apksigner no busca el fichero por convención de nombre, hay que dárselo:

$BT/apksigner verify --verbose --v4-signature-file $SCR/firmado.apk.idsig $SCR/firmado.apk \
  | sed -n '1p;7p'
Verifies
Verified using v4 scheme (APK Signature Scheme v4): true

Es el error de diagnóstico más frecuente con v4: concluir que la firma no se generó cuando lo que falta es el argumento.

10.3 Leer el .idsig

El volcado hexadecimal está descompuesto campo a campo en la sección 2.3. El recorrido programático son quince líneas:

python3 - <<'PY'
import struct
b = open("/tmp/scratch/firma/firmado.apk.idsig", "rb").read()
p = 0
ver, = struct.unpack_from("<I", b, p); p += 4
hl,  = struct.unpack_from("<I", b, p); p += 4
hi = b[p:p+hl]; p += hl
sl,  = struct.unpack_from("<I", b, p); p += 4 + sl
tl,  = struct.unpack_from("<I", b, p); p += 4
salt, = struct.unpack_from("<I", hi, 5)
rl,   = struct.unpack_from("<I", hi, 9 + salt)
print("version", ver, "hashingInfo", hl, "signingInfos", sl, "verityTree", tl)
print("hashAlgorithm", struct.unpack_from("<I", hi, 0)[0], "log2BlockSize", hi[4],
      "bloque", 2**hi[4], "salt", salt)
print("rawRootHash", hi[13+salt:13+salt+rl].hex())
print("offset del arbol", p, "bytes restantes", len(b) - p)
PY
version 2 hashingInfo 45 signingInfos 1484 verityTree 61440
hashAlgorithm 1 log2BlockSize 12 bloque 4096 salt 0
rawRootHash 404cd0b2fa6a3258fe42ac647409b0586f9395ae5018a9f1ab58024d060a60f0
offset del arbol 1545 bytes restantes 61440

Los 61.440 bytes del árbol son los que predice el cálculo de la sección 3.2.

10.4 Comprobar que la firma detecta un cambio de un solo byte

cp $SCR/firmado.apk $SCR/tocado.apk
python3 -c "
b = bytearray(open('/tmp/scratch/firma/tocado.apk','rb').read())
b[1000000] ^= 0x01
open('/tmp/scratch/firma/tocado.apk','wb').write(b)"
$BT/apksigner verify --verbose $SCR/tocado.apk
DOES NOT VERIFY
ERROR: APK Signature Scheme v3 signer #1: APK integrity check failed. CHUNKED_SHA256 digest
mismatch. Expected: <b7628a525424c06e29230c37fedf07f8b0e3fd19c2b3886ea23dee7b8ef10ee6>,
actual: <29c86f47c4b775bea6692b2a93da805698c39b7c4c776a9fc845b64edeecfe67>

Un bit invertido en un byte perdido en medio del fichero. El Expected es el mismo valor que el apkDigest del .idsig de la sección 10.3, que es el anclaje de la sección 4.

Fuentes

  1. APK signature scheme v4 — Android Open Source Project — https://source.android.com/docs/security/features/apksigning/v4 Consultado el 12 de agosto de 2026. De aquí salen la descripción del esquema, la estructura del .idsig de la sección 2.2 con sus citas literales sobre versión, algoritmo, tamaño de bloque y sal, la exigencia «A v4 signature requires a complementary v2 or v3 signature» de la sección 4 y la relación exacta con fs-verity de la sección 3.3.
  2. AOSP — tools/apksig/.../internal/apk/v4/V4Signature.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java Consultado el 12 de agosto de 2026. De aquí salen las tablas de campos de la sección 2.2: CURRENT_VERSION, HASHING_ALGORITHM_SHA256, LOG2_BLOCK_SIZE_4096_BYTES, MAX_SIGNING_INFOS_SIZE y el orden exacto de los campos de HashingInfo y SigningInfo.
  3. AOSP — tools/apksig/.../internal/apk/v4/V4SchemeSigner.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java Consultado el 12 de agosto de 2026. De aquí salen el comentario sobre el árbol como campo opcional con prefijo de longitud, la elección del apkDigest entre v3 y v2 de la sección 4 y el comentario «Salt has to stay empty for fs-verity compatibility».
  4. AOSP — tools/apksig/.../internal/apk/v4/V4SchemeVerifier.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java Consultado el 12 de agosto de 2026. De aquí salen los cuatro objetivos del verificador de v4 citados en la sección 5.4.
  5. AOSP — tools/apksig/.../internal/util/VerityTreeBuilder.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java Consultado el 12 de agosto de 2026. De aquí salen CHUNK_SIZE = 4096, el algoritmo calculateLevelOffset con su redondeo a 4096 de la sección 3.2, la exigencia de alineación de generateVerityTreeRootHash y el comentario sobre la regeneración del árbol en el dispositivo.
  6. AOSP — tools/apksig/.../ApkVerifier.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/ApkVerifier.java Consultado el 12 de agosto de 2026. De aquí sale íntegro el flujo de la sección 5.1, con las condiciones exactas de cada esquema, los comentarios citados en 5.2, la comprobación de coincidencia entre firmantes v1 y v2, la de targetSandboxVersion y la búsqueda de la entrada stamp-cert-sha256.
  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 el nombre de la entrada stamp-cert-sha256, el identificador 0x6dff800d y la descripción del atributo de marca de tiempo 0xe43c5946 de la sección 8.
  8. fs-verity — documentación del kernel de Linux — https://docs.kernel.org/filesystems/fsverity.html Consultado el 12 de agosto de 2026. De aquí salen la definición de fs-verity, la construcción del árbol de Merkle citada en la sección 3.1, el relleno con ceros del último bloque, el tratamiento de la sal y la observación de que el digest se calcula sobre el fsverity_descriptor y no sobre la raíz sola.
  9. apksigner — Android Studio — https://developer.android.com/tools/apksigner Consultado el 12 de agosto de 2026. De aquí salen las opciones --v4-signing-enabled, --v4-signature-file, --ks-pass y --key-pass usadas en la sección 10.
  10. Sign your app — Android Studio — https://developer.android.com/studio/publish/app-signing Consultado el 12 de agosto de 2026. De aquí salen la ruta del debug keystore, la cita sobre por qué las tiendas no lo aceptan, el requisito de validez hasta después del 22 de octubre de 2033 y la recomendación de 25 años de la sección 9.
  11. Client authentication — Google Play services — https://developers.google.com/android/guides/client-auth Consultado el 12 de agosto de 2026. De aquí salen la contraseña android y el alias androiddebugkey del debug keystore de la sección 9.1. Se usa esta fuente porque la página de firma de aplicaciones no publica la contraseña.
  12. Corpus de verificación~/corpus-apk Medido el 12 de agosto de 2026. De aquí salen el source stamp de Google Keep de la sección 8, el lineage de la sección 7, el porcentaje de source stamp sobre 717 APK y el APK de origen de todas las salidas de la sección 10.