LabHub
배우기 러닝패스 코스

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

Build the four cases where verification must fail

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

릴리스 산출물에 직접 서명하고, 검증이 실패해야 하는 경우를 손으로 만들어 봅니다. 변조·다른 키·회수한 키·등록부에 없는 키 네 가지를 모두 거부하는 검증기를 만듭니다.

왜 중요한가

"서명을 붙였다" 는 문장은 아무것도 보장하지 않습니다. 보장은 검증이 실패하는 자리에서 생깁니다. 그래서 서명 체계를 도입할 때 먼저 할 일은 서명이 아니라, 실패해야 할 경우를 전부 적어 놓고 하나씩 실제로 실패시키는 것입니다. 특히 마지막 두 가지가 자주 빠집니다 — 서명 자체는 수학적으로 멀쩡한데 그 키를 우리가 더는 믿지 않는 경우입니다. 원시 키에는 만료가 없어서, 언제까지 믿을지는 바깥에 따로 적어야 합니다. sigstore 가 짧은 수명의 인증서와 투명성 로그를 쓰는 이유가 여기 있습니다 — 검증하는 쪽이 서명한 사람의 키 관리 습관에 기대지 않게 하려는 것입니다.

단계

  1. 산출물을 /root/sign/dist 에 두고 지문을 /root/sign/01-digest.txt 에 적습니다.
  2. P-256 키 쌍을 /root/sign/release.key/root/sign/release.pub 에 만듭니다.
  3. 산출물에 서명해 /root/sign/dist/paygate-1.4.2.js.sig 를 만듭니다.
  4. 검증 출력을 /root/sign/04-verify.txt 에 남깁니다.
  5. 변조본을 만들어 실패 출력을 /root/sign/05-tamper.txt 에 남깁니다.
  6. 회수한 옛 키로 서명해 실패 출력을 /root/sign/06-oldkey.txt 에 남깁니다.
  7. 두 키를 기한과 지문으로 /root/sign/keyring.json 에 등록합니다.
  8. /root/sign/verify-release.sh 를 만들고 네 경우의 종료 코드를 /root/sign/08-checks.json 에 적습니다.

참고

서명할 물건을 한자리에 두고 지문을 뜬다

/opt/fixtures/sbom/release/paygate-1.4.2.js 를 /root/sign/dist/paygate-1.4.2.js 로 복사하고, 그 파일의 SHA-256 을 소문자 16진 64자 한 줄로 /root/sign/01-digest.txt 에 저장하세요.

서명은 파일 전체가 아니라 다이제스트에 하는 것입니다. 먼저 그 값을 손에 쥐고 시작하면, 뒤에서 무엇이 달라졌는지 눈으로 확인할 수 있습니다.

릴리스 키 쌍을 만든다

prime256v1(P-256) 곡선으로 키 쌍을 만들어 비밀키를 /root/sign/release.key 에, 공개키를 /root/sign/release.pub 에 저장하세요. 암호를 거는 비밀키는 쓰지 않습니다.

openssl ecparam 으로 곡선을 지정해 비밀키를 만들고, openssl ec-pubout 을 주면 같은 키에서 공개키가 나옵니다.

다이제스트에 서명한다

/root/sign/release.key 로 /root/sign/dist/paygate-1.4.2.js 에 서명해 /root/sign/dist/paygate-1.4.2.js.sig 에 저장하세요. 해시는 SHA-256 입니다.

openssl dgst-sign 을 주면 서명을, -verify 를 주면 검증을 합니다. 출력은 이진이므로 -out 으로 파일에 받으세요.

검증이 통과하는 것을 기록한다

/root/sign/release.pub 로 /root/sign/dist/paygate-1.4.2.js 와 /root/sign/dist/paygate-1.4.2.js.sig 를 검증하고, openssl 이 낸 출력을 /root/sign/04-verify.txt 에 저장하세요.

통과할 때 openssl 은 한 줄만 냅니다. 그 한 줄이 이 단계의 증거입니다.

산출물을 한 글자 고치면 어떻게 되는지 본다

/root/sign/dist/paygate-1.4.2.js 를 /root/sign/tampered/paygate-1.4.2.js 로 복사한 뒤 내용을 바꾸고, 원래 서명 /root/sign/dist/paygate-1.4.2.js.sig 로 검증해 실패 출력을 /root/sign/05-tamper.txt 에 저장하세요.

서명은 다이제스트에 걸려 있습니다. 한 바이트만 달라져도 다이제스트가 통째로 달라지니 같은 서명으로는 맞출 수 없습니다.

작년에 쓰다 회수한 키로 서명된 것을 만든다

P-256 키 쌍을 하나 더 만들어 /root/sign/old-release.key 와 /root/sign/old-release.pub 에 저장하고, 그 키로 /root/sign/dist/paygate-1.4.2.js 에 서명해 /root/sign/dist/paygate-1.4.2.js.oldsig 를 만드세요. 그다음 /root/sign/release.pub 로 그 서명을 검증해 실패 출력을 /root/sign/06-oldkey.txt 에 저장하세요.

이 서명은 위조가 아닙니다 — 수학적으로 멀쩡한 서명이고, 그 키의 공개키로는 통과합니다. 문제는 '그 키를 지금도 믿는가' 이고, 그건 서명 알고리즘이 답해 주지 않습니다.

어떤 키를 언제까지 믿을지 적어 둔다

/root/sign/keyring.json 에 keys 배열로 두 키를 등록하세요. 각 항목은 id·file·fingerprint·not_before·not_after·owner 입니다. release.pub 은 id release-2026·기한 2026-01-01 부터 2026-12-31 까지, old-release.pub 은 id release-2025·기한 2025-01-01 부터 2025-12-31 까지이고 owner 는 둘 다 payments-platform 입니다. file 은 /root/sign 안의 파일 이름만 적고, fingerprint 는 그 공개키를 DER 로 뽑아 구한 SHA-256 소문자 16진입니다.

원시 키에는 만료가 없습니다. 그래서 '언제까지 믿을지' 를 바깥에 따로 적어야 하고, 이것이 sigstore 가 짧은 수명의 인증서를 쓰는 이유이기도 합니다.

등록부를 보는 검증기를 만들고 네 경우를 판정한다

/root/sign/verify-release.sh 를 만드세요. bash /root/sign/verify-release.sh <산출물> <서명> <키id> 로 부르면 /root/sign/keyring.json 에서 그 키를 찾아 기준일 2026-09-11 에 기한이 살아 있는지 보고, 살아 있으면 그 공개키로 서명을 검증합니다. 통과하면 0, 등록부에 없거나 기한이 지났거나 검증이 실패하면 1 로 끝냅니다. 그다음 네 경우를 돌려 종료 코드를 /root/sign/08-checks.json 에 good·tampered·old_key·unknown_key 로 저장하세요. good 은 정상 산출물과 서명에 release-2026, tampered 는 변조본에 같은 서명과 release-2026, old_key 는 정상 산출물에 .oldsig 와 release-2025, unknown_key 는 등록부에 없는 release-2024 입니다.

검증기가 답해야 하는 질문은 둘입니다 — 서명이 이 바이트에 맞는가, 그리고 그 키를 지금 믿어도 되는가. 둘 중 하나만 보면 회수한 키로 서명된 것이 그대로 통과합니다.