Esquemas de firma v1, v2, v3 y v3.1

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

1. Qué es y por qué existe

Android no tiene una tienda única obligatoria ni una autoridad de certificación. Un APK puede llegar al dispositivo por Play, por F-Droid, por un adb install o por un fichero copiado a mano. Lo único que el sistema puede exigir en todos esos caminos es que el paquete venga firmado, y esa firma es la que sostiene tres reglas del modelo de seguridad:

  1. Una actualización solo puede sustituir a una aplicación firmada con la misma clave. Es la propiedad que impide que otra persona publique una «actualización» de tu aplicación.
  2. Los permisos de nivel signature se conceden entre aplicaciones firmadas con la misma clave. Es lo que permite que un paquete hable con otro sin pedir permiso al usuario.
  3. El sharedUserId y los datos de la instalación previa están atados a la clave.

Conviene separar dos cosas que se confunden todo el tiempo:

  • Integridad del contenido. ¿Han cambiado los bytes desde que se firmó? Esto la firma lo responde de forma dura y verificable.
  • Autenticidad de origen. ¿Quién firmó? Esto la firma no lo responde en Android. El certificado es autofirmado: sujeto y emisor son la misma entidad, no hay cadena hasta una CA y nadie ha validado que «CN=FDroid» sea F-Droid. Lo que el sistema comprueba es continuidad: que la clave sea la misma que la de la instalación previa. La identidad criptográfica de una aplicación en Android es el hash de su certificado, no su nombre.

Un cuarto punto que se malinterpreta con la misma frecuencia: una firma válida no dice nada sobre lo que hace la aplicación. Un APK con malware firmado correctamente verifica igual de bien que uno legítimo. La firma acota quién puede sustituirlo, no qué contiene.

Este documento cubre los esquemas que viven dentro del APK: v1 en META-INF/ y v2, v3 y v3.1 en el APK Signing Block. El esquema v4, que vive fuera del fichero, y el algoritmo completo de verificación están en Firma v4 y verificación.

Endianness: todas las estructuras binarias descritas aquí son little-endian. Los ficheros de v1 son de texto y se tratan como tales.

2. La línea del tiempo

Esquema Introducido en Dónde vive Qué añade
v1 (JAR signing) Android 1.0 META-INF/MANIFEST.MF, *.SF, *.RSA Firma por entrada
v2 Android 7.0 (API 24) Bloque 0x7109871a Firma del fichero completo
v3 Android 9 (API 28) Bloque 0xf05368c0 Rango de SDK y rotación de clave
v3.1 Android 13 (API 33) Bloque 0x1b93ad61 Segmentación de la rotación por SDK
v4 Android 11 (API 30) Fichero .idsig aparte Árbol de Merkle para instalación incremental

Los números de API salen de AndroidSdkVersion.java de apksig, que los usa como umbrales reales: MIN_SDK_WITH_V3_SUPPORT = 28, MIN_SDK_WITH_V31_SUPPORT = 33.

Hay una sexta entrada que no está documentada públicamente y que aparece en el binario local: apksigner 0.9 de las build-tools 37.0.0 conoce una constante APK_SIGNATURE_SCHEME_V32_BLOCK_ID = 0x70e1c89f con MIN_SDK_WITH_V32_SUPPORT = 37, además de un HYBRID_MIN_SDK_VERSION_ATTR_ID = 0xbf940529. Su salida ya imprime una línea Verified using v3.2 scheme (APK Signature Scheme v3.2). ⚠️ sin verificar: estas constantes se han extraído con javap -constants del lib/apksigner.jar local; no están en la rama main pública de platform/tools/apksig ni existe página de v3.2 en source.android.com. No se ha encontrado ningún APK del corpus que lleve ese bloque.

3. v1: JAR signing

El esquema v1 es el de los ficheros JAR de Java, adoptado tal cual. Firma entradas del ZIP, no el fichero. Son tres artefactos encadenados dentro de META-INF/.

┌───────────────────────┐   digest de cada entrada
│ META-INF/MANIFEST.MF  │◄──────────────────────────── contenido del APK
└──────────┬────────────┘
           │ digest del manifiesto entero
           ▼
┌───────────────────────┐
│ META-INF/CERT.SF      │
└──────────┬────────────┘
           │ firma PKCS#7 sobre los bytes del .SF
           ▼
┌───────────────────────┐
│ META-INF/CERT.RSA     │  ← certificado + firma
└───────────────────────┘

