The Build Was Green — So Who Put That Library In?
Build the four cases where verification must fail
한국어 원문으로 표시합니다.
목표
릴리스 산출물에 직접 서명하고, 검증이 실패해야 하는 경우를 손으로 만들어 봅니다. 변조·다른 키·회수한 키·등록부에 없는 키 네 가지를 모두 거부하는 검증기를 만듭니다.
왜 중요한가
"서명을 붙였다" 는 문장은 아무것도 보장하지 않습니다. 보장은 검증이 실패하는 자리에서 생깁니다. 그래서 서명 체계를 도입할 때 먼저 할 일은 서명이 아니라, 실패해야 할 경우를 전부 적어 놓고 하나씩 실제로 실패시키는 것입니다. 특히 마지막 두 가지가 자주 빠집니다 — 서명 자체는 수학적으로 멀쩡한데 그 키를 우리가 더는 믿지 않는 경우입니다. 원시 키에는 만료가 없어서, 언제까지 믿을지는 바깥에 따로 적어야 합니다. sigstore 가 짧은 수명의 인증서와 투명성 로그를 쓰는 이유가 여기 있습니다 — 검증하는 쪽이 서명한 사람의 키 관리 습관에 기대지 않게 하려는 것입니다.
단계
- 산출물을
/root/sign/dist에 두고 지문을/root/sign/01-digest.txt에 적습니다. - P-256 키 쌍을
/root/sign/release.key와/root/sign/release.pub에 만듭니다. - 산출물에 서명해
/root/sign/dist/paygate-1.4.2.js.sig를 만듭니다. - 검증 출력을
/root/sign/04-verify.txt에 남깁니다. - 변조본을 만들어 실패 출력을
/root/sign/05-tamper.txt에 남깁니다. - 회수한 옛 키로 서명해 실패 출력을
/root/sign/06-oldkey.txt에 남깁니다. - 두 키를 기한과 지문으로
/root/sign/keyring.json에 등록합니다. /root/sign/verify-release.sh를 만들고 네 경우의 종료 코드를/root/sign/08-checks.json에 적습니다.
참고
- 이 이미지에는 cosign 이 없습니다. 키 없는 서명은 Fulcio·Rekor 라는 바깥 서비스를 전제하는데 파드는 DNS 만 열려 있습니다. 그래서 openssl 로 같은 뼈대를 만듭니다.
- 서명 파일은 이진입니다.
cat으로 보지 말고-out으로 파일에 받으세요. - 검증 실패는 openssl 의 표준 오류로도 나갑니다. 기록할 때는
2>&1로 함께 받으세요. - 기준일은
2026-09-11로 고정합니다.date를 읽어 쓰면 내일 다른 답이 나옵니다. - 흔한 실수 — 서명할 때와 검증할 때 해시를 다르게 주는 것, 그리고 변조본을 만들었는데 원본에 덮어써서 두 파일이 같아지는 것입니다.
- 실습 파드에는 볼륨이 없습니다. 여기서 만든 키는 세션이 끝나면 사라집니다.
서명할 물건을 한자리에 두고 지문을 뜬다
/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 입니다.
검증기가 답해야 하는 질문은 둘입니다 — 서명이 이 바이트에 맞는가, 그리고 그 키를 지금 믿어도 되는가. 둘 중 하나만 보면 회수한 키로 서명된 것이 그대로 통과합니다.