Módulo 6: Supply Chain Sbom And Signing
6. Manos a la obra: firmando y verificando el artefacto de despliegue
Descripción
Esta es la lección donde TM-02 se resuelve de verdad: vas a firmar lambda/function.zip —el mismo .zip de 890 bytes que terraform-and-iac-guide generó con data "archive_file"— con la llave privada de la lección 5, y verificar esa firma con la llave pública, completamente offline, sin ningún registro de transparencia público de por medio. Esta lección tiene, además, un valor honesto que vale la pena anunciar desde el principio: la documentación que fundamenta el diseño de este módulo describe unos flags de cosign (--tlog-upload=false, --output-signature) que, al ejecutarlos de verdad contra la versión instalada en esta máquina (v3.1.3), resultan deprecados — cosign cambió su interfaz de línea de comandos entre el momento en que esa documentación se escribió y el momento en que se ejecutó esta lección. Vas a ver el intento fallido, el mensaje real que lo explica, y el comando correcto y actual, en ese orden — exactamente como ocurrió al escribir esta guía.
Conexión con el módulo
Esta es la lección más importante del módulo: manifest.sig, el archivo que produces aquí, es lo que la lección 7 va a intentar romper a propósito, y lo que la lección 8 va a verificar como parte del pipeline. Todo lo que aprendiste en las lecciones 4 y 5 —por qué un keypair local, cómo se generó— converge aquí en el par de comandos que de verdad importan: sign-blob y verify-blob.
Paso 1 — El primer intento, tal como lo describe la documentación de referencia
La forma de firmar sin subir al log de transparencia Rekor, según la documentación oficial de Sigstore consultada al diseñar esta guía, usa el flag --tlog-upload=false al firmar y --output-signature para guardar la firma en un archivo:
COSIGN_PASSWORD="" cosign sign-blob \
--key cosign.key \
--tlog-upload=false \
--output-signature manifest.sig \
lambda/function.zip
Qué esperar (literal, ejecutado para escribir esta lección):
Flag --tlog-upload has been deprecated, prefer using a --signing-config file with no transparency log services
Flag --output-signature has been deprecated, please use --bundle to provide the output bundle location, which will include the signature
Error: must specify --bundle with --new-bundle-format
error during command execution: must specify --bundle with --new-bundle-format
Léelo con la misma atención analítica que ya practicaste con fallos reales de otras herramientas en esta guía: cosign no falla por un error tuyo — falla porque su propia interfaz de línea de comandos evolucionó desde que se escribió la fuente que fundamenta este módulo. v3.1.3 introdujo un nuevo formato de bundle de verificación (versión v0.3 de la especificación de Sigstore) que reemplaza al par --output-signature/--signature por un único archivo --bundle, que contiene tanto la firma como todo el material de verificación en un solo documento JSON estructurado. Esto es, exactamente, la regla dura que gobierna esta guía desde su primera lección: si un comando corrió para escribirla, corrió de verdad — incluido el momento en que el comando "correcto según la documentación" resultó no serlo ya, y hubo que investigar el actual.
Paso 2 — Por qué el flag correcto no basta por sí solo: el intercambio con Rekor por defecto
El mensaje del Paso 1 sugiere agregar --bundle. Probarlo, sin nada más, revela un segundo problema, más sutil e importante:
COSIGN_PASSWORD="" cosign sign-blob \
--key cosign.key \
--bundle manifest.sig \
--yes \
lambda/function.zip
Este comando sí se ejecuta sin error — pero no de la forma que esta guía necesita. Antes de mostrar por qué, vale la pena decir con precisión qué hace por defecto: cosign v3.1.3, con --bundle y sin ninguna otra instrucción, usa --use-signing-config=true (el valor por defecto), que consulta una configuración de firma provista por TUF con las URLs de los servicios públicos de Sigstore —incluido Rekor, el registro de transparencia—, y sube la firma a ese registro público, exactamente el comportamiento keyless que la lección 4 explicó y que esta guía decidió, de forma deliberada, no usar. El .sig/.bundle resultante de ese primer intento incluye un bloque tlogEntries completo —con logIndex, rootHash, y un checkpoint firmado por rekor.sigstore.dev—, la prueba de que la subida ocurrió de verdad contra el servicio público real.
LO QUE cosign sign-blob --bundle HACE POR DEFECTO (v3.1.3)
────────────────────────────────────────────────────────────
[tu comando] → [consulta TUF por la config de firma pública] → [firma] → [sube a Rekor]
│
requiere red real, y
publica la firma para siempre
esto es exactamente el flujo KEYLESS que la lección 4 explicó — y que este módulo NO usa
Este es el momento exacto en que esta guía tuvo que detenerse y resolver el problema con precisión, en vez de aceptar un resultado que contradice su propio diseño: firmar así no es "casi offline" — es una firma keyless real, con una subida real a un servicio público real, aunque el comando use --key cosign.key. La lección 4 ya explicó por qué eso no es lo que este módulo construye.
Paso 3 — La solución real: una configuración de firma explícitamente sin Rekor
cosign v3.1.3 expone un mecanismo para esto — un archivo de configuración de firma (signing config) que puedes generar vacío, sin ningún servicio declarado, y pasarle a sign-blob para que no consulte ni Fulcio, ni Rekor, ni ningún servicio de sellado de tiempo:
cosign signing-config create --out no-tlog-signing-config.json
cat no-tlog-signing-config.json
Qué esperar (literal, ejecutado para escribir esta lección):
{"mediaType":"application/vnd.dev.sigstore.signingconfig.v0.2+json", "rekorTlogConfig":{}, "tsaConfig":{}}
Un documento mínimo, sin ninguna URL de servicio declarada — ni fulcioCertificateAuthorityUrls, ni rekorTlogUrls, ni tsaUrls. Con este archivo pasado explícitamente vía --signing-config, cosign no tiene ningún servicio al que consultar ni subir nada, y el comando de firma queda, ahora sí, completamente local:
COSIGN_PASSWORD="" cosign sign-blob \
--key cosign.key \
--signing-config no-tlog-signing-config.json \
--bundle manifest.sig \
--yes \
lambda/function.zip
Qué esperar (literal, ejecutado para escribir esta lección):
Using payload from: lambda/function.zip
Signing artifact...
Wrote bundle to file manifest.sig
Sin ninguna advertencia sobre datos personales ni transparencia pública —el consentimiento legal que cosign pide antes de subir algo a un servicio hospedado, que no apareció en esta corrida, porque no hay ninguna subida que autorizar—. Compáralo, si quieres confirmarlo con tus propios ojos, con lo que habrías visto en el Paso 2: ese comando sí muestra, antes de firmar, un aviso legal completo sobre el registro público inmutable de Sigstore, que tienes que aceptar escribiendo y (o pasando --yes, como en el Paso 2). La ausencia completa de ese aviso en este Paso 3 es, en sí misma, una confirmación de que no hay ningún servicio hospedado de por medio.
Paso 4 — Confirmando que manifest.sig no contiene ningún registro de Rekor
cat manifest.sig
Qué esperar (el campo mediaType y la estructura son literales; el campo signature es variable — ver la nota debajo):
{
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"verificationMaterial": {
"publicKey": {
"hint": "rcWoHarzQrHVD4Fsb2wPlD9/X+ZVuFA1F2yWL+A2EQk="
}
},
"messageSignature": {
"messageDigest": {
"algorithm": "SHA2_256",
"digest": "mX7XF7LjI00UJ+DFC6+/IXEoh/Cdsb9EOPAWXwmwn1U="
},
"signature": "MEUCIQDD82ke5ovVwabmMe+2xV1S1XMMwtiRA5veNQn7cb2PFQIgM5Dxo8B8358GSie2lKtX9kMS4lRx4NDd9mm5f1DmzSw="
}
}
Tres observaciones, en orden de importancia:
-
No hay ningún campo
tlogEntries— a diferencia del intento fallido del Paso 2, este bundle solo contiene lo estrictamente necesario para verificar la firma con una llave local: el material de verificación de la llave pública (verificationMaterial.publicKey) y la firma misma (messageSignature). Nada de Rekor, nada de uncheckpointpúblico. Confírmalo tú mismo:grep -c tlogEntries manifest.sigQué esperar (literal):
0— el patrón no aparece ni una sola vez en todo el archivo. -
messageDigest.digestes literal, y coincide exactamente con el hash que ya conoces.mX7XF7LjI00UJ+DFC6+/IXEoh/Cdsb9EOPAWXwmwn1U=es el mismo valor SHA-256 en Base64 queterraform-and-iac-guide(Módulo 7, lección 3) reportó parafunction.zipconoutput_base64sha256, y que confirmaste de forma independiente conshasum -a 256. No es una coincidencia: es la prueba de que estás firmando exactamente el mismo artefacto, byte por byte, que las tres guías anteriores del ecosistema construyeron —cosigncalculó su propio hash del archivo, y ese hash es idéntico al que Terraform calculó por su cuenta, meses de contenido antes, con una herramienta completamente distinta. -
messageSignature.signaturees VARIABLE — nunca esperes que este valor exacto se repita. ECDSA (el algoritmo por defecto decosign generate-key-pair) es, por diseño, no determinista: cada firma sobre el mismo contenido, con la misma llave, produce bytes distintos, porque el algoritmo incorpora un valor aleatorio en cada operación de firma (esto es una propiedad de seguridad deliberada del esquema ECDSA, no un defecto). Si regenerasmanifest.sigdiez veces sobre el mismofunction.zipsin cambiar nada, vas a obtener diez valores designaturedistintos, y los diez van a verificar correctamente contracosign.pub— exactamente lo que la lección siguiente confirma con la única propiedad que sí importa: no el valor exacto de la firma, sino el resultado de verificarla.
Paso 5 — Verificando la firma, 100% offline
cosign verify-blob \
--key cosign.pub \
--bundle manifest.sig \
--insecure-ignore-tlog=true \
lambda/function.zip
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.
Verified OK
Dos líneas, y las dos importan. La primera es una advertencia honesta de cosign mismo: sin un registro de transparencia público de por medio, nadie más que tú puede confirmar, de forma independiente, que esta firma existió en el momento que dice existir —es exactamente el trade-off que la lección 4 explicó al elegir keypair sobre keyless, y cosign te lo recuerda cada vez, en vez de dejarlo implícito—. --insecure-ignore-tlog=true es el flag que reconoce esa advertencia explícitamente: le dice a cosign "sé que no hay Rekor de por medio, verifica solo con la llave pública, como corresponde a este modo". La segunda línea, Verified OK, es el resultado que importa: la firma en manifest.sig corresponde, matemáticamente, a lambda/function.zip firmado con la llave privada que corresponde a cosign.pub.
echo $?
0
Código de salida 0 — el que cualquier job de pipeline (la lección 8 lo va a usar exactamente así) interpreta como "continuar"; un valor distinto de cero, como vas a ver en la lección 7, detiene la cadena ahí mismo.
Qué acabas de demostrar, con exactitud
Vale la pena decir con precisión qué garantiza esta cadena de comandos, sin exagerar ni minimizar: Verified OK confirma que el archivo lambda/function.zip, tal como existe en disco en el momento de correr verify-blob, es exactamente el mismo archivo, byte por byte, que existía en el momento en que corriste sign-blob en el Paso 3 —ni un byte agregado, ni uno quitado, ni uno modificado—, y que quien lo firmó tenía acceso a la llave privada correspondiente a cosign.pub. No confirma que el código dentro de ese .zip esté libre de errores, ni que sea "seguro" en el sentido de las políticas de conftest (Módulo 4) o los escaneos de Trivy/Checkov (Módulo 5) — esas son preguntas distintas, respondidas por herramientas distintas. Lo que esta firma responde es una sola pregunta, con precisión matemática: ¿es este, de verdad, el artefacto que alguien con la llave privada aprobó, sin ninguna alteración en el camino?
Errores comunes
Copiar los flags --tlog-upload=false/--output-signature de una fuente de documentación sin verificarlos contra la versión instalada (el error central de esta lección, dejado a propósito). Qué pasa: alguien encuentra esos flags en un artículo, un tutorial, o incluso en la documentación de una versión anterior de cosign, y los usa tal cual contra una instalación más reciente. Cómo detectarlo: el mensaje Flag ... has been deprecated aparece en la salida, sin ambigüedad. Cómo corregirlo: exactamente el Paso 1 de esta lección — lee el mensaje de deprecación completo, no solo el error final; casi siempre indica el reemplazo correcto (--bundle en este caso). cosign --help (o cosign sign-blob --help) contra la versión real instalada es, siempre, la fuente de verdad más confiable que cualquier documentación externa, porque describe exactamente el comportamiento de la versión que tienes en tu máquina en este momento.
Usar --bundle sin --signing-config, y subir sin querer una firma real al log público de Rekor (de configuración, el más grave de esta lección por su efecto no reversible). Qué pasa: alguien resuelve el error del Paso 1 agregando --bundle, ve que el comando corre sin fallar, y asume que el problema quedó resuelto — sin notar que, por defecto, cosign v3.1.3 sigue consultando la configuración de firma pública de Sigstore y sube la firma a Rekor, un registro público e inmutable. Cómo detectarlo: revisa el archivo .bundle resultante con grep -c tlogEntries archivo.bundle — si el conteo es mayor a cero, la firma se subió de verdad, y no hay forma de deshacerlo: Rekor es, por diseño, un registro permanente. Cómo corregirlo: siempre pasa --signing-config con un archivo sin servicios declarados (Paso 3 de esta lección) cuando el objetivo es una firma completamente local — no asumas que la ausencia de un mensaje de error significa que el comportamiento fue el esperado.
Esperar que dos corridas de sign-blob sobre el mismo archivo produzcan la misma firma, y sospechar de un error si no coinciden (de expectativa sobre ECDSA). Qué pasa: alguien firma function.zip dos veces, compara los archivos manifest.sig byte por byte, ve que son distintos, y concluye que algo falló. Cómo detectarlo: si tu prueba de "consistencia" es comparar el valor exacto de messageSignature.signature entre corridas. Cómo corregirlo: como explicó el Paso 4, ECDSA es no determinista por diseño — la firma exacta variará en cada corrida, y eso es correcto, no un error. La prueba correcta de consistencia nunca es comparar bytes de firma; es correr cosign verify-blob sobre cada una y confirmar que ambas dan Verified OK.
Ejercicios
Ejercicio 1 — Explica, a alguien que solo vio el resultado final de esta lección, por qué el Paso 1 "falló" y eso fue correcto, no un error de la guía. Un compañero, viendo el mensaje Error: must specify --bundle, pregunta por qué una guía publicada incluiría un comando que falla. ¿Cómo lo justificarías?
Ver solución
Una respuesta completa apela a la regla dura de esta guía: cada comando que aparece corrió de verdad para escribir la lección, incluidos los que no funcionaron como la documentación de referencia sugería. Mostrar el Paso 1 tal cual —con su error real— es más honesto y más útil que "corregir en silencio" el comando final sin explicar por qué cambió: alguien que investigue cosign sign-blob por su cuenta, usando una fuente desactualizada, se va a topar con exactamente este mismo error, y esta lección le da tanto el diagnóstico (cosign cambió de interfaz) como la solución (--bundle más --signing-config), en vez de dejarlo perdido frente a un mensaje que no entiende.
Ejercicio 2 — Predice qué mostraría grep -c tlogEntries sobre el .bundle que habrías obtenido en el Paso 2 (el intento sin --signing-config), y explica por qué ese número es distinto del Paso 4. Sin volver a correr el comando del Paso 2, ¿qué valor esperarías, y qué significa la diferencia?
Ver solución
El Paso 2 (sin --signing-config, con --use-signing-config=true implícito) produciría un .bundle con tlogEntries presente al menos una vez —de hecho, como un bloque JSON anidado grande, con logIndex, logId, inclusionProof y un checkpoint firmado por rekor.sigstore.dev—, mientras que el Paso 4 (con --signing-config no-tlog-signing-config.json) da 0. La diferencia no es cosmética: en el Paso 2, la firma se subió de verdad a un servicio público real y queda ahí de forma permanente; en el Paso 4, la firma nunca salió de tu máquina. Es la diferencia práctica y verificable entre el modo keyless que la lección 4 explicó (aunque aquí disparado sin querer, por omitir un flag) y el modo keypair completamente local que este módulo construye de forma deliberada.
Ejercicio 3 — Decide qué le dirías a un compañero que propone simplificar el flujo eliminando --signing-config "porque total, funciona igual". Alguien en tu equipo argumenta que, ya que el Paso 2 también produce Verified OK al final, no vale la pena la complicación extra de generar y pasar no-tlog-signing-config.json. ¿Estás de acuerdo?
Ver solución
No, y la razón no es de conveniencia sino de consecuencia real: aunque ambos flujos terminan en una firma verificable, el Paso 2 publica la firma —y, con ella, el hash del artefacto y metadata asociada— en un registro público permanente, sin que nadie lo haya decidido explícitamente. Para un artefacto de práctica de 890 bytes en un laboratorio $0, el daño es mínimo, pero el hábito que se está formando no lo es: en un proyecto real, subir sin querer un artefacto (o su hash) a un registro público inmutable podría filtrar información que la organización no quería hacer pública, sin ninguna forma de deshacerlo después. El costo extra de generar no-tlog-signing-config.json una sola vez —un archivo de una línea, reutilizable en cada firma futura— es trivial comparado con la irreversibilidad de una subida accidental a Rekor.
Resumen y siguiente paso
En esta lección firmaste lambda/function.zip de verdad, con cosign sign-blob, y verificaste esa firma con cosign verify-blob, obteniendo Verified OK completamente offline. En el camino, documentaste con precisión un caso real de deriva entre la documentación de referencia y el comportamiento actual de la herramienta —los flags --tlog-upload=false/--output-signature resultaron deprecados en v3.1.3, y el camino correcto exige --bundle combinado con un --signing-config explícitamente vacío para evitar, con certeza, una subida real al registro público de Rekor—. Confirmaste que el hash del artefacto que cosign calculó coincide, byte por byte, con el que terraform-and-iac-guide reportó, y aprendiste a distinguir qué partes de un bundle de firma son literales (el messageDigest) de las que varían por diseño en cada corrida (la signature misma).
Antes de avanzar deberías poder: explicar, sin mirar esta lección, por qué --bundle solo no es suficiente para una firma completamente offline; leer el mensaje de deprecación de un flag de cosign y encontrar su reemplazo correcto sin depender de una fuente externa; y justificar por qué comparar bytes exactos de firma nunca es la forma correcta de verificar consistencia entre dos corridas de sign-blob.
La lección 7 pone esta firma a prueba de verdad: vas a modificar function.zip después de firmarlo, y confirmar, en vivo, que cosign verify-blob lo detecta y falla explícitamente.
Recursos
- Sigstore — Signing Blobs — la referencia oficial de
sign-blob/verify-blob, incluido el formato de bundlev0.3que esta lección usa. - GitHub — sigstore/cosign issue #4503 — la discusión pública sobre el comportamiento de
--tlog-uploady su reemplazo, la misma deriva que esta lección documentó de primera mano. - Sigstore — Bundle specification — la especificación formal del formato
application/vnd.dev.sigstore.bundle.v0.3+jsonque producemanifest.sig. terraform-and-iac-guide, Módulo 7, lección 3 (03-packaging-lambda-code-with-archive-file.md) — el origen exacto del hashmX7XF7LjI00UJ+DFC6+/IXEoh/Cdsb9EOPAWXwmwn1U=que esta lección confirmó, de forma independiente, con una herramienta completamente distinta.