El nombre CERT es una convención, no una regla: se llama como el firmante quiera. En el corpus aparecen CIARANG.SF/CIARANG.RSA (F-Droid), 84D3000E.SF (Termux), BNDLTOOL.RSA (bundletool) y APKMIRRO.RSA (los contenedores de APKMirror).

3.1 META-INF/MANIFEST.MF

Fichero de texto con la sintaxis de manifiesto de JAR: una sección principal y luego una sección individual por entrada firmada, separadas por línea en blanco. La especificación de Oracle lo fija así:

«A JAR file manifest consists of a main section followed by a list of sections for individual JAR file entries, each separated by a newline. […] Each section must start with an attribute with the name as Name, and the value must be a relative path to the file».

Extracto real de org.fdroid.fdroid_1023052.apk:

Manifest-Version: 1.0

Name: AndroidManifest.xml
SHA-256-Digest: VzVLUwh1uIb6DILVlqTtFkHCrgnVyNtOSJfojGviGs0=

Name: DebugProbesKt.bin
SHA-256-Digest: rooILdYJMTg1upPD70eaOvmQOXfXvcgd9Fu9IJ7H0No=

Name: META-INF/androidx/constraintlayout/constraintlayout-core/LICENSE
 .txt
SHA-256-Digest: gJ+h7SFFD1mCfR6a7HILvEtodDT6Iig8bLXdgqR6ucA=

Tres detalles que un parser tiene que respetar:

  • El digest es del contenido descomprimido de la entrada, en Base64.
  • Ninguna línea puede pasar de 72 bytes en su forma UTF-8, y la continuación se marca con un espacio inicial. En el ejemplo, …LICENSE y .txt son una sola línea lógica. Un parser que corte por \n sin volver a unir las continuaciones lee mal el nombre.
  • El propio MANIFEST.MF no se lista, y no todas las entradas tienen por qué estarlo.

Ese fichero mide 101.577 bytes en un APK de 12 MB: 3.355 líneas de texto que hay que leer, parsear y comparar entrada por entrada.

3.2 META-INF/CERT.SF

Mismo formato, un nivel por encima. Su sección principal lleva el digest del manifiesto completo, y sus secciones individuales el digest de la sección correspondiente del manifiesto, no del fichero.

Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: AiZKqwFleFu9Xz1B+NwdN3CWlhjuSjVIAdgJCl5zE0w=
X-Android-APK-Signed: 2, 3

Name: AndroidManifest.xml
SHA-256-Digest: 1/HJZFJmZVWda6RdwNGkYNda+yehcFvuqlhvAjTSxnI=
Atributo Sección Sobre qué se calcula
x-Digest-Manifest principal El fichero MANIFEST.MF entero
x-Digest-Manifest-Main-Attributes principal Solo la sección principal del manifiesto
x-Digest individual La sección del manifiesto con ese Name, bytes incluidos
X-Android-APK-Signed principal Extensión de Android, no de Oracle

Los dos primeros existen para poder validar rápido: si x-Digest-Manifest cuadra, el verificador se ahorra recorrer las secciones una a una. Si no cuadra —porque alguien añadió una entrada al manifiesto sin firmarla— se cae a la comprobación sección a sección, que es la que permite que un JAR firmado admita entradas nuevas sin invalidar la firma. Esa tolerancia, deliberada en el mundo Java, es una debilidad en el mundo Android.

X-Android-APK-Signed es la protección contra el downgrade. Su valor es la lista de esquemas superiores con los que también se firmó el APK. En el corpus: X-Android-APK-Signed: 2, 3 en F-Droid (v1+v2+v3) y X-Android-APK-Signed: 2 en Briar (v1+v2). Un atacante que quite el APK Signing Block para que el sistema caiga a v1 deja esta línea intacta —está firmada— y el verificador la ve y rechaza el fichero. La sección 9.3 lo demuestra sobre un APK real.

El -Manifest-Main-Attributes no aparece en ninguno de los .SF del corpus: apksigner no lo escribe. La especificación de Oracle dice que «its nonexistence does not affect JAR file verification».

3.3 META-INF/CERT.RSA

Un mensaje SignedData de CMS (RFC 5652), en DER, con la firma sobre los bytes del .SF y el certificado del firmante. La extensión depende del algoritmo de la clave: .RSA para RSA y RSASSA-PSS, .DSA para DSA, .EC para EC y EdDSA.

