사전 코드 서명
개념

코드 서명

gabury1고친 사람 github-actions[bot]

코드 서명은 내려받은 프로그램을 누가 만들었고 그 뒤로 바뀌지 않았는지를 받는 쪽이 확인하게 해 주는 일입니다. 만든 쪽이 프로그램 파일에 전자 서명을 붙여서 내보냅니다. 운영체제나 설치 도구는 실행하기 전에 그 서명을 확인합니다. 서명이 맞지 않는 파일은 막습니다.

쉽고 빠른 이해

코드 서명은 프로그램 파일에 「우리가 만든 그대로입니다」라는 확인값을 붙이는 일입니다. 휴대폰은 앱을 설치하기 전에 그 값부터 확인합니다.

프로그램은 만든 사람 손을 떠나 여러 서버를 거쳐 도착합니다. 그 길 어딘가에서 악성 코드가 끼어들어도 파일 이름만 봐서는 알 수 없습니다.

  1. 만든 쪽이 자기만 가진 키로 파일에 서명합니다
  2. 「이 키는 이 회사 것」이라는 인증서를 함께 붙여 내보냅니다
  3. 받는 쪽 기기가 서명과 인증서를 확인합니다. 어긋나면 경고하거나 실행을 막습니다

대가도 있습니다. 서명 키를 도둑맞으면 남이 그 회사 이름으로 서명할 수 있어서, 키를 지키는 데 품이 듭니다. 그리고 서명은 누가 만들었는지만 알려 줄 뿐 코드가 안전한지는 알려 주지 않습니다.

서명은 남에게 건네는 결과물에 붙입니다. 받는 쪽이 배포 경로를 들여다볼 수 없어서 파일이 스스로 출처를 증명해야 하기 때문입니다. 반대로 팀이 직접 빌드해 직접 서버에 올리는 서비스 코드는 서명 대신 그 경로의 접근 권한을 지키는 경우가 많습니다.

상세

옛날 편지는 봉투를 밀랍으로 봉하고 그 위에 집안의 인장을 찍었습니다. 받는 사람은 인장 무늬를 보고 누가 보냈는지 압니다. 밀랍이 깨지지 않았으면 오는 길에 아무도 열어 보지 않았다는 것도 압니다.

코드 서명은 이 두 가지 확인을 프로그램 파일에 해 줍니다. 만든 쪽이 파일에 서명을 붙여 내보내면, 받는 쪽은 파일을 열기 전에 그 서명을 확인합니다. 서명하는 대상은 실행 파일, 라이브러리, 설치 패키지, 모바일 앱, 스크립트처럼 기기에서 돌아갈 코드를 담은 파일입니다. 예를 들어 사내 도구의 설치 파일에 회사 서명을 붙이면, 직원 컴퓨터는 그 파일이 회사 빌드 서버에서 나온 그대로인지 확인한 뒤에 설치합니다.

붙이는 서명은 전자 서명 그 자체입니다. 전자 서명은 짝을 이룬 두 키를 씁니다. 서명은 만든 쪽만 가진 개인 키로 만듭니다.

확인은 누구에게나 공개한 짝인 공개 키로 합니다. 개인 키가 없는 사람은 맞는 서명을 만들어 낼 수 없습니다.

서명이 없으면 생기는 일

프로그램 파일은 만든 사람의 손을 떠나 여러 곳을 거칩니다. 빌드 서버, 배포 서버, 다운로드 미러, 사용자의 디스크가 그 길입니다. 이 가운데 한 곳만 뚫려도 공격자는 파일에 악성 코드를 끼워 넣을 수 있습니다. 바뀐 파일도 이름과 아이콘은 원래 파일과 똑같습니다.

흔히 떠올리는 대책은 파일의 체크섬을 다운로드 페이지에 같이 적어 두는 것입니다. 체크섬은 파일 내용을 해시 함수에 넣어 얻은 짧은 요약값입니다. 파일이 한 바이트만 바뀌어도 값이 달라지니, 전송 중에 깨진 파일은 이것으로 잡힙니다. 하지만 다운로드 페이지까지 손에 넣은 공격자는 파일과 체크섬을 함께 바꿔 버립니다.

서명은 이 틈을 막습니다. 공격자가 파일을 바꾸면 원래 서명은 더는 맞지 않습니다. 만든 쪽 이름으로 새 서명을 만들려면 만든 쪽의 개인 키가 있어야 하는데, 그 키는 만든 쪽의 손을 떠나지 않습니다. 그래서 받는 쪽이 바꿔치기를 알아챕니다.

