사전 서명된 태그
개념

서명된 태그

gabury1고친 사람 github-actions[bot]

서명된 태그는 이 커밋이 정말 내가 낸 릴리스라고 보증해 주는 태그입니다. 태그를 만든 사람이 자기만 가진 키로 서명을 붙여 둡니다. 받는 사람은 그 서명을 검사해서 태그를 남이 꾸미거나 몰래 옮기지 않았는지 확인합니다.

쉽고 빠른 이해

서명된 태그는 「이 커밋이 1.0 이다」라는 표시에 만든 사람의 서명을 붙인 것입니다. 예를 들어 v1.0 태그를 받은 사람이 서명을 검사하면, 그 태그를 누가 붙였는지와 그 뒤로 바뀌지 않았는지를 함께 알 수 있습니다.

이게 없으면 태그는 그저 이름표입니다. 저장소에 쓸 수 있는 사람이면 누구나 같은 이름의 태그를 다른 커밋에 붙일 수 있습니다. 받는 쪽은 그걸 가려낼 방법이 없습니다.

  1. 만든 사람이 자기만 가진 키로 태그에 서명합니다
  2. 태그를 저장소에 올리면 서명도 함께 올라갑니다
  3. 받는 사람은 만든 사람이 공개해 둔 키로 서명을 검사합니다

대가는 키 관리입니다. 키를 만들고 지키고 남에게 나눠 줘야 합니다. 받는 쪽도 그 키가 정말 그 사람 것인지 따로 확인해야 합니다.

그래서 남에게 내놓는 릴리스에는 서명된 태그를 붙입니다. 혼자 잠깐 표시해 둘 때는 서명 없는 태그로 충분합니다.

상세

계약서 표지에 「최종본」이라고 적는 일은 누구나 할 수 있습니다. 당사자가 그 옆에 인감 도장을 찍어 두면 누가 최종본이라고 정했는지가 남습니다. 서명된 태그는 도장이 찍힌 표지입니다.

Git 의 태그는 특정 커밋에 붙이는 이름입니다. v1.0 처럼 릴리스 번호를 붙여 두면 수많은 커밋 가운데 어느 것이 1.0 인지 바로 찾을 수 있습니다. 브랜치 이름은 커밋이 쌓일 때마다 앞으로 옮겨 가지만, 태그는 한 커밋에 머무는 것이 원래 쓰임입니다.

태그의 두 종류

Git 태그는 두 가지로 나뉩니다. 서명을 붙일 수 있는 것은 그중 하나뿐입니다.

경량 태그는 커밋을 가리키는 이름 하나만 있는 태그입니다. 누가 언제 붙였는지 같은 정보는 담지 않습니다. 잠깐 표시해 두는 용도로 씁니다.

주석 달린 태그는 Git 저장소 안에 태그 객체라는 기록을 따로 하나 만듭니다. 태그 객체에는 가리키는 커밋, 태그 이름, 붙인 사람과 시각, 설명 메시지가 들어갑니다. 서명된 태그는 이 주석 달린 태그의 메시지 끝에 서명을 덧붙인 것입니다.

종류 담는 것 서명
경량 태그 커밋을 가리키는 이름 못 붙인다
주석 달린 태그 이름 · 붙인 사람 · 시각 · 메시지 안 붙였다
서명된 태그 주석 달린 태그 + 서명 붙였다

표의 셋째 줄은 셋째 종류가 아닙니다. 주석 달린 태그 가운데 서명한 것을 견주어 보려고 줄을 따로 세웠습니다.

남에게 내놓는 릴리스에는 서명된 태그를 붙입니다. 혼자 잠깐 표시해 두는 데는 경량 태그로 충분합니다. 서명하려면 키를 갖춰야 하기 때문입니다.

이름표만으로는 모자란 까닭

태그는 지울 수도 있고 같은 이름으로 다시 만들 수도 있습니다. 저장소에 푸시 권한이 있는 사람이면 v1.0 을 전혀 다른 커밋에 붙여 올릴 수 있습니다. 받는 사람의 눈에는 이름이 똑같으니 차이가 보이지 않습니다.

이 틈은 사람의 실수로도 열리고 공격으로도 열립니다. 코드를 내려받는 서버가 뚫리거나, 원본을 베껴 둔 미러 저장소가 손을 타면 악성 코드가 든 커밋에 정상 릴리스 이름이 붙을 수 있습니다. 이렇게 배포 경로 중간을 노리는 공격을 공급망 공격이라고 부릅니다.