SCR=/tmp/scratch   # copia de trabajo, ver sección 9
openssl asn1parse -inform DER -in $SCR/v1/META-INF/CIARANG.RSA | head -12
openssl pkcs7 -inform DER -in $SCR/v1/META-INF/CIARANG.RSA -print_certs -noout
    0:d=0  hl=4 l=1346 cons: SEQUENCE
    4:d=1  hl=2 l=   9 prim: OBJECT            :pkcs7-signedData
   15:d=1  hl=4 l=1331 cons: cont [ 0 ]
   19:d=2  hl=4 l=1327 cons: SEQUENCE
   23:d=3  hl=2 l=   1 prim: INTEGER           :01
   26:d=3  hl=2 l=  15 cons: SET
   28:d=4  hl=2 l=  13 cons: SEQUENCE
   30:d=5  hl=2 l=   9 prim: OBJECT            :sha256
…
subject=C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks
issuer=C=UK, ST=Unknown, L=Wetherby, O=Unknown, OU=Unknown, CN=Ciaran Gultnieks

subject e issuer idénticos: el certificado es autofirmado, como se anunció en la sección 1. El fichero completo mide 1.350 bytes frente a los 101.704 del .SF que firma.

3.4 Las debilidades estructurales de v1

No son bugs de implementación: son consecuencias del modelo «firmar entradas».

Debilidad Consecuencia
No cubre el fichero, cubre entradas Todo lo que no sea contenido de una entrada queda fuera: los bytes antes del primer Local File Header, los extra field, el Central Directory, el EOCD y el comentario final
Ignora las entradas no listadas Se puede añadir una entrada nueva al APK sin tocar la firma. El MANIFEST.MF no dice «estas y solo estas»
El orden y la ambigüedad del ZIP quedan fuera Nombres duplicados, cabeceras discrepantes, offsets: el ZIP los permite y v1 no los ve
Es lento de verificar Hay que descomprimir cada entrada, calcular su digest y compararlo contra el texto del manifiesto. En un APK de 700 entradas son 700 descompresiones
META-INF/ se autoexcluye Las entradas bajo META-INF/ que no estén en el manifiesto no están protegidas, y apksigner avisa de cada una

Esa última fila produce los avisos que inundan la salida de apksigner sobre cualquier APK moderno firmado con v1:

WARNING: META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler not protected by
signature. Unauthorized modifications to this JAR entry will not be detected. Delete or
move the entry outside of META-INF/.

Briar acumula 62 avisos de esos. No son un fallo del APK: son la descripción exacta de lo que v1 no cubre.

La primera fila es la que se explotó de verdad. Janus (CVE-2017-13156) anteponía un DEX malicioso al APK completo: las entradas del ZIP quedaban intactas, v1 las validaba una a una, y el runtime cargaba el DEX del principio del fichero. El detalle histórico está en la sección 12 de Contenedor ZIP. v2 lo cierra por construcción, y ese es exactamente su motivo de existir.

4. v2: firma del fichero completo

v2 abandona el modelo de entradas. Divide el APK en cuatro tramos y firma un digest calculado sobre los bytes, no sobre el contenido lógico:

┌──────────────────────────────────────────────────┐
│ 1. contenido de las entradas ZIP                 │  ◄── cubierto
│    (offset 0 hasta el inicio del Signing Block)  │
├══════════════════════════════════════════════════┤
│ 2. APK Signing Block                             │  ◄── NO cubierto
├──────────────────────────────────────────────────┤
│ 3. ZIP Central Directory                         │  ◄── cubierto
├──────────────────────────────────────────────────┤
│ 4. ZIP End of Central Directory                  │  ◄── cubierto, con excepción
└──────────────────────────────────────────────────┘

La especificación lo formula así: «APK signature scheme v2 protects the integrity of sections 1, 3, 4, and the signed data blocks of the APK signature scheme v2 block».

4.1 Las dos exclusiones y por qué son necesarias

El tramo 2 no se cubre por la razón obvia: el digest vive dentro de él. Lo que sí se protege es el signed data de cada firmante, que va firmado aparte con su propia clave; el resto del bloque —otros pares ID-valor, el relleno— queda fuera. Consecuencia práctica: se pueden añadir bloques nuevos al APK Signing Block sin invalidar v2, y eso es precisamente lo que hacen el source stamp, el bloque de dependencias de Google Play y el frosting (ver APK Signing Block, sección 5).