서명하고 확인하는 순서

서명을 만드는 쪽과 확인하는 쪽이 하는 계산은 다릅니다. 양쪽 모두 먼저 파일 전체를 해시 함수에 넣어 요약값을 얻습니다. 앞 절의 체크섬과 같은 방식으로 얻는 값입니다. 파일이 아무리 커도 서명할 것은 이 요약값 하나입니다.

만든 쪽은 요약값을 개인 키로 계산해 서명값을 얻습니다. 이 서명값을 파일에 붙여 내보냅니다. 받는 쪽은 받은 파일로 요약값을 다시 계산합니다. 그리고 공개 키를 써서 서명값이 그 요약값에 맞는지 확인합니다.

아래 그림은 한 번의 배포에서 양쪽이 무엇을 하는지 보입니다. 사이에 낀 배포 경로는 파일을 나르기만 하고 서명을 새로 만들지 못합니다.

sequenceDiagram
    participant 만든 as 만든 쪽
    participant 경로 as 배포 경로
    participant 기기 as 받는 쪽 기기
    Note over 만든: 파일의 요약값을 구한다
    Note over 만든: 요약값을 개인 키로 서명한다
    만든->>경로: 파일과 서명값
    경로->>기기: 받은 것을 나른다
    Note over 기기: 받은 파일의 요약값을 다시 구한다
    Note over 기기: 공개 키로 서명값이 요약값에 맞는지 따진다
    Note over 기기: 맞으면 실행하고 어긋나면 경고하거나 막는다

공개 키의 주인을 보증하는 인증서

확인하려면 받는 쪽이 만든 쪽의 공개 키를 알아야 합니다. 그런데 공개 키를 파일에 같이 실어 보내기만 하면 구멍이 생깁니다. 공격자가 자기 키 한 쌍을 새로 만들어, 바꾼 파일에 자기 개인 키로 서명하고 자기 공개 키를 붙이면 확인은 통과합니다. 서명이 맞는지와 그 키가 누구 것인지는 다른 물음입니다.

그래서 공개 키에 「이 키는 이 회사의 것」이라는 보증을 붙입니다. 이 보증 문서가 인증서입니다. 인증서에는 공개 키와 주인의 이름이 들어 있습니다.

인증서가 보증이 되려면 믿을 만한 제3자가 거기에 다시 서명해야 합니다. 이 제3자를 인증 기관(CA, Certificate Authority)이라고 부릅니다.

인증 기관의 공개 키도 누군가 보증해야 합니다. 이 물음이 끝없이 이어지지 않게, 운영체제는 믿기로 한 인증 기관의 인증서 몇 개를 처음부터 품고 나옵니다. 이것이 루트 인증서입니다. 운영체제는 루트 인증서를 다른 보증 없이 믿고 시작합니다.

루트 인증서의 키가 새면 그 아래의 보증이 모두 무너집니다. 게다가 루트 인증서는 기기마다 박혀 있어서 쉽게 바꿀 수 없습니다. 그래서 루트의 키는 날마다 쓰지 않고 따로 깊이 보관합니다. 평소의 보증은 루트가 보증한 중간 인증서가 대신 맡습니다.

회사가 받는 인증서는 이 중간 인증서가 보증합니다. 코드에 서명하는 용도로 발급한 이 인증서를 코드 서명 인증서라고 부릅니다.

루트에서 중간 인증서를 거쳐 코드 서명 인증서까지 보증이 줄줄이 이어진 줄을 인증서 체인이라고 부릅니다. 받는 쪽은 파일에 붙은 인증서에서 시작해 자기가 품은 루트 인증서까지 거슬러 올라가 봅니다.

아래 그림은 그 줄을 위에서부터 그린 것입니다. 맨 위가 믿고 시작하는 루트 인증서이고, 맨 아래가 받은 파일입니다.

flowchart TD
    R["루트 인증서 · 운영체제가 처음부터 품는다"] -->|보증한다| M["중간 인증서"]
    M -->|보증한다| C["코드 서명 인증서 · 회사 이름과 공개 키"]
    C -->|이 공개 키로 확인한다| F["서명된 파일"]

공개 키를 보증하는 쪽의 갈래