서명은 이 틈을 막습니다. 서명은 만든 사람의 키에서 나오므로 저장소나 서버를 장악해도 흉내 낼 수 없습니다. 받는 사람은 태그를 어느 경로로 받았든 서명만 검사하면 됩니다. 저장소는 믿지 않아도 됩니다. 키만 믿으면 됩니다.

서명에 쓰는 공개 키

서명된 태그는 공개 키 암호를 씁니다. 키를 한 쌍으로 만들어 하나는 자기만 갖고 하나는 세상에 공개하는 방식입니다. 자기만 갖는 쪽을 개인 키, 공개하는 쪽을 공개 키라고 합니다.

개인 키로 내용에 서명하면 그 서명은 짝이 되는 공개 키로만 검사가 통과합니다. 내용이 한 글자라도 바뀌면 검사가 실패합니다. 그래서 서명 하나로 누가 만들었는지와 그 뒤로 안 바뀌었는지를 한꺼번에 확인합니다. 이런 서명을 전자 서명이라고 부릅니다.

Git 은 서명을 직접 계산하지 않고 바깥 도구에 맡깁니다. 오래 쓰여 온 도구는 GPG(GNU Privacy Guard)라는 프로그램입니다. GPG 가 도구라면 PGP(Pretty Good Privacy)는 그 도구가 서명을 적는 형식의 이름입니다. 뒤에서 볼 서명 덩어리에 PGP SIGNATURE 라는 이름이 찍히는 것도 이 형식을 따르기 때문입니다.

GPG 말고 다른 키로도 서명할 수 있습니다. 요즘 Git 은 서버에 접속할 때 쓰는 SSH 키(Secure Shell 키)로도 태그에 서명합니다. 기관이 신원을 보증해 주는 전자 문서인 인증서로 서명하는 방법도 있습니다.

만들고 검사하는 명령

아래 세 줄이 서명된 태그를 만들고 검사하는 기본 명령입니다. 오른쪽 주석은 각 줄이 하는 일입니다.

터미널
git tag -s v1.0 -m "1.0"  # 서명 태그 생성
git tag -v v1.0           # 서명 검사
git verify-tag v1.0       # 위와 같은 검사

-s 는 태그를 만들면서 기본 개인 키로 서명하라는 뜻입니다. 서명할 키를 골라 쓰려면 -s 대신 -u <키 이름> 을 줍니다. 서명된 태그는 언제나 주석 달린 태그라서 -m 으로 메시지를 함께 줍니다.

검사가 통과하면 GPG 는 Good signature 라는 말과 함께 서명한 사람의 이름을 보여 줍니다. 태그 내용이 바뀌었으면 BAD signature 가 나옵니다. 서명 없는 태그를 검사하면 서명이 없다는 오류가 납니다.

아래는 실패하는 두 경우입니다. 첫 줄의 v1.0 은 누군가 태그 객체에서 가리키는 커밋을 바꾸고 서명 덩어리는 그대로 붙여 둔 것입니다. 서명은 남아 있지만 바뀐 내용과 맞지 않아 BAD 가 나옵니다. 둘째 줄의 v0.9 는 처음부터 서명 없이 만든 태그입니다.

터미널
git tag -v v1.0  # 바뀜: BAD signature
git tag -v v0.9  # 서명 없음: 오류

서명이 오가는 순서

아래 그림은 서명을 만든 사람에게서 받는 사람에게 가기까지를 보입니다. 저장소는 서명을 나를 뿐이고 검사에는 끼지 않습니다.

sequenceDiagram
    participant 만든사람 as 만든 사람
    participant 저장소
    participant 받는사람 as 받는 사람
    만든사람->>만든사람: 개인 키로 태그에 서명
    만든사람->>저장소: 태그를 푸시
    받는사람->>저장소: 태그를 받아 옴
    저장소-->>받는사람: 태그와 서명
    받는사람->>받는사람: 공개 키로 서명 검사
    Note over 저장소,받는사람: 중간에 태그가 바뀌면 여기서 실패

받는 사람은 만든 사람의 공개 키를 미리 갖고 있어야 합니다. 공개 키는 저장소와 다른 경로로 받습니다. 프로젝트 웹사이트에 올려 두거나, 공개 키를 모아 두는 키 서버에서 받습니다.

태그 객체 안에 든 것

서명된 태그의 속을 보면 서명이 어디 붙는지 알 수 있습니다. git cat-file -p v1.0 은 태그 객체를 글자 그대로 보여 주는 명령입니다. 아래는 그 출력의 예입니다.

object 9fceb02d0ae598e95dc970b74767f19372d61af8
type commit
tag v1.0
tagger 김개발 <dev@example.com> 1735689600 +0900