Del tramo 4 se excluye un campo concreto: offset of start of central directory, en el offset 0x10 del EOCD. Es inevitable. Ese campo apunta al Central Directory, y al insertar el APK Signing Block entre los datos y el Central Directory el valor cambia. Si estuviese cubierto, firmar sería imposible: el digest dependería de un campo que la propia inserción del digest modifica.

La solución no es ignorar el campo, sino normalizarlo: al calcular y al verificar, se sustituye por el offset donde empieza el APK Signing Block. ApkSigningBlockUtils.java lo hace literalmente antes de digerir:

// For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
// treated as though its Central Directory offset points to the start of APK Signing Block.
ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());

Así el valor queda determinado por el tamaño del tramo 1, que sí está cubierto, y no hay grado de libertad que explotar. Un verificador que se limite a «saltar 4 bytes» en vez de normalizarlos deja pasar cualquier valor en ese campo.

4.2 Qué gana y qué cuesta

v1 v2
Qué cubre Contenido de las entradas listadas Todos los bytes salvo la exclusión de 4.1
Añadir una entrada Posible sin romper la firma Imposible
Prefijo arbitrario (Janus) Posible Imposible
Coste de verificar Una descompresión por entrada Un recorrido lineal del fichero
Reempaquetar sin resignar A veces funciona Nunca funciona

Esa última fila es la que hace que la sección 4 de Alineación y zipalign exista: con v2, mover un solo byte invalida la firma, y por tanto alinear después de firmar es un error garantizado.

4.3 La protección contra el stripping, desde el otro lado

El bloque v2 lleva un atributo STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d cuyo valor es el número del esquema superior con el que también se firmó. Medido sobre el base.apk de Google Keep y sobre Termux, el valor es 3:

        attribute 0xbeeff00d STRIPPING_PROTECTION  valor=4 B
          valor = 3

Es el hermano binario del X-Android-APK-Signed de la sección 3.2, y cumple el mismo papel un nivel más arriba: quitar el bloque v3 de un APK que tiene v2 y v3 no cuela, porque el v2 —que sigue validando— declara que debía haber un v3. La demostración está en la sección 9.3.

5. v3: rango de SDK y rotación de clave

v3 es v2 con dos añadidos. La documentación oficial lo resume en una frase: «V3 adds information about the supported SDK versions and a proof-of-rotation struct to the APK signing block».

5.1 minSdkVersion y maxSdkVersion en el signer

Cada firmante declara el rango de versiones de plataforma en el que aplica. Los valores aparecen dos veces: dentro del signed data, donde están protegidos por la firma, y fuera, a nivel de signer, «used to skip verification of this signature if the current platform is not in range». La copia de fuera es una optimización; la de dentro es la autoridad. Un verificador que solo lea la copia de fuera acepta un rango manipulado.

Volcado real del base.apk de Google Keep:

  bloque v3.1: valor=5589 B, secuencia de signers=5585 B
    signer: minSdkVersion=33 maxSdkVersion=2147483647
      …
        minSdkVersion=33 maxSdkVersion=2147483647  (copia firmada)
  bloque v3: valor=1763 B, secuencia de signers=1759 B
    signer: minSdkVersion=24 maxSdkVersion=32
      …
        minSdkVersion=24 maxSdkVersion=32  (copia firmada)

Los dos rangos son contiguos y no se solapan: [24, 32] para el bloque v3 y [33, 2147483647] para el v3.1. Eso es exactamente la rotación segmentada de la sección 6.

5.2 El lineage de rotación

El lineageglosario— es la cadena de rotación de certificados: acredita que una clave nueva sucede legítimamente a una anterior.

El problema que resuelve. Antes de v3, cambiar la clave de firma de una aplicación era imposible en la práctica: la instalación previa está atada a la clave anterior, así que un APK firmado con una clave nueva se rechaza como si fuese de otro autor. Y si la clave se ha filtrado, o se ha perdido, o vive en un HSM que hay que retirar, no había salida.

Cómo lo resuelve. El bloque v3 admite un atributo PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c que contiene una lista enlazada simple de certificados, del más antiguo al más reciente, donde cada nodo está firmado por la clave del nodo anterior. El sistema, al ver un APK firmado con una clave que no conoce, recorre el lineage: si encuentra en él el certificado que sí tiene asociado a la instalación, acepta la sucesión.

Formato exacto, según V3SigningCertificateLineage.java:

uint32                     version del lineage (1)
secuencia de nodos, cada uno con prefijo uint32 de longitud:
  ├─ bytes con prefijo: signed data
  │    ├─ bytes con prefijo: certificado X.509 en DER
  │    └─ uint32           : id del algoritmo con el que este nodo firma al siguiente
  ├─ uint32               : flags de capacidad
  ├─ uint32               : id del algoritmo de la firma que sigue
  └─ bytes con prefijo    : firma del nodo anterior sobre este signed data

El primer nodo no lleva firma (no hay nodo anterior): su campo de firma mide 0 bytes.

Las capacidades. Cada nodo lleva un uint32 de flags que dice qué sigue pudiendo hacer el certificado antiguo una vez rotada la clave. Los nombres y los valores salen de SigningCertificateLineage.java:

Flag Valor Comentario literal del código Qué significa
PAST_CERT_INSTALLED_DATA 1 «accept data from already installed pkg with this cert» Sin esto, la actualización pierde los datos de la instalación anterior
PAST_CERT_SHARED_USER_ID 2 «accept sharedUserId with pkg with this cert» Mantiene el sharedUserId con paquetes aún firmados con la clave vieja
PAST_CERT_PERMISSION 4 «grant SIGNATURE permissions to pkgs with this cert» Mantiene los permisos de nivel signature frente a terceros no rotados
PAST_CERT_ROLLBACK 8 «Enable updates back to this certificate» Peligroso: permite volver a la clave vieja
PAST_CERT_AUTH 16 «Preserve authenticator module-based access in AccountManager» Conserva el acceso a las cuentas del AccountManager

El código advierte explícitamente sobre ROLLBACK: «this effectively removes any benefit of signing certificate changes, since a compromised key could retake control of an app even after change». Por eso el valor por defecto que construye apksig es INSTALLED_DATA | PERMISSION | SHARED_USER_ID | AUTH = 0x17, con ROLLBACK apagado.

Y eso es exactamente lo que lleva el lineage real de Google Keep:

          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

Dos nodos: una clave RSA de 2048 bits que firma a su sucesora de 4096 bits con una firma de 256 bytes. El nodo 1 declara el algoritmo (0x0103) con el que firmará al siguiente y no lleva firma propia; el nodo 2 lleva la firma y no declara sucesor.

6. v3.1: la rotación segmentada por SDK

6.1 Por qué se separó de v3

El problema es de compatibilidad hacia atrás. Un dispositivo con Android 9 a 12 entiende el bloque v3, así que si el desarrollador rota la clave y escribe el nuevo firmante en el bloque v3, ese dispositivo intenta usar la clave nueva. Pero las plataformas anteriores a Android 13 tienen defectos conocidos en el tratamiento de la rotación, y el resultado eran fallos de actualización. La documentación lo dice sin rodeos: «The v3.1 scheme addresses some of the known issues with APK Signature Scheme v3 regarding rotation».

La solución es de una simplicidad brutal: el mismo formato, otro identificador de bloque.

«The v3.1 signing block has the same contents as the v3 signing block, but with the new block ID these signatures are recognized only on devices running Android 13 and higher.»

Un Android 12 no conoce 0x1b93ad61, así que lo ignora igual que ignora el relleno. Un Android 13 lo busca primero. Nada más hace falta.

6.2 Cómo conviven los dos bloques

En el mismo APK, a la vez, con rangos de SDK disjuntos:

APK Signing Block
├── 0x1b93ad61  v3.1 → firmante ROTADO   , minSdk=33, maxSdk=INT_MAX
├── 0xf05368c0  v3   → firmante ORIGINAL , minSdk=24, maxSdk=32
│                      + attr 0x559f8b02 ROTATION_MIN_SDK_VERSION = 33
└── 0x7109871a  v2   → firmante ORIGINAL
                       + attr 0xbeeff00d STRIPPING_PROTECTION = 3
  • Android 13 o superior usa el firmante rotado del bloque v3.1.
  • Android 12 o inferior ignora el bloque v3.1 y usa el firmante original del bloque v3.
  • Android 7.0 a 8.1 ignora los dos y usa el bloque v2.

El bloque v3 lleva además un atributo de protección: ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02. Su valor es el minSdkVersion que debería declarar el bloque v3.1. El comentario de V3SchemeConstants.java explica la regla:

«If this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version for rotation in the v3.1 signing block is not X, then the APK should be rejected.»

En Keep vale exactamente 33, y el bloque v3.1 declara minSdkVersion=33. Cuadra. Quitar el bloque v3.1 para forzar que un Android 13 use la clave vieja no funciona, porque el bloque v3 —que sigue validando— exige que exista.

