Esquemas de firma v1, v2, v3, v3.1 y v3.2
Antes conviene leer «Contenedor ZIP»
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:
- 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.
- Los permisos de nivel
signaturese conceden entre aplicaciones firmadas con la misma clave. Es lo que permite que un paquete hable con otro sin pedir permiso al usuario. - El
sharedUserIdy 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 |
| v3.2 | Android 17 (API 37) | Bloque 0x70e1c89f |
Firma híbrida: un firmante clásico y otro poscuántico |
| v4 | Android 11 (API 30) | Fichero .idsig aparte |
Árbol de Merkle para instalación incremental |
Los umbrales son constantes de V3SchemeConstants.java de apksig, definidas con los niveles de
AndroidSdkVersion.java: MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P (28),
MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T (33) y
MIN_SDK_WITH_V32_SUPPORT = AndroidSdkVersion.C (37).
v3.2 es la pieza para la transición a la criptografía poscuántica, y la documenta
source.android.com (página actualizada el 29 de julio de 2026). El bloque 0x70e1c89f tiene
el formato de v3.0, pero lleva exactamente dos firmantes con el mismo rango de SDK: uno con
un algoritmo clásico —cualquiera de los de v3: RSA, ECDSA o DSA— y otro con ML-DSA-65 o
ML-DSA-87. El verificador exige los dos. Android 16 y anteriores ignoran el bloque, así que, para
instalarse en ellos, el APK tiene que llevar además un bloque v3.0 o v3.1 firmado con una sola
clave clásica; Android 17 acepta uno que solo lleve el bloque híbrido. Para que nadie pueda quitar
el bloque híbrido y quedarse con el clásico, los firmantes v3.0 y v3.1 llevan el rango de SDK del
bloque híbrido en dos atributos, 0xbf940529 (mínimo, obligatorio) y 0x9f06b79c (máximo,
opcional): Android 17 los compara con el bloque v3.2 y rechaza el APK si falta o no coincide, y
Android 16 y anteriores los ignoran. Las constantes están en V3SchemeConstants.java de apksig
en la etiqueta android-17.0.0_r1, no en la rama main, que está congelada desde marzo de 2025,
y apksigner 0.9 de las build-tools 37.0.0 ya firma y verifica v3.2. El detalle está en la
«sección 7 · v3.2: la firma híbrida». No se ha encontrado ningún APK del
«muestrario» 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
muestrario 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,
…LICENSEy.txtson una sola línea lógica. Un parser que corte por\nsin volver a unir las continuaciones lee mal el nombre. - El propio
MANIFEST.MFno 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 muestrario:
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 10.4 · Ver los ficheros de v1»
enseña esa línea en un .SF real, y la 10.3 demuestra el mismo mecanismo un piso más arriba,
con el atributo del bloque v2.
El -Manifest-Main-Attributes no aparece en ninguno de los .SF del muestrario: 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 10
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 · Qué es y por qué existe». 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 |
Solo ignora las entradas no listadas de META-INF/ |
Añadir una entrada normal sí se detecta: apksigner responde No digest for res/evil.png in META-INF/MANIFEST.MF. Pero isJarEntryDigestNeededInManifest exime a todo lo que empiece por META-INF/ o acabe en /, así que ahí sí se puede añadir sin tocar la firma. Es la misma puerta de la última fila |
| 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 · Nota histórica: Master Key y Janus». 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 → §4 · Identificadores de bloque conocidos»).
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 · El orden importa: alinear ANTES de firmar» 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 · META-INF/CERT.SF», 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 10.3 · La protección contra el downgrade, demostrada».
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 · v3.1: la rotación segmentada por SDK».
5.2 El lineage de rotación
El lineage —«glosario»— 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 el nodo anterior firmó este
├─ uint32 : flags de capacidad
├─ uint32 : id del algoritmo con el que este nodo firma al siguiente
└─ 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. Los dos algoritmos se encadenan: el de fuera de un nodo tiene que coincidir con el de dentro del siguiente —la spec v3 lo dice así: «must match the one from the signed data section in the next level»—, y es el que se usa para verificar la firma de ese siguiente.
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. v3.2: la firma híbrida
Android 17 (API 37) añade un tercer bloque a la familia v3 para la transición a la criptografía
poscuántica. El APK se firma a la vez con una clave clásica y con una ML-DSA, y el
verificador exige las dos: para falsificarlo habría que romper los dos algoritmos, y ML-DSA se
despliega sin apostarlo todo a un algoritmo recién estandarizado. El CDD de Android 17 lo hace
obligatorio (requisito 4 [C-0-2]: «MUST support verifying ".apk" files using APK Signature
Scheme v3.2, […]»); en AOSP depende del flag android.security.apk_pqc_hybrid_signing, que la
configuración cp2a de android-17.0.0_r1 —la de aosp_current— deja ENABLED y
READ_ONLY. Android 16 y anteriores no conocen 0x70e1c89f y se lo saltan.
7.1 El bloque 0x70e1c89f
El formato es el de v3.0 sin un byte distinto: lo escribe el mismo V3SchemeSigner con otro
identificador, y cada firmante tiene la estructura de la «sección 5 de
APK Signing Block · Estructura interna de un bloque v2 o v3». Cambian tres reglas:
- Exactamente dos firmantes, uno clásico y otro ML-DSA.
apksigescribe primero el clásico, pero el orden no importa: cada uno se clasifica por la clave de su primer certificado, que es PQC si su algoritmo esML-DSAo uno de los OID de ML-DSA. - El mismo rango de SDK en los dos, en la copia firmada y en la de fuera.
lineageen los dos, con la misma historia salvo el último nodo («sección 7.4 · Rotación implícita y ROLLBACK»).
| Firmante | Algoritmo | Digest de contenido | Clave |
|---|---|---|---|
| Clásico | El que v3 elige para la clave: 0x0103/0x0104 (RSA), 0x0201/0x0202 (ECDSA), 0x0301 (DSA) |
CHUNKED_SHA256 o CHUNKED_SHA512 |
RSA, EC o DSA |
| PQC | 0x0501 ML_DSA |
CHUNKED_SHA512 |
ML-DSA-65 o ML-DSA-87 |
ML-DSA-65 y ML-DSA-87 comparten el identificador 0x0501. El conjunto de parámetros va en
el OID del SubjectPublicKeyInfo: 2.16.840.1.101.3.4.3.18 y 2.16.840.1.101.3.4.3.19. En el
dispositivo la clave la decodifica Conscrypt, que solo admite esos dos: «Only ML-DSA-65 and
ML-DSA-87 are currently supported». apksig no lo comprueba y firma y verifica con ML-DSA-44.
Los atributos que protegen el bloque no van dentro de él, sino en otros firmantes:
| ID | Nombre en apksig |
Dónde | Valor |
|---|---|---|---|
0xbf940529 |
HYBRID_MIN_SDK_VERSION_ATTR_ID |
Firmantes v3.0 y v3.1 | int32 LE: SDK mínimo del bloque v3.2 |
0x9f06b79c |
HYBRID_MAX_SDK_VERSION_ATTR_ID |
Firmantes v3.0 y v3.1 | int32 LE: SDK máximo del bloque v3.2 |
0xc2a6b3ba |
SIGNER_TARGETS_DEV_RELEASE_ATTR_ID |
Firmantes v3.1 y v3.2 | Vacío: apunta a una release en desarrollo |
MIN_SDK_WITH_V32_SUPPORT vale AndroidSdkVersion.C, es decir, 37. Volcado del APK de la
«sección 7.5 · Receta: firmar y verificar con apksigner 37.0.0» (K0, RSA de 2048 bits, en v2 y v3.0; C_K1, EC P-256, y PQC_K1, ML-DSA-65, en v3.2):
0x70e1c89f v3.2 19957 B
firmante #1: 2219 B
digest alg=0x0201 ECDSA_WITH_SHA256 32 B dcd1fb977ea9caa36aefbe65…
minSdkVersion=36 maxSdkVersion=2147483647 (copia firmada)
atributo 0x3ba06f8c PROOF_OF_ROTATION (1530 B)
atributo 0xc2a6b3ba SIGNER_TARGETS_DEV_RELEASE (0 B)
firmante #2: 17726 B
digest alg=0x0501 ML_DSA 64 B c8794620a014c7e0e6707ad8…
certificado X.509 DER: 5588 B
atributo 0x3ba06f8c PROOF_OF_ROTATION (6707 B)
firma alg=0x0501 ML_DSA 3309 B
clave pública SubjectPublicKeyInfo: 1974 B (ML-DSA-65)
0xf05368c0 v3.0 1473 B
atributo 0xbf940529 HYBRID_MIN_SDK_VERSION = 36
El APK sin firmar mide 759 bytes y el firmado 29.006: la firma ML-DSA-65 ocupa 3.309 bytes y
la clave 1.952 (con ML-DSA-87, 4.627 y 2.592), y el certificado PQC va dos veces, en la
secuencia de certificados y al final del lineage. El minSdkVersion=36 con 0xc2a6b3ba y la
falta de 0x9f06b79c son cosa de apksigner 37.0.0 («sección 7.5 · Receta: firmar y verificar con apksigner 37.0.0»).
7.2 Cómo se verifica
Todo lo de esta sección está leído en el código de android-17.0.0_r1. No se ha podido ejecutar
en un Android 17: su imagen de emulador no arranca en la máquina de referencia
(«El muestrario → §8 · La máquina de referencia»).
La plataforma sigue probando v4, v3, v2 y v1, pero dentro de v3
ApkSignatureSchemeV3Verifier.verify prueba ahora v3.2, luego v3.1 y por último v3.0. Los
dos primeros se saltan si no están o si ninguno de sus firmantes cubre el SDK del dispositivo;
el que aplica y falla rechaza el APK con INSTALL_PARSE_FAILED_NO_CERTIFICATES. Cada firmante
del v3.2 pasa la verificación v3 normal de la «sección 5 · v3: rango de SDK y rotación de clave» y, además:
| Comprobación | Mensaje de la plataforma | Issue de apksig |
|---|---|---|
| Dos firmantes | APK Signature Scheme V3.2 supports exactly two signers; N found |
V32_MISSING_DUAL_SIGNERS |
| Uno clásico y uno PQC | …requires one classical and one PQC signer; both are PQC |
V32_INCORRECT_ALGORITHM_PAIR |
| Mismo rango de SDK | …requires both signers target the same SDK range; classical: a-b, PQC: c-d |
V32_SIG_INCONSISTENT_SDK_TARGETING |
| Lineage en los dos, con los mismos certificados | …requires both signers have the same signing history, but […] |
V32_SIG_LINEAGE_MISMATCH_* |
| Mismas capacidades | …but the capabilities diverge at index i |
V32_SIG_LINEAGE_MISMATCH_IN_CAPABILITIES |
| Digests iguales si comparten algoritmo | SHA-512 contents digest does not match the digest specified by a preceding signer |
— |
Superado eso, la integridad se comprueba con todos los digests de los dos —con un clásico
P-256, SHA-256 y SHA-512— y la identidad del paquete pasa a ser el firmante PQC:
SigningDetails guarda sus certificados con la versión menor MINOR_VERSION_32_HYBRID (2).
Un firmante PQC en solitario no vale fuera del bloque híbrido: en v3.0 y v3.1 la plataforma
responde The platform does not currently support single PQC signers in the v3 signature blocks. La documentación extiende la regla a v2, pero el verificador v2 de la etiqueta no tiene
esa comprobación. ⚠️ sin verificar: no se ha probado en un dispositivo un APK cuya única firma
sea un bloque v2 con ML-DSA.
7.3 La protección contra el stripping
El bloque v3.0 o v3.1 que acompaña al híbrido declara sus límites: 0xbf940529 con el
minSdkVersion del v3.2 y 0x9f06b79c con el maxSdkVersion. Android 17 los mira cuando acaba
verificando con v3.0 o v3.1. Si los firmantes del v3.2 quedaban fuera del rango del dispositivo,
compara ese rango con los atributos: Expected a v3.2 signing block targeting min SDK version X, but the v3.2 block was targeting Z, y lo mismo con el máximo. Si no hay bloque v3.2: Expected a v3.2 signing block targeting SDK version X to Y, but a v3.2 block was not found. El máximo es
opcional, pero un máximo sin mínimo es un error.
El máximo existe para que el híbrido pueda dejar de usarse: un APK puede declararlo hasta
la API Y y volver a un firmante único desde Y+1, sin que nadie recorte el rango. Android 16 y
anteriores ignoran estos atributos y no les hace falta más: nunca usaron el bloque v3.2.
Quitándolo del APK de la «sección 7.5 · Receta: firmar y verificar con apksigner 37.0.0», con el relleno ampliado para no mover un byte fuera del
bloque como en la «sección 10.3 · La protección contra el downgrade, demostrada»:
DOES NOT VERIFY
ERROR: APK Signature Scheme v3 signer #1: The v3.0 signer indicates a hybrid signer should be
supported starting from SDK version 36, but a v3.2 block was not found
Con --max-sdk-version 36 el mismo fichero verifica: para Android 16 y anteriores es correcto.
7.4 Rotación implícita y ROLLBACK
El bloque híbrido es una rotación. La plataforma arma el lineage del paquete con la
historia del firmante clásico —que acaba en la clave clásica nueva— y pone detrás el
certificado PQC: [K0, …, C_K1, PQC_K1]. La documentación: «appends the new classical key to
the app's existing signing lineage as the next-to-last key, and treats the new PQC key as the
app's current signing identity». Por eso los dos lineages tienen que coincidir salvo el último
nodo, en certificados y en capacidades. Sin lineage, la plataforma usa [C_K1, PQC_K1] con
las capacidades por defecto, 0x17. Reglas de actualización de SigningDetails (las dos
últimas filas son la regla «New key material»: las claves del híbrido tienen que ser nuevas):
| Instalado | Actualización | Se acepta si |
|---|---|---|
| K0 (v3.0 o v3.1) | K0 + bloque v3.2 | K0 está en los dos lineages híbridos, con INSTALLED_DATA |
| Híbrido | Otro híbrido | C_K1 y PQC_K1 están en los dos lineages nuevos |
| Híbrido | Firmante único nuevo | C_K1 y PQC_K1 están en su lineage |
| Híbrido | C_K1 o PQC_K1 como firmante único | Nunca: reutilización de clave |
| K0 | Híbrido que usa K0 como clave clásica | Nunca: reutilización de clave |
ROLLBACK. Durante el despliegue, la documentación recomienda concedérsela a K0, la clave
clásica original, en el lineage del bloque híbrido, para poder volver a publicar versiones
firmadas solo con K0 si algo falla, y retirarla cuando el despliegue esté asentado. La
actualización firmada con K0 entra entonces por la segunda condición de
PackageManagerServiceUtils.verifySignatures: que la firma instalada conceda ROLLBACK a la
nueva. Dos detalles que no están en la guía:
- Hay que ponerla en los dos lineages, porque las capacidades de K0 tienen que coincidir.
apksignerno lo comprueba al firmar, y luego rechaza el resultado conThe signers in the v3.2 block don't have the same capabilities in their history starting from index 0. - Concedérsela a C_K1 no sirve: volver a una de las claves híbridas es reutilización, tenga
o no
ROLLBACK. La prueba CTS lo dice así: «an update APK signed with one of the hybrid keys should fail the roll back attempt, even if the hybrid key has been granted the ROLLBACK capability».
7.5 Receta: firmar y verificar con apksigner 37.0.0
Hace falta un JDK con ML-DSA, que llegó con Java 24 (JEP 497): el apksigner.jar trae las
clases de Conscrypt pero no su biblioteca nativa, y con Java 21 falla la firma y también la
verificación de un híbrido válido, con ML-DSA KeyFactory not available. OpenSSL 3.5 o
posterior genera la clave, pero en la forma priv-only: con la de por defecto, apksigner
responde Not an RSA, EC, DSA, or ML-DSA private key. Ejecutado el 25 de septiembre de 2026
con zsh, Temurin 25.0.2 y OpenSSL 3.6.3:
BT=~/Library/Android/sdk/build-tools/37.0.0
export PATH=~/.sdkman/candidates/java/25.0.2-tem/bin:$PATH # un JDK 24 o posterior
apks() { "$BT/apksigner" -J-enable-native-access=ALL-UNNAMED "$@"; }
OSSL=/opt/homebrew/opt/openssl@3/bin/openssl
keytool -genkeypair -keystore k0.p12 -storetype PKCS12 -alias k0 -keyalg RSA -keysize 2048 \
-validity 10000 -storepass pruebas -dname "CN=K0 clasica original, O=Ejemplo, C=ES"
keytool -genkeypair -keystore c_k1.p12 -storetype PKCS12 -alias c_k1 -keyalg EC \
-groupname secp256r1 -validity 10000 -storepass pruebas \
-dname "CN=C_K1 clasica hibrida, O=Ejemplo, C=ES"
$OSSL genpkey -algorithm ML-DSA-65 -out pqc_k1.pem
$OSSL req -new -x509 -key pqc_k1.pem -out pqc_k1.crt -days 10000 \
-subj "/CN=PQC_K1 ML-DSA-65/O=Ejemplo/C=ES"
$OSSL pkcs8 -topk8 -nocrypt -in pqc_k1.pem -outform DER -out pqc_k1.pk8 \
-provparam ml-dsa.output_formats=priv-only
apks rotate --out lin_c.bin --old-signer --ks k0.p12 --ks-pass pass:pruebas \
--set-rollback true --new-signer --ks c_k1.p12 --ks-pass pass:pruebas
apks rotate --out lin_p.bin --old-signer --ks k0.p12 --ks-pass pass:pruebas \
--set-rollback true --new-signer --key pqc_k1.pk8 --cert pqc_k1.crt
apks sign --out hibrido.apk --ks k0.p12 --ks-pass pass:pruebas \
--next-signer --hybrid-signer-role classical --ks c_k1.p12 --ks-pass pass:pruebas \
--signer-lineage lin_c.bin \
--next-signer --hybrid-signer-role pqc --key pqc_k1.pk8 --cert pqc_k1.crt \
--signer-lineage lin_p.bin sin_firmar.apk
apks verify -v --print-certs hibrido.apk | grep -E 'v3\.2 scheme|Hybrid .*(DN|algorithm)'
Verified using v3.2 scheme (APK Signature Scheme v3.2): true
V3.2 Hybrid Classical Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) certificate DN: CN=C_K1 clasica hibrida, O=Ejemplo, C=ES
V3.2 Hybrid Classical Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) key algorithm: EC
V3.2 Hybrid PQC Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) certificate DN: C=ES, O=Ejemplo, CN=PQC_K1 ML-DSA-65
V3.2 Hybrid PQC Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) key algorithm: ML-DSA
El primer firmante, sin papel, es K0: firma v1, v2 y v3.0. Los dos con --hybrid-signer-role
forman el bloque v3.2, y cada uno necesita su --signer-lineage: sin él, apksigner 37.0.0
termina con un NullPointerException en DefaultApkSignerEngine.createHybridSignerConfig. Tres
cosas más de esta versión:
- Se compiló con Android 17 aún en desarrollo (
DEV_RELEASE = 37,PROD_RELEASE = 36): por eso escribeminSdkVersion=36con0xc2a6b3bayHYBRID_MIN_SDK_VERSION = 36, y no conoce0x9f06b79cni--hybrid-max-sdk-version. Compilado desde la etiquetaandroid-17.0.0_r1,apksigescribe 37 y los dos atributos. Según el código, Android 17 acepta las dos variantes. apksigner lineage --in hibrido.apkno lee los lineages del bloque v3.2: solo mira v3.1 y v3.0, y aquí respondeThe provided APK does not contain a valid lineage.- Genera el
.idsigde v4 firmado solo con K0, mientras que, según el código, la instalación incremental en Android 17 exige en el.idsigun bloque para0x70e1c89f, queapksigno sabe escribir. ⚠️ sin verificar en un dispositivo.
8. Qué esquema exige o admite cada nivel de API
| Nivel de API | v1 | v2 | v3 | v3.1 | v3.2 | v4 |
|---|---|---|---|---|---|---|
| 1 – 23 | Único posible | Ignorado | Ignorado | Ignorado | Ignorado | Ignorado |
| 24 – 27 (7.0–8.1) | Admitido | Preferido | Ignorado | Ignorado | Ignorado | Ignorado |
| 28 – 29 (9–10) | Admitido | Admitido | Preferido | Ignorado | Ignorado | Ignorado |
| 30 – 32 (11–12L) | Insuficiente si targetSdk ≥ 30 |
Admitido | Preferido | Ignorado | Ignorado | Admitido |
| 33 – 36 (13–16) | Insuficiente si targetSdk ≥ 30 |
Admitido | Admitido | Preferido | Ignorado | Admitido |
| 37 y superiores (17+) | Insuficiente si targetSdk ≥ 30 |
Admitido | Admitido | Admitido | Preferido si su rango lo cubre | Admitido |
Desde la 37, si el APK trae un bloque v3.2 cuyo rango de SDK cubre el del dispositivo, se
verifica ese bloque y ningún otro; si no lo cubre, se cae a v3.1 o v3.0, que comprueban los
atributos contra el stripping («sección 7.3 · La protección contra el stripping»). Los dispositivos anteriores lo ignoran.
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 elAPK 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 10.1 · Veredicto por esquema sobre varios APK», donde un
APKcon bloque v2 presente sale comov2: false. - Un esquema superior presente obliga a que valide. Si el bloque v3 está y no verifica, el
APKse rechaza; no se cae a v2.ApkVerifierlo hace devolviendo el resultado en cuantoresult.containsErrors(). El detalle está en «Firma v4 y verificación → §5.2 · Las reglas, en orden».
9. 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,
apksigneruses the value of theminSdkVersionattribute 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 muestrario 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 | ✅ (ficheros presentes) | ✅ | ✅ |
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.
10. 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 muestrario. Para el uso
general de estas herramientas, ver
«Firma y empaquetado».
10.1 Veredicto por esquema sobre varios APK
BT=~/Library/Android/sdk/build-tools/37.0.0
MUESTRAS=~/muestras-apk
$BT/apksigner verify --verbose --print-certs $MUESTRAS/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 firma híbrida de Android 17 de la «sección 2 · La línea del tiempo».
Sobre el base.apk de Google Keep, extraído del .apks del muestrario, aparece el caso completo:
SCR=/tmp/scratch
unzip -o -q $MUESTRAS/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 · v3.1: la rotación segmentada por SDK» en un
APK de producción.
10.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?».
10.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 10.2 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 · La protección contra el stripping, desde el otro lado» 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.
10.4 Ver los ficheros de v1
unzip -o -q $MUESTRAS/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
- 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
APKde la «sección 4 · v2: firma del fichero completo», la cita sobre qué protege v2, y la tabla de identificadores de algoritmo de firma que se amplía en el «documento 02 · El APK Signing Block». - 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
0x3ba06f8cdelproof-of-rotationy la descripción deminSDK/maxSDKde la «sección 5.1 · minSdkVersion y maxSdkVersion en el signer». - 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 · v3.1: la rotación segmentada por SDK»: 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.
- 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 línea del tiempo», la recomendación de firmar con todos los esquemas de la «sección 9 · Por qué un APK moderno lleva v1+v2+v3 (y cuándo se puede omitir v1)» y la explicación del rechazo de firmas v2+ eliminadas.
- AOSP —
tools/apksig/.../internal/apk/v3/V3SchemeConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java Consultado el 25 de septiembre de 2026. De aquí salenAPK_SIGNATURE_SCHEME_V3_BLOCK_ID,APK_SIGNATURE_SCHEME_V31_BLOCK_ID,ROTATION_MIN_SDK_VERSION_ATTR_IDy su comentario citado en la «sección 6.2 · Cómo conviven los dos bloques», y los umbralesMIN_SDK_WITH_V3_SUPPORTyMIN_SDK_WITH_V31_SUPPORT. - AOSP —
tools/apksig/.../SigningCertificateLineage.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/SigningCertificateLineage.java Consultado el 25 de septiembre de 2026. De aquí sale íntegra la tabla de capacidades de la «sección 5.2 · El lineage de rotación» con sus valores y sus comentarios literales, incluida la advertencia sobrePAST_CERT_ROLLBACK. - AOSP —
tools/apksig/.../internal/apk/v3/V3SigningCertificateLineage.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java Consultado el 25 de septiembre de 2026. De aquí sale el formato binario del lineage de la «sección 5.2 · El lineage de rotación», tomado del comentario// FORMAT (little endian):. - AOSP —
tools/apksig/.../internal/apk/ApkSigningBlockUtils.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java Consultado el 25 de septiembre de 2026. De aquí sale la normalización del offset delCentral Directoryen elEOCDde la «sección 4.1 · Las dos exclusiones y por qué son necesarias», con su comentario literal. - AOSP —
tools/apksig/.../ApkVerifier.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/ApkVerifier.java Consultado el 25 de septiembre de 2026. De aquí salen las condiciones exactas bajo las que se verifica cada esquema (secciones «8» y «9»), el orden v3.2, v3.1 y v3.0 de la «sección 7.2 · Cómo se verifica» y el errorV31_BLOCK_FOUND_WITHOUT_V3_BLOCKde la «sección 6.2 · Cómo conviven los dos bloques». - AOSP —
tools/apksig/.../internal/apk/v2/V2SchemeConstants.java— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java Consultado el 25 de septiembre de 2026. De aquí saleSTRIPPING_PROTECTION_ATTR_ID = 0xbeeff00dde la «sección 4.3 · La protección contra el stripping, desde el otro lado». - 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
.SFde 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/.ECy el procedimiento de validación en cuatro pasos. - 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(apartado 5.1 del RFC) que contiene el fichero.RSAde la «sección 3.3 · META-INF/CERT.RSA». - 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
targetSdkVersion30 («sección 8 · Qué esquema exige o admite cada nivel de API»). - 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-versiony la regla de queapksignerdecide los esquemas a partir del rango de SDK («sección 9 · Por qué un APK moderno lleva v1+v2+v3 (y cuándo se puede omitir v1)»). lib/apksigner.jardebuild-tools37.0.0 — inspeccionado conjavap -constantsel 12 de agosto de 2026. De aquí salieron las constantes de v3.2 de las secciones «2» y «7», antes de encontrarlas en la etiquetaandroid-17.0.0_r1deapksig(fuente 17).- APK Signature Scheme v3.2 — Android Open Source Project — https://source.android.com/docs/security/features/apksigning/v3-2
Consultado el 24 de septiembre de 2026; la página se actualizó el 29 de julio. De aquí salen
el bloque híbrido, los dos firmantes, ML-DSA-65 y ML-DSA-87, la recomendación de mantener un
bloque v3.0 o v3.1 clásico, la rotación implícita, el linaje compartido y la recomendación de
ROLLBACKpara K0 (secciones «2» y «7»). - AOSP —
V3SchemeConstants.javaen la etiquetaandroid-17.0.0_r1— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java Consultado el 24 de septiembre de 2026. De aquí salenMIN_SDK_WITH_V32_SUPPORT = 37y los comentarios de0xbf940529,0x9f06b79cy0xc2a6b3bade las secciones «2» y «7». - AOSP — verificación de firmas de la plataforma en
android-17.0.0_r1— https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java, y en el mismo directorioApkSignatureVerifier.java,ApkSigningBlockUtils.javayApkSignatureSchemeV4Verifier.java. Consultado el 25 de septiembre de 2026. De aquí salen el orden de la «sección 7.2 · Cómo se verifica», sus comprobaciones y mensajes, los atributos de la «sección 7.3 · La protección contra el stripping» y el linaje fusionado de la «sección 7.4 · Rotación implícita y ROLLBACK». - AOSP —
SigningDetails.javayPackageManagerServiceUtils.javaenandroid-17.0.0_r1— https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/content/pm/SigningDetails.java y https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java. Consultado el 25 de septiembre de 2026. De aquí salen las reglas de actualización y de reutilización de claves de la «sección 7.4 · Rotación implícita y ROLLBACK». - AOSP —
HybridSignatureVerificationTest.javade CTS enandroid-17.0.0_r1— https://android.googlesource.com/platform/cts/+/refs/tags/android-17.0.0_r1/hostsidetests/appsecurity/src/android/appsecurity/cts/HybridSignatureVerificationTest.java Consultado el 25 de septiembre de 2026. De aquí salen la instalación de unAPKcon solo el bloque híbrido de la «sección 2 · La línea del tiempo» y la cita sobreROLLBACKde la «sección 7.4 · Rotación implícita y ROLLBACK». - Android 17 Compatibility Definition — https://source.android.com/docs/compatibility/17/android-17-cdd Consultado el 25 de septiembre de 2026; la página se actualizó el 31 de agosto. De aquí sale el requisito 4 [C-0-2] de la «sección 7 · v3.2: la firma híbrida».
- AOSP — el flag
android.security.apk_pqc_hybrid_signing— https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/security/flags.aconfig y https://android.googlesource.com/platform/build/release/+/refs/tags/android-17.0.0_r1/release_config_map.textproto. Consultado el 25 de septiembre de 2026. De aquí sale que v3.2 esté activado enandroid-17.0.0_r1(«sección 7 · v3.2: la firma híbrida»). - AOSP —
apksigenandroid-17.0.0_r1— https://android.googlesource.com/platform/tools/apksig/+/refs/tags/android-17.0.0_r1/src/main/java/com/android/apksig/ (ApkVerifier.java,DefaultApkSignerEngine.java,ApkSigner.java,internal/apk/v3/V3SchemeSigner.javaeinternal/apk/SignatureAlgorithm.java). Consultado el 25 de septiembre de 2026. De aquí salen el formato de la «sección 7.1 · El bloque 0x70e1c89f», losIssuede la «sección 7.2 · Cómo se verifica» y lo que hace la herramienta en la «sección 7.5 · Receta: firmar y verificar con apksigner 37.0.0». - AOSP — Conscrypt en
android-17.0.0_r1— https://android.googlesource.com/platform/external/conscrypt/+/refs/tags/android-17.0.0_r1/common/src/main/java/org/conscrypt/OpenSslMlDsaKeyFactory.java Consultado el 25 de septiembre de 2026. De aquí sale que la plataforma solo admita ML-DSA-65 y ML-DSA-87 («sección 7.1 · El bloque 0x70e1c89f»). - JEP 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm — https://openjdk.org/jeps/497 Consultado el 25 de septiembre de 2026. De aquí sale que ML-DSA llegue al JDK en la versión 24 («sección 7.5 · Receta: firmar y verificar con apksigner 37.0.0»).
apksigner0.9 debuild-tools37.0.0, con Temurin 25.0.2 y OpenSSL 3.6.3 — ejecutado el 25 de septiembre de 2026. De aquí salen el volcado de la «sección 7.1 · El bloque 0x70e1c89f», el rechazo sin el bloque v3.2 de la «sección 7.3 · La protección contra el stripping» y la receta de la «sección 7.5 · Receta: firmar y verificar con apksigner 37.0.0».