1.0 릴리스
-----BEGIN PGP SIGNATURE-----

iQEzBAABCAAdFiEE...
-----END PGP SIGNATURE-----

맨 위 object 줄은 이 태그가 가리키는 커밋의 해시입니다. 해시는 내용을 짧은 값 하나로 줄인 지문입니다. 내용이 조금만 달라도 값이 완전히 달라집니다. 그래서 커밋을 해시로 부르면 그 커밋의 내용까지 한 값으로 못박게 됩니다.

그 아래로 태그 이름, 붙인 사람과 시각, 메시지가 이어집니다. 맨 끝의 BEGIN PGP SIGNATURE 덩어리가 서명입니다. 서명은 그 위의 모든 줄을 덮습니다. 그래서 가리키는 커밋을 바꾸든 메시지를 고치든 서명이 맞지 않게 됩니다.

서명 하나가 이력 전체를 덮는 까닭

서명이 직접 덮는 것은 태그 객체 하나뿐입니다. 그런데 Git 에서는 그 하나가 이력 전체를 못박습니다. 커밋끼리 해시로 이어져 있기 때문입니다.

커밋은 파일 목록인 트리의 해시와 바로 앞 커밋인 부모 커밋의 해시를 담습니다. 파일 하나만 바뀌어도 트리 해시가 바뀝니다. 그러면 커밋 해시도 바뀝니다. 부모 커밋이 바뀌어도 자식 커밋의 해시가 따라 바뀝니다.

flowchart TD
    T["서명된 태그 v1.0"] -->|커밋 해시| C3["커밋 3"]
    C3 -->|트리 해시| R["트리 · 파일 목록"]
    R --> F1["파일 내용"]
    C3 -->|부모 해시| C2["커밋 2"]
    C2 -->|부모 해시| C1["커밋 1"]

그림의 화살표는 전부 해시로 가리키는 관계입니다. 커밋 1 의 파일 하나를 몰래 바꾸면 커밋 1 의 해시가 바뀝니다. 그 변화가 커밋 2 와 커밋 3 을 거쳐 태그의 object 줄까지 올라옵니다. 결국 서명 검사가 실패합니다.

이렇게 해시로 엮어 꼭대기 값 하나가 아래 전부를 대표하게 만든 구조를 머클 트리라고 부릅니다. 덕분에 릴리스마다 서명을 한 번만 하면 됩니다.

대신 이 보증은 같은 해시를 내는 두 내용을 일부러 만들 수 없을 때만 성립합니다. 서로 다른 두 내용이 같은 해시를 내는 일을 해시 충돌이라고 합니다. 충돌을 일부러 만들 수 있으면 서명이 덮는 값은 그대로 두고 내용만 갈아 끼울 여지가 생깁니다.

Git 이 써 온 해시 계산 방법은 SHA-1(Secure Hash Algorithm 1)입니다. SHA-1 은 충돌을 일부러 만드는 공격이 가능하다고 알려져 있습니다. 그래서 Git 은 충돌을 만들기 더 어려운 SHA-256 으로 옮겨 가는 중입니다.

검사가 알려 주는 것과 못 알려 주는 것

검사가 통과했다는 것은 「이 공개 키의 짝인 개인 키로 서명했고 그 뒤로 안 바뀌었다」까지입니다. 그 공개 키가 정말 그 사람의 것인지는 검사가 말해 주지 않습니다. 공격자가 자기 키로 서명하고 그 공개 키를 함께 퍼뜨리면 검사는 통과합니다.

그래서 공개 키를 믿을 수 있는 경로로 받는 일이 서명만큼 중요합니다. 한 가지 방법은 키 지문을 맞춰 보는 것입니다. 키 지문은 공개 키의 해시라서 공개 키보다 훨씬 짧습니다. 키 주인을 직접 만나 지문을 대조하면 받은 키가 그 사람 것인지 알 수 있습니다.

GPG 는 신뢰의 웹이라는 방법도 씁니다. 사람들이 서로의 공개 키에 「이 키는 이 사람 것이 맞다」는 보증 서명을 해 주는 방식입니다. 예를 들어 내가 믿는 A 가 B 의 키에 보증 서명을 했다면, 나는 B 를 직접 못 만났어도 B 의 키를 믿을 수 있습니다.

검사는 코드가 안전하다는 것도 말해 주지 않습니다. 서명은 누가 이 커밋을 릴리스로 골랐는지를 보증할 뿐입니다. 그 사람이 실수로 버그를 넣었거나 그 사람의 컴퓨터가 뚫렸다면 서명은 멀쩡합니다.