Y hay una regla simétrica en ApkVerifier.java: un bloque v3.1 sin bloque v3 es un error, Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK, «as a v3.1 signature is intended to support key rotation on T+ with the v3 signature containing the original signing key».

7. Qué esquema exige o admite cada nivel de API

Nivel de API v1 v2 v3 v3.1 v4
1 – 23 Único posible Ignorado Ignorado Ignorado Ignorado
24 – 27 (7.0–8.1) Admitido Preferido Ignorado Ignorado Ignorado
28 – 29 (9–10) Admitido Admitido Preferido Ignorado Ignorado
30 – 32 (11–12L) Insuficiente si targetSdk ≥ 30 Admitido Preferido Ignorado Admitido
33 y superiores (13+) Insuficiente si targetSdk ≥ 30 Admitido Admitido Preferido Admitido

Reglas duras que hay detrás de la tabla:

  • targetSdkVersion ≥ 30 obliga a v2 o superior. Literal de developer.android.com: «Apps that target Android 11 (API level 30) that are currently only signed using APK Signature Scheme v1 must now also be signed using APK Signature Scheme v2 or higher. Users can't install or update apps that are only signed with APK Signature Scheme v1 on devices that run Android 11».
  • minSdkVersion < 24 obliga a v1. No hay alternativa: esas plataformas no miran el APK Signing Block.
  • «Preferido» significa que los inferiores no se comprueban. Si hay un esquema superior válido para el rango, el sistema no verifica los de abajo. Eso es lo que produce la salida de la sección 9.1, donde un APK con bloque v2 presente sale como v2: false.
  • Un esquema superior presente obliga a que valide. Si el bloque v3 está y no verifica, el APK se rechaza; no se cae a v2. ApkVerifier lo hace devolviendo el resultado en cuanto result.containsErrors(). El detalle está en Firma v4 y verificación, sección 4.

8. Por qué un APK moderno lleva v1+v2+v3 (y cuándo se puede omitir v1)

La recomendación oficial es acumular: «sign apps with all schemes, first with v1, then v2, and then v3». El motivo es que cada esquema lo entiende un tramo de versiones distinto y ninguno es un superconjunto del otro en cobertura de dispositivos.

apksigner decide qué esquemas aplica a partir de --min-sdk-version y --max-sdk-version, y por defecto toma el minSdkVersion del manifiesto:

«By default, apksigner uses the value of the minSdkVersion attribute from the app's manifest file.»

v1 se puede omitir cuando minSdkVersion ≥ 24, porque a partir de ahí todo dispositivo entiende v2. Firmarlo igualmente no molesta —solo engorda el APK con el MANIFEST.MF— pero tampoco aporta nada. En el corpus se ve el reparto exacto:

APK minSdkVersion v1 v2 v3
org.briarproject.briar.android_10517.apk 21
org.fdroid.fdroid_1023052.apk 23
com.termux_1002.apk 24
com.google.android.keep.apk (base) 32 ✅ (bloque presente) ✅ + v3.1

Ojo con la última fila: Keep tiene bloque v2, pero apksigner informa v2: false porque con minSdkVersion=32 ese bloque nunca se usa. Que un esquema no se verifique no significa que no esté. Es la distinción entre el binario y el veredicto, y confundirlas es el error más frecuente al leer la salida de apksigner.

9. Recetas

Herramientas: apksigner 0.9 y aapt2 2.20-15087165 de build-tools 37.0.0, unzip y openssl 3.6.3 del sistema. Ejecutado el 12 de agosto de 2026 sobre el corpus. Para el uso general de estas herramientas, ver Firma y empaquetado.

9.1 Veredicto por esquema sobre varios APK

BT=~/Library/Android/sdk/build-tools/37.0.0
CORPUS=~/corpus-apk
$BT/apksigner verify --verbose --print-certs $CORPUS/com.termux_1002.apk
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
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme 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=FDroid, OU=FDroid, O=fdroid.org, L=ORG, ST=ORG, C=UK
V3.0 Signer: certificate SHA-256 digest: 228fb2cfe90831c1499ec3ccaf61e96e8e1ce70766b9474672ce427334d41c42
V3.0 Signer: key algorithm: RSA
V3.0 Signer: key size (bits): 2048
…

v1: false con los ficheros META-INF/84D3000E.SF y .RSA presentes en el APK: no es que falten, es que minSdkVersion=24 y apksigner no los comprueba. Y el v3.2 de la quinta línea es la constante indocumentada de la sección 2.

