Module 5: Securing The Ai Workload
6. Hands-on: signing the extractor's artifact with `cosign`
Description
This lesson really signs this guide's first genuinely new binary artifact: extract-shipment-manifest-fields's .zip. It uses exactly the same cosign.key/cosign.pub cloud-security-and-guardrails-guide, Module 6, lesson 5 already generated — the same keypair, byte for byte, nothing regenerated — and the same offline flow with --signing-config that guide, Module 6, lesson 6, already precisely documented, including the real finding that two flags from an earlier version of the documentation (--tlog-upload=false, --output-signature) are deprecated in cosign v3.1.3. None of that gets repeated here from scratch — it gets applied, as-is, to a different artifact.
Connection to the module
ci.yml's verify-artifact job (inherited from cloud-security-and-guardrails-guide, Module 8, lesson 3) today verifies a single artifact: lambda/function.zip, process-shipment-manifest's. This module's lesson 8 extends that same job — without creating a new one — so it also verifies the .zip this lesson signs here. Without this lesson's manifest.sig, that new step would have nothing to verify.
Analogy: the same seal, applied to a different package
A notary who already has their official seal registered doesn't need to process a new seal every time they certify a different document — the same seal, applied with the same procedure, certifies a sales contract today and a power of attorney tomorrow. What changes is the document; the seal, and the procedure for applying it, stay the same. cosign.key is that seal. lambda/function.zip already has it applied, since cloud-security-and-guardrails-guide, Module 6, lesson 6. This lesson applies the same seal, with the exact same procedure, to extract-shipment-manifest-fields's .zip.
Step 1 — Confirming the keypair is still there, nothing regenerated
cosign version
What to expect (literal — the same version cloud-security-and-guardrails-guide, Module 6, lesson 5 already confirmed, with no new installation in this module):
GitVersion: v3.1.3
GitCommit: 11926fa5bbbbde47e88fc006b625a17769b743b2
GitTreeState: "clean"
BuildDate: 2026-08-05T23:43:27Z
GoVersion: go1.26.5
Compiler: gc
Platform: darwin/arm64
ls -la cosign.key cosign.pub no-tlog-signing-config.json
What to expect (literal — the three files that same lesson already left in andes-cargo-infra/; none of them get touched in this module):
-rw------- 1 andes-cargo staff 653 cosign.key
-rw-r--r-- 1 andes-cargo staff 178 cosign.pub
-rw-r--r-- 1 andes-cargo staff 106 no-tlog-signing-config.json
Three files, zero installation commands. no-tlog-signing-config.json is the signing configuration file with no service declared — no Fulcio, no Rekor, no timestamping — that lesson already built to reliably avoid an accidental upload to the public transparency log; it gets reused here exactly as-is.
Step 2 — The artifact: extract-shipment-manifest-fields/function.zip
functions/extract-shipment-manifest-fields/handler.py is the minimal handler this guide's Module 1 presents (minimal, not polished — the boundary with AI Engineering, Module 1, lesson 4). Package it:
cd functions/extract-shipment-manifest-fields
zip -X function.zip handler.py
What to expect (literal):
adding: handler.py (deflated 53%)
ls -la function.zip
shasum -a 256 function.zip
What to expect (literal — executed to write this lesson):
-rw-r--r-- 1 andes-cargo staff 1230 function.zip
aefcd07a3f2a3b426ebea98efc002cd7986061f08f07be2242f487289b359ba2 function.zip
1,230 bytes — smaller than lambda/function.zip (890 bytes, process-shipment-manifest's handler), but comparable: both are practice artifacts, tiny, exactly the point that signing discipline doesn't depend on code size, but on there being a verifiable chain between what someone reviewed and what gets deployed.
Step 3 — Signing, offline, with the already-existing keypair
The complete flow cloud-security-and-guardrails-guide, Module 6, lesson 6, already walked through with its two failed attempts — deprecated --tlog-upload=false/--output-signature, --bundle accidentally uploading to Rekor by default — doesn't get repeated here: this lesson goes straight to the correct, already-known command:
COSIGN_PASSWORD="" cosign sign-blob \
--key cosign.key \
--signing-config no-tlog-signing-config.json \
--bundle functions/extract-shipment-manifest-fields/manifest.sig \
--yes \
functions/extract-shipment-manifest-fields/function.zip
What to expect (literal — executed to write this lesson):
Using payload from: functions/extract-shipment-manifest-fields/function.zip
Signing artifact...
Wrote bundle to file functions/extract-shipment-manifest-fields/manifest.sig
No legal notice about a public registry — the same indirect confirmation cloud-security-and-guardrails-guide already explained: if cosign were about to upload something to a hosted service, it would ask for explicit consent before signing. Its absence here is proof there's no upload involved.
cat functions/extract-shipment-manifest-fields/manifest.sig
What to expect (the mediaType field and the structure are literal; messageSignature.signature is VARIABLE — see the note below):
{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial":{"publicKey":{"hint":"rcWoHarzQrHVD4Fsb2wPlD9/X+ZVuFA1F2yWL+A2EQk="}}, "messageSignature":{"messageDigest":{"algorithm":"SHA2_256", "digest":"rvzQej8qO0JuvqmO/AAs15hgYfCPB74iQvSHKJs1m6I="}, "signature":"MEUCIAtn2Qxn7aZMe2rSvEYhgy5/z9VxbOT+4Rn8TVp3btN/AiEAtL5+X9Q54z3aAGczzn7jU5AVMuF4OjF0kOaXRy4KBVA="}}
Notice verificationMaterial.publicKey.hint: rcWoHarzQrHVD4Fsb2wPlD9/X+ZVuFA1F2yWL+A2EQk=, exactly the same hint cloud-security-and-guardrails-guide, Module 6, lesson 6 already showed in its own manifest.sig for lambda/function.zip. That's not a coincidence — it's literal, in-file confirmation that this manifest.sig was signed with the same keypair, not a new one. messageDigest.digest is the real SHA-256 hash of this specific .zip, different from lambda/function.zip's because the content is different; messageSignature.signature is the value that will vary in your own run — ECDSA, the keypair's default algorithm, is non-deterministic by design, the same security property cloud-security-and-guardrails-guide already explained.
grep -c tlogEntries functions/extract-shipment-manifest-fields/manifest.sig
What to expect (literal): 0 — no public transparency log involved, exactly as the reused no-tlog-signing-config.json guarantees.
Step 4 — Verifying, 100% offline
cosign verify-blob \
--key cosign.pub \
--bundle functions/extract-shipment-manifest-fields/manifest.sig \
--insecure-ignore-tlog=true \
functions/extract-shipment-manifest-fields/function.zip
What to expect (literal — executed to write this lesson):
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
Verified OK
echo $?
0
Verified OK — the exact same guarantee cloud-security-and-guardrails-guide, Module 6, lesson 6 already precisely explained: this .zip, exactly as it exists on disk right now, is exactly the same, byte for byte, as it was when it was signed, and whoever signed it had access to the private key corresponding to cosign.pub. It doesn't confirm the code inside the .zip is correct, nor that it passes policy/'s policies (this module's lesson 3) or the Trivy scan (lesson 5) — those are different questions, answered by different controls, chained together in the gate lesson 8 runs end to end.
Why this lesson doesn't repeat cloud-security-and-guardrails-guide's Module 6 mistake
It's worth precisely stating why this lesson doesn't show the two failed attempts (deprecated --tlog-upload=false, --bundle without --signing-config accidentally uploading to Rekor) that guide did document: not because this lesson is hiding something, but because that lesson already did the work of investigating the correct command, with real evidence, and this lesson inherits that finding as-is — exactly the same "don't re-investigate what a sibling guide already resolved" discipline this ecosystem applies in every new guide. Repeating the two failed attempts here would be padding, not honesty — the honesty is already documented, with its exact date and version, in the original source.
Common mistakes
Generating a new cosign.key/cosign.pub for this module, instead of reusing the existing one (not confirming what already exists before acting). What happens: someone, arriving at this lesson, runs cosign generate-key-pair without first checking whether a keypair already exists in the project. How to spot it: if cosign.key/cosign.pub in your project have today's modification date, instead of the date cloud-security-and-guardrails-guide, Module 6, originally generated them. How to fix it: this lesson's Step 1 explicitly confirms, with ls -la, that all three files already exist before signing anything new — this entire module's hard rule is zero reinstallation, and a new keypair, even though it would technically work, would break the continuity of lambda/function.zip and the extractor's new .zip sharing the same chain of trust.
Verifying with --bundle manifest.sig without specifying the full path, and confusing lambda/'s manifest.sig with functions/extract-shipment-manifest-fields/'s (two artifacts with the same signature filename). What happens: someone, working from andes-cargo-infra/'s root, runs cosign verify-blob --bundle manifest.sig ... without the full path, and the command finds — or doesn't find — the wrong file, because both artifacts use the same conventional name manifest.sig in different directories. How to spot it: a file-not-found error, or — worse, silent — a verification that compares the wrong signature against the wrong artifact. How to fix it: always use the full, explicit path, as in this lesson's Step 4 (functions/extract-shipment-manifest-fields/manifest.sig, never just manifest.sig) — the same explicit-paths discipline this module's lesson 8 applies inside ci.yml.
Expecting messageDigest.digest to be the same between lambda/function.zip and the extractor's .zip (confusing "same key" with "same content"). What happens: someone, seeing that both manifest.sig files share the same publicKey.hint, assumes they should also share the same messageDigest.digest. How to spot it: if you compare the two manifest.sig files expecting them to match on more than the public key's hint. How to fix it: the hint identifies who signed (the same key, in both cases); the messageDigest.digest identifies what was signed (each .zip's exact content, necessarily different because process-shipment-manifest's handler.py and extract-shipment-manifest-fields's are different files). Sharing the key doesn't imply sharing the content — these are two independent questions cosign's bundle answers separately.
Exercises
Exercise 1 — Without running any cosign command, check whether you'd expect rvzQej8qO0JuvqmO/AAs15hgYfCPB74iQvSHKJs1m6I= (this lesson's messageDigest.digest) to match any hash you've already seen in another guide in this ecosystem. Explain why or why not.
See solution
It wouldn't match any hash from another guide, because extract-shipment-manifest-fields/function.zip is a genuinely new artifact of this guide — the handler didn't exist in terraform-and-iac-guide or aws-serverless-and-containers-guide. The only hash that would match between two independent sources would be lambda/function.zip's (process-shipment-manifest's), which cloud-security-and-guardrails-guide, Module 6, lesson 6 already confirmed matched what terraform-and-iac-guide, Module 7, reported with output_base64sha256 — two different tools, same artifact, same hash. This lesson's .zip is new, so its hash has no precedent to confirm against either.
Exercise 2 — Explain why cosign verify-blob needs both cosign.pub and manifest.sig as arguments, and what would happen if you only had one of the two. Think about what information each file contributes on its own.
See solution
cosign.pub answers "who claims to have signed this?" — the public key corresponding to the private one used; manifest.sig answers "what's the specific signature, and over what exact hash?" Without cosign.pub, there'd be nothing to mathematically compare the signature against — you wouldn't know if it corresponds to a legitimate key or to just any key. Without manifest.sig, there'd be no signature to verify at all — you'd only have a public key with nothing to confirm against it. The two files, together, are the minimum needed for verification to make sense: a signature without a public key is useless, a public key without a signature verifies nothing.
Exercise 3 — Predict what cosign verify-blob would show if you tried to verify functions/extract-shipment-manifest-fields/function.zip against lambda/'s manifest.sig (the wrong artifact, on purpose). Would it fail with an error, or with a different message?
See solution
It would fail — but with a message indicating the signature doesn't correspond to the content, not a "file not found" or "invalid key" error. The public key would still be valid (it's the same key for both artifacts), but the messageDigest.digest inside lambda/'s manifest.sig corresponds to lambda/function.zip's hash, not functions/extract-shipment-manifest-fields/function.zip's — cosign would compute the real hash of the file you gave it and compare it against what the signature claims to have signed; since they wouldn't match, verification would fail with a message along the lines of an invalid signature for that specific content, exactly the same kind of failure cloud-security-and-guardrails-guide, Module 6, lesson 7 already demonstrated by deliberately breaking the chain, modifying the artifact after signing it.
Summary and next step
This lesson signed, with a real cosign sign-blob, this guide's first genuinely new binary artifact — extract-shipment-manifest-fields/function.zip —, reusing exactly the same cosign.key/cosign.pub/no-tlog-signing-config.json cloud-security-and-guardrails-guide already generated, with nothing new installed and no repeating that guide's investigation work. You confirmed, with literal evidence, that the resulting manifest.sig shares the same public-key hint as lambda/function.zip's — same key, different content —, that it contains no tlogEntries (0, truly offline), and that cosign verify-blob returns Verified OK with exit code 0.
Before moving on you should be able to: sign and verify a new artifact reusing an existing keypair, without needing to generate your own; explain why two different manifest.sig files can share the same public-key hint but never the same messageDigest.digest; and anticipate what message cosign verify-blob would show if the signature and the artifact didn't correspond to each other.
Lesson 7 returns to THREAT-MODEL.md — the document cloud-security-and-guardrails-guide left with seven rows — and adds the first two rows specific to an AI workload, with the exact mitigation this guide's Modules 4 and 5 already built.
Resources
- Sigstore — Signing Blobs — official reference for
sign-blob/verify-bloband thev0.3bundle format used in this lesson. cloud-security-and-guardrails-guide, Module 6, lesson 5 (05-hands-on-installing-cosign-and-a-local-keypair.md) — the exact origin of the keypair this lesson reuses, without regenerating.cloud-security-and-guardrails-guide, Module 6, lesson 6 (06-hands-on-signing-and-verifying-the-deployment-artifact.md) — the complete investigation of the deprecated flags and the correct offline flow, inherited as-is by this lesson.- This guide's Module 1, lesson 4 — the boundary with AI Engineering that fixes why this lesson's
handler.pyis minimal, not polished.