인증 기관만 보증을 서는 것은 아닙니다. 누가 공개 키를 보증하느냐에 따라 코드 서명은 크게 네 갈래로 나뉩니다.

누가 보증하나 받는 쪽이 믿는 근거 흔히 보는 곳
인증 기관 루트 인증서까지 이어지는 인증서 체인 데스크톱 설치 파일 · 드라이버
플랫폼 운영자 플랫폼에 등록한 개발자의 키 모바일 앱 스토어
관리자 관리자가 미리 등록해 둔 저장소의 공개 키 리눅스 패키지 관리자
보증하는 쪽 없음 처음 설치할 때 본 키 [[Android

둘째 줄은 플랫폼 운영자가 보증을 맡는 경우입니다. 개발자는 먼저 플랫폼에 자기 신원과 서명 키를 등록합니다. 앱 스토어는 등록한 개발자가 그 키로 서명해 올린 앱만 받아 줍니다. 받는 쪽은 인증 기관 대신 이 등록을 근거로 앱의 주인을 믿습니다.

백엔드 개발자가 가장 자주 만나는 것은 셋째 줄입니다. 서버에 새 패키지 저장소를 추가할 때, 그 저장소의 공개 키부터 등록하라는 안내를 봅니다. 그 키가 없거나 바뀌면 패키지 관리자는 서명을 확인할 수 없다며 설치를 멈춥니다.

넷째 줄에는 제3자가 없습니다. 안드로이드 앱은 개발자가 스스로 만든 인증서로 서명해도 설치됩니다. 이런 인증서는 자기 개인 키로 자기 공개 키에 서명한 것이라 누구의 보증도 아닙니다. 이것을 자기 서명 인증서라고 부릅니다.

대신 기기는 처음 설치한 앱의 키를 기억해 둡니다. 그리고 업데이트가 같은 키로 서명됐을 때만 앱을 덮어씁니다. 처음 한 번을 믿은 뒤로는 같은 주인인지만 확인하는 셈입니다.

서명을 붙이는 두 방식

서명값을 어디에 두느냐도 둘로 갈립니다. 하나는 파일 안에 넣는 방식입니다. 실행 파일이나 앱 묶음은 파일 형식 안에 서명값과 인증서를 담을 칸을 따로 둡니다. 받는 쪽 운영체제는 파일을 열 때 그 칸을 알아서 꺼내 봅니다.

다른 하나는 원래 파일을 한 바이트도 건드리지 않고 서명을 별도 파일로 옆에 두는 방식입니다. .sig 나 .asc 같은 서명 파일을 따로 만들어 함께 내려받게 합니다. 이 방식을 분리 서명이라고 부릅니다. 서명 칸이 없는 압축 파일이나 소스 묶음을 배포할 때 이 방식을 씁니다.

인증서가 만료된 뒤의 서명

인증서에는 유효 기간이 있습니다. 확인하는 쪽이 오늘 날짜만 기준으로 삼으면, 인증서가 만료되는 날 그 인증서로 서명한 파일이 전부 무효가 됩니다. 몇 해 전에 받아 잘 쓰던 설치 파일이 어느 날 갑자기 경고를 띄우게 됩니다.

이를 막으려고 서명할 때 타임스탬프 기관의 확인을 함께 받습니다. 만든 쪽이 서명값을 타임스탬프 기관에 보내면, 기관은 「이 서명값은 이 시각에 이미 있었다」는 문장에 자기 서명을 붙여 돌려줍니다. 만든 쪽은 이것을 서명과 함께 파일에 붙입니다.

받는 쪽은 오늘 날짜가 아니라 그 시각을 기준으로 인증서의 유효 기간을 확인합니다. 서명할 때 인증서가 유효했다면, 지금은 만료됐어도 서명을 받아들입니다.

서명 키가 샜을 때

코드 서명에서 가장 지켜야 할 것은 개인 키입니다. 키를 손에 넣은 사람은 그 회사 이름으로 무엇에든 서명할 수 있습니다. 받는 쪽은 그 서명과 진짜 서명을 가릴 수 없습니다.

키가 샌 것을 알면 인증 기관에 알려 인증서를 무효로 돌립니다. 이것을 인증서 폐기라고 합니다. 받는 쪽은 서명을 확인할 때 인증 기관이 공개한 폐기 목록도 함께 봅니다. 폐기된 인증서로 한 서명은 받아들이지 않습니다.

키가 새는 일을 줄이려고 개인 키를 하드웨어 보안 모듈(HSM, Hardware Security Module)에 넣어 두기도 합니다. 이 장치는 키를 밖으로 내보내지 않습니다. 서명해 달라는 요청만 받아 안에서 계산합니다. 빌드 서버가 뚫려도 공격자가 키 자체를 들고 나갈 수는 없습니다.

서명이 말해 주지 않는 것

서명이 알려 주는 것은 둘뿐입니다. 누가 서명했는지와, 서명한 뒤로 파일이 바뀌지 않았는지입니다. 그 코드가 안전한지는 알려 주지 않습니다. 악의를 품은 개발자도 인증서를 받아 자기 프로그램에 서명할 수 있습니다.

서명하기 전 단계가 뚫리는 경우도 있습니다. 공격자가 빌드 서버에 들어가 소스나 빌드 도구에 악성 코드를 심으면, 그 코드는 정상 빌드를 거쳐 회사 키로 서명됩니다. 받는 쪽이 보기에는 흠 없는 서명입니다. 이렇게 배포 과정의 앞 단계를 노리는 공격을 공급망 공격이라고 합니다.

서명이 막는 것은 빌드가 끝난 뒤의 바꿔치기입니다. 빌드 앞 단계는 다른 장치가 맡습니다. 소스 저장소의 권한 관리나, 같은 소스에서 같은 파일이 나오는지 확인하는 재현 가능한 빌드가 그런 장치입니다.

코드 서명을 만나는 곳

대상마다 서명을 확인하는 쪽이 다릅니다. 서명이 없거나 어긋날 때 벌어지는 일도 다릅니다. 개발하다 부딪히는 경우를 모으면 아래와 같습니다.

서명하는 대상 확인하는 쪽 서명이 없거나 어긋나면
데스크톱 설치 파일 운영체제 경고를 띄우거나 실행을 막습니다
모바일 앱 운영체제 · 앱 스토어 설치하지 않습니다
리눅스 패키지 패키지 관리자 설치를 멈춥니다
컨테이너 이미지 서명 확인을 켠 클러스터 그 이미지로 컨테이너를 띄우지 않습니다
디바이스 드라이버 운영체제 커널 드라이버를 불러오지 않습니다

표의 결과는 설정에 따라 달라집니다. 컨테이너 이미지처럼 확인을 따로 켜야 동작하는 대상이 있습니다. 모바일 앱처럼 운영체제가 처음부터 막는 대상도 있습니다.

남에게 건네는 결과물일수록 서명이 필요합니다. 받는 쪽은 만든 쪽의 배포 경로를 들여다볼 수 없어서 파일 자신이 출처를 증명해야 합니다. 반대로 팀이 직접 빌드해 직접 서버에 올리는 서비스 코드는 파일마다 서명하지 않는 경우가 많습니다. 경로 전체를 팀이 쥐고 있으니 그 경로의 접근 권한을 지키는 것으로 대신합니다.

관련 항목

코드 서명이 기대는 암호 기술

전자 서명 · 공개 키 암호 · 공개 키 · 개인 키 · 키 쌍 · 해시 함수

공개 키의 주인을 보증하는 인증서 체계

인증서 · X.509 · 인증 기관 · 루트 인증서 · 중간 인증서 · 인증서 체인 · 코드 서명 인증서 · 자기 서명 인증서 · 공개 키 기반 구조

서명의 유효 기간을 다루는 장치

타임스탬프 기관 · 인증서 만료 · 인증서 폐기 · 인증서 폐기 목록 · OCSP

서명 키를 지키는 수단

하드웨어 보안 모듈 · 키 관리 · 비밀 관리 · 키 교체

코드 서명이 막으려는 공격

중간자 공격 · 공급망 공격 · 악성 코드

서명된 결과물이 지나는 배포 단계

빌드 · 실행 파일 · 라이브러리 · 패키지 관리자 · 컨테이너 이미지 · 아티팩트 저장소 · 앱 스토어 · 배포

서명을 확인하는 실행 환경

운영체제 · Android · Windows · macOS · 디바이스 드라이버 · 보안 부팅 · Kubernetes

코드 서명과 함께 쓰는 무결성 보장 수단

무결성 · 체크섬 · 분리 서명 · 패키지 서명 · 커밋 서명 · 재현 가능한 빌드 · SBOM · 메시지 인증 코드

다른 이름: code signing · 코드 사이닝 · 코드서명