Sobre el base.apk de Google Keep, extraído del .apks del corpus, aparece el caso completo:

SCR=/tmp/scratch
unzip -o -q $CORPUS/splits/com.google.android.keep.apks -d $SCR/keep_x
$BT/apksigner verify --verbose --print-certs $SCR/keep_x/com.google.android.keep.apk
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): false
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
Verified for SourceStamp: true
Number of signers: 1
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate DN: CN=Android, …
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) key size (bits): 4096
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate DN: CN=Android, …
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) key size (bits): 2048
Source Stamp Signer: certificate DN: CN=Android, …
Source Stamp Timestamp: 1786040112

Dos claves distintas para dos rangos de SDK disjuntos: la rotación de la sección 6 en un APK de producción.

9.2 El veredicto depende del rango de SDK que se le pase

Es el mismo fichero en las cuatro invocaciones:

for m in 21 24 33; do
  echo "--- min-sdk-version $m"
  $BT/apksigner verify --min-sdk-version $m --verbose $SCR/keep_x/com.google.android.keep.apk \
    | head -6
done
$BT/apksigner verify --min-sdk-version 24 --max-sdk-version 32 --verbose \
  $SCR/keep_x/com.google.android.keep.apk | head -6
--- min-sdk-version 21
DOES NOT VERIFY
ERROR: Missing META-INF/MANIFEST.MF
--- min-sdk-version 24
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
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
--- min-sdk-version 33
Verified using v2 scheme (APK Signature Scheme v2): false
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
--- min-sdk 24, max-sdk 32
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false

Cuatro veredictos distintos sobre los mismos bytes. Con --min-sdk-version 21 el APK no verifica, porque una plataforma de API 21 exigiría v1 y aquí no hay MANIFEST.MF. Con --max-sdk-version 32 el bloque v3.1 desaparece del veredicto, porque ninguna plataforma del rango lo mira. «¿Verifica este APK?» es una pregunta mal formulada; la buena es «¿verifica para qué rango de plataformas?».

9.3 La protección contra el downgrade, demostrada

Se quita el bloque v3 del APK conservando el v2 y el mismo tamaño total del APK Signing Block, ajustando el relleno, para que ningún otro byte se mueva y la firma v2 siga siendo aritméticamente correcta. Todo sobre una copia en el scratchpad.

# firmado.apk es una copia de org.fossify.calculator_1.4.0.apk firmada en la sección 9 de
# 03-firma-v4-y-verificacion.md; sin_v3.apk es el mismo fichero sin el par 0xf05368c0
python3 dump_sigblock.py $SCR/firma/sin_v3.apk
$BT/apksigner verify --verbose $SCR/firma/sin_v3.apk
== sin_v3.apk
   inicio del bloque : 7057408   (multiplo de 4096: True)
   size (cabeza/cola): 4088 / 4088   coinciden: True
   offset del CD     : 7061504   total del bloque: 4096
   0x7109871a       1536 bytes  v2 (APK_SIGNATURE_SCHEME_V2_BLOCK_ID)
   0x42726577       2504 bytes  padding (VERITY_PADDING_BLOCK_ID)

DOES NOT VERIFY
ERROR: APK Signature Scheme v2 signer #1: APK Signature Scheme v2 signature 0 indicates the
APK is signed using APK Signature Scheme v3 but no such signature was found. Signature
stripped?

El digest v2 cuadra —no se ha movido un byte fuera del bloque— y aun así el APK se rechaza, porque el atributo 0xbeeff00d de la sección 4.3 dice que tenía que haber un v3. Es el mismo mecanismo que el X-Android-APK-Signed de v1, un piso más arriba.

9.4 Ver los ficheros de v1

unzip -o -q $CORPUS/org.fdroid.fdroid_1023052.apk 'META-INF/*' -d $SCR/v1
ls $SCR/v1/META-INF/ | grep -E '\.(SF|RSA)$|MANIFEST'
head -8 $SCR/v1/META-INF/MANIFEST.MF
head -5 $SCR/v1/META-INF/CIARANG.SF
CIARANG.RSA
CIARANG.SF
MANIFEST.MF
Manifest-Version: 1.0

Name: AndroidManifest.xml
SHA-256-Digest: VzVLUwh1uIb6DILVlqTtFkHCrgnVyNtOSJfojGviGs0=

