Módulo 6: Supply Chain Sbom And Signing
7. Manos a la obra: rompiendo la cadena a propósito
Descripción
Verified OK de la lección 6 confirma que la firma funciona cuando nada cambia. Esta lección prueba la otra mitad de la garantía, la que de verdad importa en un incidente real: ¿qué pasa cuando sí cambia? Vas a modificar lambda/function.zip después de firmarlo —un solo byte agregado al final del archivo, nada más— y correr cosign verify-blob contra esa versión alterada. La firma va a fallar, en vivo, con un mensaje de error explícito. Y vas a descubrir, en el camino, un hecho incómodo y real: el archivo .zip alterado sigue siendo un .zip perfectamente válido, que unzip abre sin ninguna queja — la firma criptográfica detecta algo que la validación de formato del propio .zip no detecta en absoluto.
Conexión con el módulo
Esta lección es la contraparte necesaria de la lección 6: una firma que nunca se ha visto fallar no es una firma en la que confiar, es una firma en la que se espera que funcione. La lección 8 va a usar exactamente este mismo mecanismo —cosign verify-blob devolviendo un código de salida distinto de cero— como la condición que detiene un apply dentro del pipeline heredado.
Paso 1 — Confirmando el estado limpio de partida
Antes de romper nada, confirma que manifest.sig de la lección 6 sigue verificando correctamente contra lambda/function.zip sin modificar — el punto de referencia contra el que vas a comparar el resto de esta lección:
cosign verify-blob --key cosign.pub --bundle manifest.sig --insecure-ignore-tlog=true lambda/function.zip
echo "exit: $?"
Qué esperar (literal):
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
Verified OK
exit: 0
Este es el resultado que ya viste en la lección 6. A partir de aquí, cualquier cambio en el resultado tiene una sola causa posible: lo que le hagas al archivo, no algo intermitente de la herramienta.
Paso 2 — Modificando el artefacto después de firmado, un solo byte
Trabaja sobre una copia —nunca sobre el archivo real que ya firmaste, para no perder el estado limpio de la lección 6— y agrégale un solo byte nulo al final:
cp lambda/function.zip lambda/function.zip.tampered
printf '\x00' >> lambda/function.zip.tampered
ls -la lambda/function.zip lambda/function.zip.tampered
Qué esperar (literal, ejecutado para escribir esta lección):
-rw-r--r-- 890 lambda/function.zip
-rw-r--r-- 891 lambda/function.zip.tampered
Un byte de diferencia — 890 frente a 891. No reescribiste handler.py, no volviste a comprimir nada, no tocaste el contenido que Lambda ejecutaría al descomprimir el archivo: solo agregaste un byte al final del .zip ya construido. Es, deliberadamente, la alteración más pequeña posible — si la firma detecta esto, detecta cualquier cosa más grande también.
Paso 3 — El hallazgo incómodo: el .zip alterado sigue siendo un .zip válido
Antes de verificar la firma, vale la pena confirmar algo que sorprende la primera vez: el formato .zip, por su propia estructura interna, tolera bytes extra al final del archivo sin considerarlo corrupto. Confírmalo:
unzip -t lambda/function.zip.tampered
Qué esperar (literal, ejecutado para escribir esta lección):
Archive: lambda/function.zip.tampered
testing: handler.py OK
No errors detected in compressed data of lambda/function.zip.tampered.
Cero errores. unzip abre el archivo, descomprime handler.py, y lo reporta como íntegro — porque, técnicamente, lo es: el formato .zip guarda su índice completo (el End of Central Directory) y busca esa estructura desde el final del archivo hacia atrás; un byte extra después de esa estructura no rompe nada de lo que unzip necesita para funcionar. Si subieras este .zip alterado a Lambda tal cual, la función probablemente seguiría desplegándose y ejecutándose sin ningún error visible — el mismo handler.py, byte por byte, sigue adentro, sin cambios.
LO QUE unzip VE LO QUE cosign VE
──────────────── ─────────────────
[ZIP header] [handler.py comprimido] [ZIP header] [handler.py comprimido]
[central directory] [EOCD] [central directory] [EOCD] [byte extra]
↑
unzip busca el EOCD desde el final cosign hashea el ARCHIVO COMPLETO,
y lo encuentra — reporta "OK" byte por byte, sin ninguna excepción
Esta es exactamente la razón por la que este módulo existe, dicha con la evidencia más concreta posible: la validez de formato de un archivo no es lo mismo que la integridad de su contenido. Trivy y Checkov (Módulo 5) pueden decirte si tu configuración de infraestructura sigue buenas prácticas; unzip -t puede decirte si un .zip está bien formado; ninguno de los dos puede decirte si ese archivo es, exactamente, el que alguien con autoridad aprobó. Esa es, únicamente, la pregunta que una firma responde.
Paso 4 — Verificando la firma contra el archivo alterado
cosign verify-blob --key cosign.pub --bundle manifest.sig --insecure-ignore-tlog=true lambda/function.zip.tampered
echo "exit: $?"
Qué esperar (literal, ejecutado para escribir esta lección):
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
Error: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
error during command execution: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
exit: 1
Este es el mensaje real de esta versión de cosign — un poco distinto, en su redacción exacta, de un simple "Error: invalid signature" genérico, y vale la pena leerlo con precisión en vez de memorizar una frase aproximada: invalid signature when validating ASN.1 encoded signature — ASN.1 (Abstract Syntax Notation One) es el formato estándar en el que se codifican los dos números que componen una firma ECDSA; cosign está diciendo, con precisión, que decodificó correctamente la estructura de la firma (no es un archivo manifest.sig corrupto o malformado), pero que al aplicar la operación matemática de verificación contra el hash del archivo que le pasaste, esa firma no corresponde a ese hash. Y el código de salida —1, no 0— es la señal que cualquier script o job de CI necesita para detener lo que sigue.
Paso 5 — Por qué esto ocurre: el hash cambió, la firma no pudo cambiar con él
La razón técnica exacta, sin ninguna magia: cosign sign-blob no firma "el .zip" en un sentido abstracto — firma el hash SHA-256 del contenido completo del archivo, byte por byte, en el momento exacto de la firma. Confírmalo tú mismo:
shasum -a 256 lambda/function.zip | awk '{print $1}' | xxd -r -p | base64
shasum -a 256 lambda/function.zip.tampered | awk '{print $1}' | xxd -r -p | base64
Qué esperar (literal, ejecutado para escribir esta lección):
mX7XF7LjI00UJ+DFC6+/IXEoh/Cdsb9EOPAWXwmwn1U=
Ol85F3RXsWjhOQoIQzwvGKB5r5oqMDNVWaKlxDPq/6E=
Un byte de diferencia en el archivo produjo un hash completamente distinto —no "parecido", no "casi igual"— una propiedad fundamental de cualquier función hash criptográfica bien diseñada, llamada efecto avalancha: el cambio más pequeño posible en la entrada produce una salida totalmente distinta e impredecible, nunca una salida "cercana" a la original. manifest.sig contiene una firma calculada sobre el primer hash (mX7XF7...); cuando cosign verify-blob recalcula el hash del archivo que le diste (Ol85F3..., sobre el .zip alterado) y lo compara contra lo que la firma matemáticamente garantiza, los dos no coinciden, y la verificación falla — exactamente el mecanismo que el Paso 4 acabas de observar en vivo.
Paso 6 — Restaurando el estado correcto
rm lambda/function.zip.tampered
cosign verify-blob --key cosign.pub --bundle manifest.sig --insecure-ignore-tlog=true lambda/function.zip
Qué esperar (literal — de vuelta al resultado del Paso 1):
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
Verified OK
lambda/function.zip, el archivo real que este módulo firmó, nunca se tocó — solo trabajaste sobre una copia (function.zip.tampered), que borras aquí. andes-cargo-infra/ queda, al cerrar esta lección, exactamente en el mismo estado correcto en el que la lección 6 lo dejó.
Una pregunta honesta: ¿podría un atacante simplemente volver a firmar el archivo alterado?
Vale la pena responder, con precisión, la objeción más razonable que esta lección puede generar: si alguien logra alterar function.zip en algún punto de la cadena, ¿no podría simplemente generar una firma nueva sobre el archivo alterado, y hacerla pasar como legítima? La respuesta, con la misma honestidad que gobierna esta guía, tiene dos partes. No, no sin cosign.key: firmar un archivo nuevo requiere la llave privada, que el Paso 5 de la lección 5 excluyó explícitamente del repositorio con .gitignore — un atacante que solo tenga acceso al código fuente de andes-cargo-infra/ (por ejemplo, a través de una fuga del repositorio de Git) no tiene esa llave, y no puede producir una firma válida sobre ningún archivo, alterado o no. Sí, en cambio, si el atacante también compromete la máquina o el proceso que sí tiene acceso a cosign.key —por ejemplo, un runner de CI comprometido con la llave cargada como secreto—: en ese escenario, la firma por sí sola no protege nada, porque el atacante puede firmar de verdad con la llave legítima. Este es, precisamente, el motivo por el que la firma de este módulo es una capa de defensa dentro de un sistema más amplio, no una bala de plata aislada: protege contra la alteración del artefacto después de que sale del proceso de construcción confiable, pero no reemplaza la necesidad de proteger ese proceso de construcción en sí —el mismo principio de mínimo privilegio e identidad federada que los Módulos 2 y 3 ya construyeron para el propio pipeline.
Errores comunes
Confundir el error de firma inválida con un archivo .zip corrupto (de diagnóstico, el error central que esta lección previene). Qué pasa: alguien ve Error: failed to verify signature y asume que el .zip en sí está dañado o mal formado, e intenta "repararlo" con herramientas de reparación de archivos ZIP. Cómo detectarlo: si tu primer instinto frente a este error es correr unzip -t o una herramienta de reparación, en vez de preguntarte si el archivo cambió desde que se firmó. Cómo corregirlo: el Paso 3 de esta lección ya lo demostró — un .zip puede estar perfectamente bien formado (unzip -t reporta cero errores) y aun así fallar la verificación de firma. Son dos preguntas completamente distintas: "¿es este un archivo .zip válido?" (respondida por unzip) y "¿es este exactamente el archivo que alguien firmó?" (respondida solo por cosign verify-blob). Un .zip corrupto de verdad fallaría en unzip -t primero; un .zip bien formado pero alterado después de firmarse solo falla en la verificación de firma.
Pensar que el mensaje "ASN.1 encoded signature" indica un problema con el formato de manifest.sig, no con el contenido de function.zip (de lectura del mensaje de error). Qué pasa: alguien lee "ASN.1" —un término poco familiar— y sospecha que el archivo de firma en sí está corrupto o mal generado. Cómo detectarlo: si tu primer paso de diagnóstico es volver a generar manifest.sig desde cero, antes de comprobar si function.zip cambió. Cómo corregirlo: como explicó el Paso 4, ASN.1 es solo el formato de codificación estándar de los números de una firma ECDSA — el mensaje dice que cosign decodificó la firma correctamente, pero que el resultado de la verificación matemática no coincide con el archivo que le diste. La causa casi siempre está en el archivo que se está verificando, no en la firma misma —a menos que manifest.sig también se haya alterado, un caso que produciría un error distinto, de parseo del bundle, no de firma inválida.
**Asumir que la firma protege el contenido "lógico" del .zip" (los archivos que contiene) en vez del archivo binario completo (de modelo mental incorrecto).** Qué pasa: alguien asume que cosignde alguna forma entiende la estructura interna del.zip—qué archivos contiene, con qué contenido— y firma eso, de forma que reordenar archivos internos sin cambiar su contenido no rompería la firma. Cómo detectarlo: si esperas que dos.zipcon el mismohandler.pypero comprimidos con parámetros distintos (por ejemplo, un nivel de compresión diferente) verifiquen contra la misma firma. Cómo corregirlo:cosign sign-blob/verify-blobno tienen ningún conocimiento del formato.zip` — tratan el archivo como una secuencia arbitraria de bytes ("blob", el nombre del propio comando lo dice) y firman el hash de esos bytes exactos, sin importar qué formato representen. Cualquier cambio en el archivo binario resultante —incluida una recompresión con parámetros distintos, aunque el contenido lógico sea idéntico— produce un hash distinto y rompe la verificación, exactamente como un byte extra al final la rompió en esta lección.
Ejercicios
Ejercicio 1 — Predice el resultado si alteraras cosign.pub en vez de lambda/function.zip. Sin ejecutarlo, ¿esperarías el mismo mensaje de error del Paso 4, uno distinto, o que el comando fallara de una forma completamente diferente?
Ver solución
Un mensaje distinto, aunque relacionado. Alterar cosign.pub —por ejemplo, cambiando un solo carácter dentro del bloque PEM— probablemente produciría un error de parseo de la llave pública (algo como "failed to parse public key" o similar), no el error de "invalid signature" del Paso 4, porque cosign fallaría antes incluso de llegar a la operación matemática de verificación: no podría interpretar el archivo como una llave ECDSA válida en absoluto. Si el cambio fuera lo suficientemente sutil como para seguir siendo una llave ECDSA válida mátemáticamente (pero distinta de la original usada para firmar), el resultado se parecería más al Paso 4: una verificación que corre completa, pero falla, porque la llave pública ya no corresponde a la privada que produjo la firma original.
Ejercicio 2 — Explica, con tus propias palabras, por qué unzip -t reportando "cero errores" sobre un archivo con un byte extra NO es un bug de unzip. Un compañero, sorprendido por el resultado del Paso 3, pregunta si unzip debería considerar eso un archivo corrupto. ¿Qué le responderías?
Ver solución
No es un bug — es una consecuencia razonable del diseño del formato .zip, que fue pensado para tolerar cierto grado de datos adicionales (por ejemplo, un .zip autoextraíble tiene, deliberadamente, un ejecutable pegado antes de la estructura ZIP real, y sigue siendo un .zip perfectamente válido para cualquier herramienta que lo abra). El formato busca su índice (End of Central Directory) desde el final del archivo, y mientras esa búsqueda encuentre la estructura esperada, el resto se ignora. unzip -t responde correctamente la pregunta que le corresponde ("¿puedo extraer el contenido declarado sin error?") — simplemente esa no es la misma pregunta que "¿es este archivo, byte por byte, idéntico al que alguien aprobó?", que es exactamente la pregunta que esta lección demostró que solo una firma criptográfica responde.
Ejercicio 3 — Diseña, en prosa, un tercer escenario de manipulación distinto al de esta lección, y predice si cosign verify-blob lo detectaría. En vez de agregar un byte al final, imagina que alguien reemplaza handler.py dentro del .zip por una versión maliciosa, pero recomprime el archivo de forma que el .zip resultante tenga exactamente el mismo tamaño en bytes que el original (890 bytes). ¿Cambiaría eso el resultado de la verificación?
Ver solución
No, el resultado sería el mismo: Error: failed to verify signature. El tamaño en bytes del archivo alterado es irrelevante para la verificación — lo que importa es el hash SHA-256 sobre el contenido binario completo, y prácticamente cualquier cambio en el contenido (agregar un byte, cambiar una sola línea de handler.py y recomprimir, o cualquier otra alteración) produce, por el efecto avalancha ya explicado en el Paso 5, un hash completamente distinto — sin importar si el tamaño resultante coincide por casualidad con el original. Este ejercicio es, de hecho, el escenario de manipulación más realista y peligroso de los dos: un atacante sofisticado que intente ocultar su cambio manteniendo el tamaño del archivo constante seguiría siendo detectado con la misma certeza que el byte extra, mucho más obvio, de esta lección.
Resumen y siguiente paso
En esta lección rompiste la cadena de confianza a propósito, de la forma más pequeña posible —un solo byte agregado al final de lambda/function.zip después de firmarlo— y confirmaste, con la salida literal de cosign verify-blob, que la verificación falla con un mensaje explícito y un código de salida distinto de cero. Descubriste, con evidencia real, que el propio formato .zip no detecta esta alteración (unzip -t sigue reportando "cero errores"), lo que hace que la firma criptográfica sea la única capa de este módulo capaz de responder la pregunta que de verdad importa: ¿es este, exactamente, el artefacto que alguien aprobó? Y respondiste, con honestidad, hasta dónde llega esa protección: contra alteración después del punto de firma, sí; contra un atacante que también tuviera acceso a cosign.key, no —una razón más para que la identidad del Módulo 2 y los secretos del Módulo 3 sigan siendo necesarios, no reemplazados por este módulo.
Antes de avanzar deberías poder: explicar por qué un .zip alterado puede seguir siendo válido según unzip y aun así fallar la verificación de firma; leer el mensaje invalid signature when validating ASN.1 encoded signature sin confundirlo con un archivo corrupto; y defender, con precisión, hasta dónde llega la garantía de una firma y dónde termina.
La lección 8 integra todo lo que este módulo construyó —SBOM, keypair, firma, y esta prueba de que la verificación de verdad detecta manipulación— en un job verify-artifact dentro del apply.yml heredado, cerrando TM-02 de RISK-MAP.md.
Recursos
- Sigstore — Signing Blobs — la misma referencia de la lección 6, con la sección de verificación y sus posibles resultados de error.
- NIST — Secure Hash Standard (FIPS 180-4) — la especificación formal de SHA-256, el algoritmo detrás del efecto avalancha que esta lección demostró en el Paso 5.
- PKWARE — .ZIP File Format Specification — la especificación del formato ZIP, la fuente de por qué el índice
End of Central Directoryse busca desde el final del archivo, la razón técnica exacta del Paso 3.