TT Lab
Get started
Learn Learning paths Courses

The Build Was Green — So Who Put That Library In?

Build the four cases where verification must fail

Continue in TT Lab

Goal

You sign a release artifact yourself and build by hand the cases where verification must fail. You build a verifier that rejects all four: tampering, a different key, a revoked key, and a key not in the keyring.

Why it matters

The sentence "we attached a signature" guarantees nothing. The guarantee comes from where verification fails. So the first thing to do when adopting a signing scheme is not to sign, but to write down every case that should fail and make each one actually fail. The last two are especially often missed — cases where the signature itself is mathematically fine but we no longer trust that key. A raw key has no expiry, so how long to trust it must be recorded separately, outside. This is why sigstore uses short-lived certificates and a transparency log — so that the verifier does not depend on the signer's key management habits.

Steps

  1. Put the artifact in /root/sign/dist and write its fingerprint to /root/sign/01-digest.txt.
  2. Create a P-256 key pair at /root/sign/release.key and /root/sign/release.pub.
  3. Sign the artifact to create /root/sign/dist/paygate-1.4.2.js.sig.
  4. Save the verification output to /root/sign/04-verify.txt.
  5. Create a tampered copy and save the failure output to /root/sign/05-tamper.txt.
  6. Sign with the revoked old key and save the failure output to /root/sign/06-oldkey.txt.
  7. Register the two keys in /root/sign/keyring.json with their validity periods and fingerprints.
  8. Create /root/sign/verify-release.sh and write the exit codes of the four cases to /root/sign/08-checks.json.

Notes

Put the thing to sign in one place and take its fingerprint

Copy /opt/fixtures/sbom/release/paygate-1.4.2.js to /root/sign/dist/paygate-1.4.2.js, and save the SHA-256 of that file to /root/sign/01-digest.txt as one line of 64 lowercase hexadecimal characters.

A signature is applied to the digest, not to the whole file. If you start with that value in hand, you can see with your own eyes what changed later.

Create the release key pair

Create a key pair with the prime256v1 (P-256) curve and save the private key to /root/sign/release.key and the public key to /root/sign/release.pub. Do not use a password-protected private key.

Create the private key with openssl ecparam specifying the curve, and give -pubout to openssl ec to get the public key from the same key.

Sign the digest

Sign /root/sign/dist/paygate-1.4.2.js with /root/sign/release.key and save the result to /root/sign/dist/paygate-1.4.2.js.sig. The hash is SHA-256.

openssl dgst signs when you give it -sign and verifies when you give it -verify. The output is binary, so write it to a file with -out.

Record that verification passes

Verify /root/sign/dist/paygate-1.4.2.js and /root/sign/dist/paygate-1.4.2.js.sig with /root/sign/release.pub, and save the output that openssl produced to /root/sign/04-verify.txt.

When it passes, openssl prints just one line. That one line is the evidence for this step.

See what happens when you change one character of the artifact

Copy /root/sign/dist/paygate-1.4.2.js to /root/sign/tampered/paygate-1.4.2.js, then change its contents, verify it with the original signature /root/sign/dist/paygate-1.4.2.js.sig, and save the failure output to /root/sign/05-tamper.txt.

The signature is applied to the digest. If even one byte differs, the digest changes entirely, so the same signature cannot match.

Create something signed with a key used last year and then revoked

Create one more P-256 key pair and save it to /root/sign/old-release.key and /root/sign/old-release.pub, sign /root/sign/dist/paygate-1.4.2.js with that key to create /root/sign/dist/paygate-1.4.2.js.oldsig. Then verify that signature with /root/sign/release.pub and save the failure output to /root/sign/06-oldkey.txt.

This signature is not a forgery — it is a mathematically sound signature and it passes with that key's public key. The question is 'do we still trust that key?', and the signature algorithm does not answer that.

Write down which key to trust until when

Register the two keys in /root/sign/keyring.json as a keys array. Each entry has id, file, fingerprint, not_before, not_after, and owner. release.pub has the id release-2026 and the validity period from 2026-01-01 to 2026-12-31, old-release.pub has the id release-2025 and the validity period from 2025-01-01 to 2025-12-31, and the owner of both is payments-platform. For file, write only the file name within /root/sign, and fingerprint is the lowercase hexadecimal SHA-256 computed by extracting that public key as DER.

A raw key has no expiry. So 'until when to trust it' has to be recorded separately, outside, and this is also why sigstore uses short-lived certificates.

Build a verifier that reads the keyring and judge the four cases

Create /root/sign/verify-release.sh. When called as bash /root/sign/verify-release.sh <산출물> <서명> <키id> (the three arguments are the artifact, the signature, and the key id), it looks up that key in /root/sign/keyring.json, checks whether its validity period is still active on the reference date 2026-09-11, and if so verifies the signature with that public key. It exits with 0 if it passes, and with 1 if the key is not in the keyring, the validity period has passed, or verification fails. Then run the four cases and save the exit codes to /root/sign/08-checks.json as good, tampered, old_key, and unknown_key. good is the normal artifact and signature with release-2026, tampered is the tampered copy with the same signature and release-2026, old_key is the normal artifact with .oldsig and release-2025, and unknown_key is release-2024, which is not in the keyring.

The verifier has to answer two questions — does the signature match these bytes, and can we trust that key right now. If you look at only one of them, something signed with a revoked key passes as is.