LabHub
배우기 러닝패스 코스

빌드는 초록불이었는데 그 라이브러리는 누가 넣었나 · 서명은 멀쩡한데 그 키를 우리가 회수했다 · 이론

서명은 멀쩡한데 그 키를 우리가 회수했다

LabHub 에서 이어서 보기

한 줄 요약

서명이 보장하는 것은 "이 바이트가 그 키로 서명됐다" 뿐이고, 나머지 절반 — **그 키를
지금도 믿는가** — 은 서명 알고리즘 바깥에서 따로 관리해야 합니다.

왜 이게 필요했나

"산출물에 서명을 붙였습니다" 라는 문장은 생각보다 아무것도 말해 주지 않습니다.
보장은 서명을 붙이는 자리가 아니라 검증이 실패하는 자리에서 생기기 때문입니다.
그래서 서명 체계를 들일 때 먼저 할 일은, 실패해야 하는 경우를 전부 적어 놓고 하나씩
실제로 실패시켜 보는 것입니다.

목록을 적어 보면 보통 네 줄이 나옵니다. 산출물이 바뀐 경우, 서명이 우리 키가 아닌
경우, 우리 키이긴 한데 회수했거나 기한이 지난 경우, 그리고 아예 모르는 키인 경우.
앞의 둘은 서명 알고리즘이 알아서 잡아 줍니다. 뒤의 둘은 잡아 주지 않습니다.
원시 키 쌍에는 만료가 없고, "이 키가 우리 것인가" 라는 질문에 답할 자료가 키 안에
들어 있지 않기 때문입니다.

어떻게 동작하나

서명은 파일 전체가 아니라 다이제스트에 겁니다. 해시를 먼저 구하고 그 값에 비밀키로
서명하므로, 산출물이 한 바이트만 달라져도 해시가 통째로 달라져 같은 서명으로는 맞출 수
없습니다. 그래서 변조 탐지는 사실상 공짜로 따라옵니다.

문제는 키입니다. 키를 우리가 들고 있으면 그 키를 누가 어디에 두었는지, 유출되면 어떻게
알아차리는지, 바꿀 때 무엇을 다시 서명해야 하는지가 전부 우리 숙제가 됩니다. sigstore 가
이 숙제를 다른 방식으로 푼 이유가 여기 있습니다. 기본 흐름인 신원 기반 서명에서는
메모리 안에서 임시 키 쌍을 만들고, 인증 기관이 OpenID Connect 신원 토큰을 확인한 뒤 그
신원을 공개키에 묶은 짧은 수명의 인증서를 발급합니다. 서명이 끝나면 비밀키는 곧
파기되고 인증서도 만료됩니다. 검증하는 쪽은 파기된 키 대신 **투명성 로그에 남은
시각이 찍힌 항목**을 보고 판단합니다
([sigstore 서명 개요](https://docs.sigstore.dev/cosign/signing/overview/)).

키를 직접 관리하고 싶다면 cosign 은 KMS 를 통해 그렇게 할 수 있게 해 둡니다. AWS KMS,
GCP KMS, Azure Key Vault, HashiCorp Vault, 쿠버네티스 시크릿 같은 제공자를 awskms://,
gcpkms://, k8s:// 처럼 URI 로 가리키고, cosign public-key --key <URI> 로 공개키를
꺼내 검증에 씁니다
([cosign 키 관리](https://docs.sigstore.dev/cosign/key_management/overview/)).
검증 쪽에서는 cosign verify 가 서명뿐 아니라 서명 인증서가 신뢰하는 인증 기관에서
나온 것인지까지 확인한다고 문서가 적고 있습니다
([cosign 검증](https://docs.sigstore.dev/cosign/verifying/verify/)).

어느 길을 가든 남는 원칙은 같습니다. **"이 서명이 맞는가" 와 "이 키를 지금 믿는가" 는
서로 다른 질문이고, 둘 다 물어야 합니다.** 직접 관리한다면 어떤 키를 언제부터 언제까지
믿을지를 적은 등록부가 필요하고, sigstore 를 쓴다면 인증서의 수명과 투명성 로그가 그
자리를 대신합니다.

현장에서 만나는 모습

가장 비싼 실패는 검증을 하긴 하는데 실패를 삼키는 것입니다. 파이프라인에서 검증
명령의 종료 코드를 보지 않거나, 오류를 로그로만 남기고 다음 단계로 넘어가는 형태입니다.
이 상태는 서명을 안 붙인 것보다 나쁩니다 — 모두가 보호받고 있다고 믿기 때문입니다.

두 번째는 키 교체 때 드러납니다. 새 키로 옮기면서 옛 키를 등록부에서 지우지 않으면,
회수한 키로 서명된 산출물이 계속 통과합니다. 반대로 옛 키를 그냥 지워 버리면 아직
배포되지 않은 예전 릴리스가 전부 막힙니다. 그래서 지우는 대신 기한을 적는 쪽이
운영에서 살아남습니다.

세 번째는 서명 대상이 흐릿한 것입니다. 무엇에 서명했는지가 서명 파일 안에 들어 있지
않으면, 검증하는 쪽은 "이 서명이 저 파일의 것" 이라는 사실을 파일 이름으로 짐작하게
됩니다. 릴리스가 여러 개인 디렉터리에서 이 짐작은 곧 틀립니다. 그래서 실무에서는 서명을
산출물에 직접 걸기보다, 대상과 그 다이제스트를 적은 문서에 서명하고 그 문서를 함께
배포하는 쪽으로 갑니다. 다음 모듈의 출처 증명이 정확히 그 문서입니다.

네 번째는 비밀키를 빌드 단계에서 읽을 수 있게 두는 것입니다. 빌드 스크립트가 키에 닿을
수 있으면 그 빌드에서 도는 어떤 코드든 키에 닿을 수 있고, 의존 하나가 오염된 순간 서명
자체가 무의미해집니다. 키를 KMS 뒤에 두거나 아예 임시 키를 쓰는 설계가 이 문제를
정면으로 겨냥한 것입니다.

다섯 번째는 서명을 만든 뒤 검증을 한 번도 해 보지 않는 것입니다. 서명 명령은 대개
조용히 성공하기 때문에, 해시 알고리즘을 서로 다르게 주었거나 엉뚱한 키로 서명했어도
그 자리에서는 아무 일도 일어나지 않습니다. 문제는 몇 주 뒤 배포 파이프라인에서, 그것도
급한 날에 드러납니다. 서명한 직후 같은 자리에서 검증까지 돌려 보는 한 줄이 이 사고를
통째로 없앱니다.

다음 실습에서 할 것

키 쌍을 만들어 산출물에 서명하고, 검증이 실패해야 하는 네 경우를 직접 만들어 봅니다.
변조본, 회수한 옛 키로 만든 멀쩡한 서명, 등록부에 없는 키, 그리고 정상 산출물. 마지막에는
등록부를 읽어 기한까지 확인하는 검증기를 만들어 네 경우를 한 번에 갈라 냅니다.