개인 키를 잃어버리거나 도둑맞으면 더 큰 문제가 생깁니다. 도둑맞은 키로는 누구나 그 사람 이름으로 서명할 수 있습니다. 이럴 때를 대비하는 장치가 둘 있습니다.

키 폐기는 이 키를 더는 믿지 말라고 알리는 일입니다. 폐기 사실은 공개 키를 나눠 주던 경로, 곧 키 서버나 프로젝트 웹사이트에 올려 둡니다. 받는 쪽은 공개 키를 다시 받아 올 때 그 사실을 알게 됩니다.

키 만료는 키를 만들 때 정해 둔 날이 지나면 그 키를 못 쓰게 하는 것입니다. 폐기를 알리지 못한 채 키를 잃어도 피해가 그날까지로 묶입니다.

서명된 커밋과의 차이

Git 은 커밋에도 서명할 수 있습니다. git commit -S 로 만드는 서명된 커밋입니다. 둘은 서명하는 단위와 횟수가 다릅니다.

서명된 태그 서명된 커밋
서명하는 것 릴리스 하나 커밋 하나하나
서명하는 사람 릴리스를 내는 사람 커밋을 만든 사람 각자
보증하는 것 이 커밋까지의 이력을 릴리스로 골랐다 이 커밋을 내가 만들었다
서명 횟수 릴리스마다 한 번 커밋마다 한 번

서명된 태그는 앞 커밋들을 누가 만들었는지까지는 보증하지 않습니다. 해시로 이어진 덕에 내용이 안 바뀌었다는 것만 보증합니다. 기여자마다 자기 커밋을 책임지게 하려면 커밋 서명을 따로 씁니다.

풀 리퀘스트에 실리는 서명된 태그

서명된 태그는 릴리스 말고도 변경을 넘겨줄 때 쓰입니다. 리눅스 커널처럼 메일로 풀 리퀘스트를 주고받는 프로젝트가 그렇습니다.

이런 프로젝트에는 코드의 한 구역을 맡아 변경을 모으는 메인테이너가 여럿 있습니다. 이들은 모은 변경을 윗선 메인테이너에게 넘깁니다. 윗선 메인테이너는 넘겨받은 변경을 최종 저장소에 합칩니다.

메인테이너는 넘길 변경의 마지막 커밋에 서명된 태그를 붙입니다. 태그 메시지에는 이번 변경의 설명을 적습니다. 그리고 그 태그를 가져가 달라는 메일을 git request-pull 로 만들어 보냅니다.

윗선 메인테이너는 태그를 가져와 서명부터 검사합니다. 메일이나 중간 서버가 바꿔치기를 당했어도 서명이 맞지 않으면 바로 드러납니다.

서명된 태그를 병합하면 Git 은 그 태그 객체를 병합 커밋 안에 함께 담아 둡니다. 나중에 git log --show-signature 로 이력을 보면 그 병합에 쓰인 태그의 서명까지 다시 검사할 수 있습니다. 누가 어떤 변경을 넘겼는지가 이력에 영영 남습니다.

관련 항목

서명된 태그가 속하는 태그 종류

태그 · 태그 객체 · 경량 태그 · 주석 달린 태그 · 릴리스 태깅 · 시맨틱 버저닝

서명된 태그가 못박는 Git 구조

커밋 · 부모 커밋 · 트리 · 해시 · 머클 트리 · 커밋 그래프 · 참조 · Git

서명을 만드는 암호 기법

공개 키 암호 · 개인 키 · 공개 키 · 전자 서명 · 해시 함수 · SHA-1 · SHA-256 · 해시 충돌

서명에 쓰는 키와 도구

GPG · OpenPGP · SSH 키 · X.509 · 인증서 · 키 서버 · 키 지문 · 신뢰의 웹

키를 지키고 버리는 절차

키 관리 · 키 폐기 · 키 만료 · 공개키 기반구조 · 하드웨어 보안 키

서명된 태그를 만들고 검사하는 명령

git tag · git verify-tag · git cat-file · git log · git request-pull

서명된 태그와 같은 것을 보증하는 다른 수단

서명된 커밋 · 코드 서명 · 체크섬 · Sigstore · 재현 가능한 빌드

서명된 태그가 막으려는 위협

공급망 공격 · 미러 저장소 · 이력 강제 덮어쓰기 · 강제 푸시 · 무결성

서명된 태그가 오가는 협업 흐름

풀 리퀘스트 · 병합 · 병합 커밋 · 메일링 리스트 · 리눅스 커널 · 버전관리

다른 이름: signed tag · 서명 태그 · GPG 서명 태그 · signed git tag