Name: DebugProbesKt.bin
SHA-256-Digest: rooILdYJMTg1upPD70eaOvmQOXfXvcgd9Fu9IJ7H0No=
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: AiZKqwFleFu9Xz1B+NwdN3CWlhjuSjVIAdgJCl5zE0w=
X-Android-APK-Signed: 2, 3

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 las cuatro secciones del APK de la sección 4, la cita sobre qué protege v2, y la tabla de identificadores de algoritmo de firma que se amplía en el documento 02.
  2. APK signature scheme v3 — Android Open Source Project — https://source.android.com/docs/security/features/apksigning/v3 Consultado el 12 de agosto de 2026. De aquí salen la cita «V3 adds information about the supported SDK versions…», el identificador 0x3ba06f8c del proof-of-rotation y la descripción de minSDK/maxSDK de la sección 5.1.
  3. APK signature scheme v3.1 — Android Open Source Project — https://source.android.com/docs/security/features/apksigning/v3-1 Consultado el 12 de agosto de 2026. De aquí sale íntegra la sección 6: el motivo de la separación, la cita «same contents as the v3 signing block, but with the new block ID» y el reparto por versión de Android.
  4. Application signing — Android Open Source Project — https://source.android.com/docs/security/features/apksigning Consultado el 12 de agosto de 2026. De aquí salen las versiones de introducción de v1, v2 y v3 de la sección 2, la recomendación de firmar con todos los esquemas de la sección 8 y la explicación del rechazo de firmas v2+ eliminadas.
  5. 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 APK_SIGNATURE_SCHEME_V3_BLOCK_ID, APK_SIGNATURE_SCHEME_V31_BLOCK_ID, ROTATION_MIN_SDK_VERSION_ATTR_ID y su comentario citado en la sección 6.2, y los umbrales MIN_SDK_WITH_V3_SUPPORT y MIN_SDK_WITH_V31_SUPPORT.
  6. AOSP — tools/apksig/.../SigningCertificateLineage.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/SigningCertificateLineage.java Consultado el 12 de agosto de 2026. De aquí sale íntegra la tabla de capacidades de la sección 5.2 con sus valores y sus comentarios literales, incluida la advertencia sobre PAST_CERT_ROLLBACK.
  7. AOSP — tools/apksig/.../internal/apk/v3/V3SigningCertificateLineage.java — https://android.googlesource.com/platform/tools/apksig/+/refs/heads/main/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java Consultado el 12 de agosto de 2026. De aquí sale el formato binario del lineage de la sección 5.2, tomado del comentario // FORMAT (little endian):.
  8. 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í sale la normalización del offset del Central Directory en el EOCD de la sección 4.1, con su comentario literal.
  9. 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í salen las condiciones exactas bajo las que se verifica cada esquema (secciones 7 y 8) y el error V31_BLOCK_FOUND_WITHOUT_V3_BLOCK de la sección 6.2.
  10. 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í sale STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d de la sección 4.3.
  11. JAR File Specification — Oracle, Java SE 21 — https://docs.oracle.com/en/java/javase/21/docs/specs/jar/jar.html Consultado el 12 de agosto de 2026. De aquí salen la estructura del manifiesto y del .SF de las secciones 3.1 y 3.2, el límite de 72 bytes por línea con su regla de continuación, las extensiones .RSA/.DSA/.EC y el procedimiento de validación en cuatro pasos.
  12. RFC 5652 — Cryptographic Message Syntax (CMS) — https://www.rfc-editor.org/rfc/rfc5652.html Consultado el 12 de agosto de 2026. Standards Track, septiembre de 2009. Es la especificación del SignedData (sección 5.1 del RFC) que contiene el fichero .RSA de la sección 3.3.
  13. Behavior changes: Apps targeting Android 11 — https://developer.android.com/about/versions/11/behavior-changes-11 Consultado el 12 de agosto de 2026. De aquí sale citada la obligación de firmar con v2 o superior a partir de targetSdkVersion 30 (sección 7).
  14. apksigner — Android Studio — https://developer.android.com/tools/apksigner Consultado el 12 de agosto de 2026. De aquí salen la descripción de --min-sdk-version y la regla de que apksigner decide los esquemas a partir del rango de SDK (sección 8).
  15. 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 las constantes de v3.2 de la sección 2, que no aparecen en la rama pública de apksig.
  16. Corpus de verificación~/corpus-apk Medido el 12 de agosto de 2026. De aquí salen los extractos de MANIFEST.MF y .SF, los volcados de bloques v2, v3 y v3.1, la tabla de la sección 8 y todas las salidas de la sección 9.