자동 업데이트의 보안 설계 - HTTPS만으로는 부족한 이유

· 업데이트: · · Windows 개발, 보안, updater, 자동 업데이트, 서명, MSIX, ClickOnce

수정 이력(7건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 앞부분에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
발행자를 고정하기 위해 구성했던 `X509Chain`이 현재 시각으로 유효 기간을 다시 판정하고 있던 것을 고쳤습니다. `X509ChainPolicy.VerificationTime`의 기본값은 생성자를 호출한 시각이므로, 서명 인증서가 만료된 시점에 `NotTimeValid`로 실패합니다. 타임스탬프가 붙은 서명은 만료 후에도 `Status = Valid`인 채로 남으므로, 이 형태에서는 「타임스탬프가 통해 유효로 판정된 패키지를 바로 뒤에서 만료로 걸러내는」 결과가 되어, 인증서를 갱신한 순간에 과거 릴리스가 전부 실패합니다. 유효 기간과 신뢰 판정은 `Get-AuthenticodeSignature`로 끝나 있고, 이 `Build`는 발행자를 조사하기 위한 것뿐이므로 `IgnoreNotTimeValid`를 지정하는 형태로 바꿨습니다(서명 시각을 알 수 있다면 `VerificationTime`에 넣습니다). 같은 이유로 해지 확인도 여기서는 하지 않으며, 키 유출 시 멈추는 수단은 blocklist와 minimum allowed version임을 명시하고 있습니다.
Authenticode 검증 예에서 서명자 고정을 `Subject`(식별명) 일치만으로 하던 것을 고쳤습니다. 식별명은 고유 식별자가 아니며, 같은 CN/O/C를 가진 코드 서명 인증서는 사내 CA에서도 공개 CA에서도 발급할 수 있습니다. 그 CA를 클라이언트가 신뢰하고 있으면, 다른 키·다른 발행자로 서명된 교체본이 `Status = Valid`이면서 `Subject` 일치로 통과합니다. `X509Chain`을 구성해 기대하는 발행자(중간 CA/루트)의 thumbprint가 경로에 포함되는지 확인하고, `Subject`는 그 위에서 좁히는 용도로 쓰는 형태로 바꿨습니다. 함께 기댓값을 배열로 두고 이전 값과 새 값을 나란히 받는 전환 절차를 명시하고, 체크리스트에도 항목 2개를 추가했습니다.
TUF의 정식 명칭을 병기하고, 위협과 대책의 대응표(TLS만으로 막을 수 있는지의 열을 포함)를 신설했습니다. 서명된 metadata의 최소 JSON 예와, 검증 전에 URL을 읽지 않는 순서, 정규화를 정하지 않으면 검증이 어긋난다는 점을 추가했습니다. Authenticode 서명을 클라이언트에서 어떻게 검증하는지의 절(`Status=Valid`는 누구의 서명인지를 보장하지 않는다는 점을 포함), 용어표, 신뢰 연쇄 그림도 추가했습니다.
본문 중 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635244)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「자동 업데이트의 보안 설계 - HTTPS만으로는 부족한 이유」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635244 https://comcomponent.com/ko/blog/comcomponent-autoupdate-security/

DOI(최신 버전)
10.5281/zenodo.21635244
DOI(이 버전)
10.5281/zenodo.21635245

목차

  1. 먼저 결론
  2. 왜 자동 업데이트는 위험 영역인가
  3. 나쁜 패턴
  4. 베스트 프랙티스
  5. 최소 안전 구성
  6. Windows 프로젝트에서는 어떻게 볼 것인가
  7. 최소한의 체크리스트
  8. 정리
  9. 참고 자료

이 글의 지식 맵

이 글은 자동 업데이트 기능의 안전성은 TLS만으로는 확보할 수 없고, 클라이언트 측 trust anchor와 서명된 metadata 검증으로 성립한다고 정리합니다. TUF가 정의하는 rollback·freeze·mix-and-match 공격에는 지금까지 확인한 최고 버전을 유지하는 것, 유효 기간, hash·size·version 고정 같은 대책이 유효하며, 검증 실패 시에는 fail-open이 아니라 fail-closed로 두는 것이 권장됩니다. Windows에서는 요건이 맞으면 MSIX App Installer나 ClickOnce를 우선하고, 자체 updater를 만들 때도 Authenticode 서명 검증, 발급자 체인 고정, 타임스탬프가 있는 서명, 키의 분리 운영, helper로의 권한 분리, kill switch나 blocklist에 의한 사고 대응까지 포함해 설계해야 한다고 합니다.

자동 업데이트 보안 설계의 지식 맵자동 업데이트가 signed metadata와 trust anchor를 전제로 rollback·freeze·mix-and-match 공격을 어떻게 막고, MSIX/ClickOnce와의 용도 구분이나 키 관리·권한 분리와 어떻게 연결되는지를 보여주는 그림입니다.전제로 한다전제로 한다이용한다방지한다방지한다방지한다이용한다권장되는 대응사용은 비권장권장되는 대응권장되는 대응전제로 한다에서 확인할 수 있다이용한다전제로 한다이용한다완화한다권장되는 대응전제로 한다이용한다권장되는 대응전제로 한다전제로 한다전제로 한다전제로 한다권장되는 대응전제로 한다자동 업데이트 기능(updater)서명된 업데이트 metadataupdate의 trust anchor(신뢰의 기점)공개 키 암호mix-and-match 공격(갱신)freeze 공격(업데이트)minimum allowed versionrollback 공격(업데이트)TUF(The Update Framework)fail-closed 설계검증 실패 시 동작fail-open 설계MSIX + App InstallerWindows 앱 업데이트 배포ClickOnce코드 서명 인증서Get-AuthenticodeSignatureWinVerifyTrust발급자 체인 고정인증서 체인지문(Thumbprint)코드 서명 타임스탬프인증서 만료서명 키 분리 운영HSM/토큰에 비밀 키 보관updater 권한 분리(helper로 분리)kill switch(업데이트 긴급 중지)업데이트 blocklist

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 27건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

1. 먼저 결론

먼저 실무에서 정리해 둘 합의점을 나열합니다.

  • 요건이 맞다면 먼저 MSIX App Installer나 ClickOnce 같은 기존 업데이트 기반을 우선한다
  • 자체 updater가 필요하면 먼저 넣어야 할 것은 UI가 아니라 서명 검증과 실패 시 복구
  • latest.json 같은 업데이트 정보는 미서명 설정 파일이 아니라 서명된 metadata로 다룬다
  • TLS는 필요하지만 충분 조건은 아니다
  • 업데이트 판정은 「서버가 그렇게 말하고 있으니까」가 아니라 「클라이언트가 검증해 올바르다고 판단할 수 있었으니까」로 한다
  • 서명 키는 개발용과 운영용을 분리하고, HSM이나 서명 서비스로 보호한다
  • 업데이트 실패 시에는 fail-open이 아니라 fail-closed로 한다
  • 롤백 대책이 없는 updater는 취약한 버전으로 되돌려진다는 전제로 생각하는 편이 안전하다
  • 서명 검증을 아직 넣을 수 없는 단계라면, 자동 업데이트보다 수동의 서명된 installer 배포가 더 안전하다

용어만 먼저 고정합니다. 이후에는 이 의미로 씁니다.

용어 의미
fail-closed 이상 시 「멈추는 쪽」으로 기울이는 설계. 서명 검증에 실패하면 업데이트를 진행하지 않는다
fail-open 이상 시에도 「진행하는 쪽」으로 기울이는 설계. 경고만 내고 업데이트를 계속하는 형태
staging 새 버전을 일단 다른 위치에 펼쳐 두고, 검증이 끝난 뒤에 전환하는 방식
kill switch 배포 중인 업데이트를 서버 측 조작으로 즉시 멈추는 메커니즘
trust anchor 클라이언트가 처음부터 신뢰하는 시작점. root 공개키나 고정한 인증서 체인

요컨대 자동 업데이트의 핵심은 「어떻게 다운로드할 것인가」가 아니라 「무엇을 신뢰하고, 어디서 검증하고, 깨졌을 때 어떻게 되돌릴 것인가」입니다.

2. 왜 자동 업데이트는 위험 영역인가

일반 기능은 앱 안에 닫혀 있습니다. 반면 updater는 다음 3가지를 한꺼번에 가집니다.

  1. 외부에서 파일을 가져온다
  2. 그 파일을 신뢰한다
  3. 기존 실행 파일을 교체한다

즉, 임의 코드 실행 경로가 제품 안에 처음부터 들어 있다는 뜻입니다.

여기서 흔한 오해가 「HTTPS니까 안전하다」는 것입니다. 물론 TLS는 필요합니다. 다만 그것으로 지킬 수 있는 것은 주로 통신 경로와 연결 대상의 정당성입니다. 업데이트 서버 자체가 침해됐다, 잘못된 artifact가 정규 CDN에 놓였다, 미서명 manifest가 교체됐다, 같은 이야기에는 그것만으로는 부족합니다.

실제로 TUF(The Update Framework. 소프트웨어 업데이트의 신뢰 모델을 정의한 사양)가 정리한 위협만 봐도, 업데이트 계열에는 이 정도가 있습니다.

  • 임의의 부정 소프트웨어를 넣게 한다
  • 취약한 오래된 버전으로 되돌리는 rollback
  • 새 버전을 보여 주지 않는 freeze
  • 서로 맞지 않는 metadata와 artifact를 섞는 mix-and-match

즉 자동 업데이트는 「파일 전송」이 아니라 「신뢰의 배포」입니다. 여기를 설계해야 비로소 자동 업데이트가 안전하게 돌기 시작합니다.

2.1 위협과 대책의 대응표

위협과 대책은 이 기사에서는 다른 장에 나뉘어 있습니다. 먼저 대응 관계만 한 장으로 모아 둡니다. 설계의 골격은 이것으로 충분합니다.

위협 무엇이 일어나는가 TLS만으로 막을 수 있는가 주요 대책 자세한 장
부정 artifact 배포 공격자가 준비한 파일을 정규 업데이트로 넣게 된다 막을 수 없다(origin 침해, 오배포에 무력) 서명된 metadata와 artifact의 hash / 서명을 클라이언트에서 검증 4.2 / 4.3 / 4.4
rollback 서명은 올바르지만, 알려진 취약점이 있는 이전 버전으로 되돌려진다 막을 수 없다(서명도 TLS도 올바르다) 알려진 최고 release version을 유지하고, 그보다 오래된 것을 거부 4.8
freeze 새 버전이 나왔는데도 오래된 metadata를 계속 돌려 업데이트시키지 않는다 막을 수 없다 metadata에 expires_at을 두고, 너무 오래된 것을 거부 4.3 / 4.8
mix-and-match 서로 맞지 않는 metadata와 artifact 조합을 받는다 막을 수 없다 manifest에 대상 artifact의 hash / size / version을 고정 4.3 / 4.8
서명 키 침해 정규 서명이 붙은 부정 업데이트가 배포된다 막을 수 없다 개발 / 운영 키 분리, HSM이나 서명 서비스, 승인 흐름과 감사 로그, root와 metadata 키 분리 4.5
업데이트 실패·중단 교체 도중에 실패해 앱이 시작되지 않게 된다 대상 외 staging + atomic activate + rollback 4.6
검증 우회 skipVerify 같은 빠져나갈 구멍이 운영에 남는다 대상 외 fail-closed를 사양으로 고정하고, 우회 플래그를 두지 않는다 3.7 / 4.6
사고 시 멈추지 못한다 문제가 있는 버전이 계속 배포된다 대상 외 blocklist, minimum allowed version, kill switch 4.8 / 7

「TLS만으로 막을 수 있는가」 열이 전부 「막을 수 없다」거나 「대상 외」인 것이 이 기사의 취지입니다. TLS는 경로를 지키지만, 배포하는 내용과 배포 권한을 지키는 것은 다른 메커니즘입니다.

3. 나쁜 패턴

먼저 실무에서 자주 보는 위험한 형태를 정리합니다.

나쁜 패턴 무엇이 위험한가 최소한의 고치는 방법
version.json을 HTTPS로 가져와 URL의 zip / exe를 그대로 실행 origin 침해, 설정 교체, 오배포에 약하다 서명된 metadata와 artifact의 클라이언트 검증으로 바꾼다
바이너리만 서명하고 manifest는 미서명 URL, version, channel, 필수 업데이트 플래그를 변조할 수 있다 version / hash / size / channel / expiry를 포함한 signed manifest로 한다
서명 키를 개발 PC나 CI의 파일에 둔다 침해되면 정규 서명이 붙은 멀웨어를 배포당한다 HSM / 서명 서비스 + 승인 흐름 + 감사 로그
업데이트 실패 시 「검증 오류를 무시하고 계속」 사고 때 가장 약한 경로가 열린다 fail-closed로 한다
이전 버전을 남기지 않고 덮어쓰기 업데이트 전원 차단, 디스크 부족, 도중 실패로 시작 불가 staging + atomic activate + rollback
버전 비교만으로 오래된 버전을 허용 취약 버전으로의 rollback이 통과한다 단조 증가하는 release version과 지금까지 본 최고 버전 저장
updater 전체를 관리자 권한으로 돌린다 침해 시 피해 범위가 넓다 다운로드/검증은 낮은 권한, 교체만 최소 helper로 분리
델타 업데이트부터 시작한다 구현이 복잡하고 검증 누락이 늘어난다 우선 전체 패키지 업데이트부터 시작한다

아래에서는 조금 더 자세히 봅니다.

3.1 HTTPS니까 괜찮다,에서 멈춘다

이것이 가장 많습니다.

  • 시작 시 latest.json을 읽는다
  • downloadUrl을 꺼낸다
  • zip / exe를 받는다
  • 풀어 교체한다
  • 종료

겉모습은 그럭저럭이지만, 신뢰의 뿌리가 서버 응답에 지나치게 기대고 있습니다. 업데이트 서버나 배포 설정이 침해되면, 올바른 HTTPS 위에서 부정 업데이트를 배포할 수 있습니다.

TLS는 필요합니다. 다만 TLS만으로는 updater 설계가 끝나지 않습니다.

3.2 서명은 하지만 클라이언트 측에서 검증하지 않는다

릴리스 시 파일에 서명해도, 클라이언트가 그것을 보지 않으면 의미가 없습니다.

흔한 형태는 다음과 같습니다.

  • CI에서는 서명한다
  • 그런데 updater는 hash만 본다
  • 게다가 그 hash 자체가 미서명 manifest에서 온다

이러면 manifest가 교체된 시점에 hash도 함께 바뀝니다. 「hash를 보고 있으니 안전하다」는 hash의 출처까지 지켜야 성립합니다.

3.3 manifest가 미서명

업데이트 계열에서 진짜 지켜야 할 것은 실행 파일 자체만이 아닙니다. 적어도 다음 정보는 변조되면 위험합니다.

  • version / release id
  • 다운로드 대상 URL이나 파일명
  • hash / size
  • channel(stable / beta 등)
  • 필수 업데이트인지 여부
  • 적용 가능한 OS / 아키텍처
  • metadata의 유효 기간
  • 최저 필요 updater version

즉, 업데이트 판단에 쓰는 정보는 전부 signed metadata에 넣는다 정도의 감각이 맞습니다.

3.4 서명 키 취급이 허술하다

업데이트 기능의 안전성은 상당 부분이 키 관리의 안전성입니다.

운영 서명 키가 다음과 같이 놓여 있다면 상당히 위험합니다.

  • 개발 PC의 인증서 저장소에 그대로 넣어 둔다
  • CI의 secret으로 .pfx를 업로드한다
  • 여러 사람이 같은 비밀키를 로컬에 배포한다
  • 개발용 서명과 운영용 서명이 같은 신뢰 사슬이다

이러면 updater 자체가 올바르더라도 「정규 서명된 부정 업데이트」를 막지 못합니다.

3.5 이전 버전을 남기지 않는 덮어쓰기 업데이트

업데이트는 성공 시보다 실패 시의 설계가 중요합니다.

  • 다운로드 도중에 끊겼다
  • 풀기에 실패했다
  • 교체 도중에 전원이 끊겼다
  • 새 버전은 시작됐지만 첫 migration에서 실패했다

이때 이전 버전이 사라지면 복구가 무거워집니다. 실무에서는 「업데이트에 실패했다」는 사실보다 「현장에서 앱이 시작되지 않게 됐다」가 문제가 됩니다.

3.6 rollback을 생각하지 않는다

서명된 정규 버전이라도, 오래된 취약 버전이면 공격자에게 유리한 경우가 있습니다.

예를 들어,

  • version 1.8에 알려진 취약점이 있다
  • 현장은 2.3으로 올라가 있다
  • 공격자가 1.8을 재배포한다

이것이 통과하면 서명 자체는 올바른데도 위험합니다.

「서명되어 있는가」뿐 아니라 「그 버전을 지금 넣어도 되는가」까지 보지 않으면 부족합니다.

3.7 fail-open

운영에서 가장 하면 안 되는 것이 이것입니다.

  • 서명 검증에 실패하면 경고만 내고 계속한다
  • 인증서 만료 오류를 무시할 수 있는 hidden flag가 있다
  • 디버그용 skipVerify=true가 운영에도 남는다

장애 시나 공격 시일수록 이런 우회가 핵심 경로가 됩니다.

4. 베스트 프랙티스

4.1 우선 기존 업데이트 기반을 쓴다

자체 updater가 정말 필요한지는 처음에 의심하는 편이 안전합니다.

Windows라면 요건이 맞는 한 다음을 우선 검토하기 쉽습니다.

  • MSIX + App Installer
  • ClickOnce
  • Store / MDM / 사내 배포 기반
  • MSI + 기업 측 배포 관리

이유는 단순합니다. 업데이트 자체의 책임 범위를 어느 정도 플랫폼 쪽으로 옮길 수 있기 때문입니다. 물론 자유도는 떨어지지만, 업데이트 UI, 배포 manifest, 패키지 서명, 운영과의 맞춤이 쉬워집니다.

자체 updater가 필요해지는 것은 예를 들어 이럴 때입니다.

  • stable / beta / preview의 여러 채널을 엄밀히 제어하고 싶다
  • 단계 배포나 rollout 비율을 갖고 싶다
  • 독자적인 업무 사정으로 업데이트 시점을 세밀히 제어하고 싶다
  • MSIX / ClickOnce에 맞지 않는 구성이 있다

이 경우에도 「자유도가 필요하다」가 아니라 「업데이트 책임을 우리가 진다」로 이해하는 편이 흔들리지 않습니다.

4.2 신뢰의 시작점을 클라이언트 측에 둔다

안전한 updater는 서버 응답을 그대로 믿지 않습니다. 클라이언트 측에 적어도 다음 2가지가 필요합니다.

  1. 신뢰하는 공개키나 인증서 체인
  2. 그 키로 서명된 metadata를 검증하는 메커니즘

바꿔 말하면 「서버가 최신 버전이라고 말한다」가 아니라 「이 metadata는 신뢰하는 서명자가 낸 최신 버전이다」를 클라이언트가 확인할 수 있는 상태를 만들어야 합니다.

신뢰가 root에서 파일까지 어떻게 이어지는지를 그림으로 둡니다.

NGOKNGOKNGOKroot 키클라이언트에 넣는 trust anchormetadata 서명 키root에서 위임하고 자주 갱신한다서명된 update metadataversion / url / hash / size / expiry서명·유효 기간·version을 검증업데이트를 중지fail-closedartifact를 staging 영역으로 다운로드size / hash / 패키지 서명을 검증이전 버전을 남긴 채 activate첫 시작의 상태 확인rollback해 이전 버전으로 되돌린다업데이트 완료

그림에서 잡아 두었으면 하는 것은 2점입니다. 위에서 아래로 신뢰가 한 가닥의 사슬로 이어진다는 것. 그리고 사슬이 어디서 끊겨도 목적지가 업데이트를 중지 또는 rollback이지, 일단 진행이 되지 않는다는 것입니다.

4.3 signed metadata를 중심으로 설계한다

최소한, 업데이트 metadata에는 다음을 넣어 서명 대상으로 합니다.

항목 넣는 이유
release version / release id rollback 방지, 감사
artifact 이름, URL, package type 어떤 파일을 받을지를 고정한다
hash, size 변조 감지, 깨진 배포 감지
channel stable에 beta를 섞지 않는다
대상 OS / architecture 오배포 방지
minimum updater version protocol 변경 시 오래된 updater를 멈춘다
expires_at freeze 대책
published_at 감사, 원인 분리
mandatory / optional 업데이트 UX 분기도 변조 불가로 만든다

여기서 중요한 것은 업데이트 판단에 쓰는 재료를 전부 signed metadata에 모으는 것입니다. 로직은 클라이언트에 두고, 정보의 진정성은 서명으로 지키는 형태로 가져가면 사고가 줄어듭니다.

형태가 보이지 않으면 구현으로 이어지지 않으므로, 최소 예를 적어 둡니다. 먼저 서명의 대상이 되는 내용입니다.

{
  "schema_version": 1,
  "channel": "stable",
  "release_version": "2.4.1",
  "published_at": "2026-04-09T01:00:00Z",
  "expires_at": "2026-04-16T01:00:00Z",
  "minimum_updater_version": "2.0.0",
  "minimum_allowed_version": "2.2.0",
  "mandatory": false,
  "artifacts": [
    {
      "os": "windows",
      "arch": "x64",
      "package_type": "msi",
      "file_name": "MyApp-2.4.1-x64.msi",
      "url": "https://updates.example.com/stable/MyApp-2.4.1-x64.msi",
      "size": 48234496,
      "sha256": "5f2c...64자리 16진..."
    }
  ]
}

이것을 서명과 함께 감쌉니다.

{
  "signed": {
    "schema_version": 1,
    "channel": "stable",
    "release_version": "2.4.1",
    "_comment": "위의 객체를 그대로 넣는다"
  },
  "signatures": [
    {
      "keyid": "3f9a...",
      "sig": "MEUCIQ..."
    }
  ]
}

이 형태로 하는 이유는 업데이트 판단에 쓰는 값이 전부 signed 안쪽에 있기 때문입니다. URL도 version도 channel도 필수 업데이트 플래그도 안쪽이므로, 바깥만 바꿔도 검증에서 실패합니다. 클라이언트 측 처리 순서는 「signed를 검증한다 → 통과하면 그 안의 값만 쓴다」로 고정합니다. 검증 전에 url을 읽고 다운로드를 시작하는 구현은, 이 형태로 만든 의미가 없어집니다.

구현에서 주의할 점이 하나 있습니다. JSON은 키 나열 순서나 공백 넣는 방식에 따라 바이트열이 바뀝니다. 서명 검증은 바이트열에 대해 수행하므로, 서명 대상을 「정규화한 표현」으로 할지 「수신한 바이트열 그 자체」로 할지를 먼저 정하십시오. 여기를 정하지 않고 서버 측은 생성한 객체를, 클라이언트 측은 다시 직렬화한 결과를 서명 대상으로 하면, 올바른 업데이트인데도 검증에 실패합니다. 반대로 이 불일치를 「일단 통과시켜도 된다」로 피하기 시작하면, fail-open으로 가는 첫걸음이 됩니다.

4.4 artifact 자체도 검증한다

metadata를 검증한 뒤, 다운로드한 artifact에서도 다음을 확인합니다.

  • size
  • hash
  • 패키지 서명 / 코드 서명
  • 발행자나 기대하는 식별자

Windows의 PE / MSI / MSIX를 다룬다면 Authenticode나 패키지 서명 검증을 클라이언트 측에서 하는 전제로 두는 편이 안전합니다. macOS라면 Developer ID와 notarization을 업데이트 경로에서도 전제로 두는 편이 흔들리지 않습니다.

4.5 키는 기능이 아니라 운영으로 지킨다

키 관리는 구현보다 운영에서 차이가 납니다.

최소한 다음은 나누는 편이 안전합니다.

  • 개발용 서명 키
  • 스테이징용 서명 키
  • 운영용 서명 키

나아가 운영용은

  • HSM
  • 클라우드 서명 서비스
  • 승인 흐름이 있는 signing system
  • 감사 로그
  • key rotation 절차
  • timestamp가 붙은 서명

까지 포함해 설계하고 싶습니다.

「운영 빌드가 통과하면 CI가 자동 서명한다」는 편리하지만, 침해 시 피해 반경도 커집니다. 적어도 누가 무엇을 언제 서명했는지는 추적할 수 있게 해 두어야 합니다.

운영이 올라오면, 거의 바꾸지 않는 root trust와 자주 재서명하는 업데이트 metadata 키를 나누면 더 안전합니다. root를 offline에 가깝게 유지하고 업데이트 metadata에는 다른 키를 쓰는 설계는, 키 침해 시 피해 반경을 낮추기 쉽습니다.

4.6 fail-closed와 staged update

업데이트 흐름은 다음 순서가 기본입니다.

  1. metadata를 취득
  2. 서명과 유효 기간과 version을 검증
  3. artifact를 staging 영역으로 다운로드
  4. hash / size / 서명을 검증
  5. 이전 버전을 남긴 채 activation 준비
  6. 재시작 시 또는 전용 helper로 전환
  7. 첫 시작의 상태 확인
  8. 문제가 있으면 rollback

여기서 중요한 것은 검증이 끝나기 전에 교체하지 않는다 실패하면 진행하지 않는다 의 두 가지입니다.

4.7 updater의 권한을 줄인다

updater 전체를 관리자 권한으로 돌리는 것은 피하고 싶습니다.

이상은 다음 분리입니다.

  • 다운로드와 검증: 낮은 권한
  • 실제 파일 교체만: 최소 권한을 가진 helper
  • helper는 「검증된 package를 정해진 위치에 둔다」 이상을 하지 않는다

권한 상승이 필요한 설계일수록, 상승 전에 무엇을 검증했는지를 분명히 나누지 않으면 위험해집니다.

4.8 rollback / freeze / mix-and-match를 처음부터 막는다

여기는 나중에 넣으면 힘드므로, 처음에 넣는 편이 좋습니다.

  • rollback 대책 클라이언트는 「지금까지 본 최고 metadata version / release version」을 유지하고, 그보다 오래된 것을 거부한다

  • freeze 대책 metadata에 expiry를 두고, 너무 오래된 metadata를 거부한다

  • mix-and-match 대책 metadata끼리의 일관성을 둔다. 적어도 manifest 자체에 대상 artifact의 hash / size / version을 고정한다

더해, 특정 build를 거부하는 blocklist나 minimum allowed version을 signed metadata로 배포할 수 있으면, 사고 시 봉쇄가 빨라집니다.

TUF를 그대로 채택하지 않더라도, 이 3가지 성질은 상당히 중요합니다.

4.9 처음에는 전체 업데이트부터 시작한다

델타 업데이트는 대역에는 효과가 있지만, 첫 구현으로는 복잡합니다.

  • 어느 이전 버전에서 어느 새 버전으로 적용하는 델타인가
  • 델타 적용 전의 전제 hash
  • 델타 적용 후의 최종 hash
  • 도중 실패 시 복구
  • 부분 적용이나 오래된 델타 정리

이 부분이 한꺼번에 늘어납니다. 초기 버전에서는 서명된 전체 패키지를 안전하게 교체하는 것까지로 충분합니다.

5. 최소 안전 구성

풀 TUF만큼 거창하게 하지 않더라도, 자체 updater의 최소 안전 구성은 대체로 이 형태가 됩니다.

5.1 클라이언트가 가지는 것

클라이언트가 가지는 것은 서버 응답을 의심하기 위한 재료입니다. 여기가 비어 있으면, 업데이트해도 되는지를 서버 말만으로 결정하게 됩니다.

  • 신뢰하는 root 공개키, 또는 고정한 인증서 체인
  • 현재 동작 중인 version
  • 과거에 본 최고 metadata version / release version
  • 허용하는 channel
  • rollback용 직전 버전

5.2 서버가 돌려주는 것

서버가 돌려주는 것은 판단 자체가 아니라 판단의 재료입니다. 어느 것도 단독으로는 신뢰되지 않고, 5.1의 trust anchor를 쓴 검증을 통과해야 비로소 의미가 있습니다.

  • 서명된 update metadata
  • 서명되었거나 platform 서명된 artifact
  • 필요하면 blocklist / minimum allowed version 정보

5.3 전형적인 흐름

metadata를 받는다
  ↓
서명·expiry·version·channel을 검증
  ↓
artifact를 staging으로 받는다
  ↓
size / hash / package signature를 검증
  ↓
이전 버전을 남긴 채 activation
  ↓
첫 시작에 실패하면 rollback

여기서 중요한 것은 업데이트 서버의 응답만으로는 아무것도 성립하지 않는다는 점입니다. 성립시키는 것은 클라이언트가 가진 trust anchor와 검증 로직입니다.

6. Windows 프로젝트에서는 어떻게 볼 것인가

Windows 앱에서는 먼저 배포 방식부터 거꾸로 계산하는 편이 정리하기 쉽습니다.

  • 요건이 맞다면 MSIX App Installer
  • .NET 사내 앱에서 per-user가 맞다면 ClickOnce
  • 서비스, driver, shell extension, 독자 채널 제어까지 필요하면 MSI + 독자 updater도 비교 대상

다만 독자 updater를 골라도 해야 할 일은 줄어들지 않습니다. 오히려 늘어납니다.

  • Authenticode / 패키지 서명 검증
  • signed manifest
  • rollback 대책
  • 업데이트 helper의 권한 분리
  • updater 자신의 업데이트 전략

6.1 Authenticode 서명을 클라이언트에서 어떻게 검증하는가

「Authenticode를 검증한다」고만 쓰면 구현으로 이어지지 않으므로, Windows에서의 입구를 적어 둡니다.

하고 싶은 일 수단
updater 코드 안에서 검증한다 WinVerifyTrust API(wintrust.dll). WINTRUST_ACTION_GENERIC_VERIFY_V2를 지정하면 Authenticode 검증 정책이 적용된다
운영 절차나 CI에서 확인한다 PowerShell의 Get-AuthenticodeSignature
릴리스 작업에서 서명과 확인을 한다 Windows SDK의 signtool sign / signtool verify

PowerShell이라면 최소 확인은 이것뿐입니다.

$path = ".\MyApp-2.4.1-x64.msi"
$sig = Get-AuthenticodeSignature -FilePath $path

if ($sig.Status -ne 'Valid') {
    throw "signature check failed: $($sig.Status) / $($sig.StatusMessage)"
}

# 「서명이 유효」만으로는 부족하다. 누구의 서명인지도 고정한다.
# 다만 Subject(식별명)는 고유하지 않다. 같은 CN/O/C를 가진 인증서는
# 다른 CA에서 얼마든지 발급할 수 있고, 그 CA를 이 단말이 신뢰하면
# Status는 Valid가 되며, Subject 비교도 통과한다.
# 고정해야 할 것은 「발행자 체인」 또는 「공개키」이고, Subject는 그 보조.
$expectedIssuers = @(                      # 발행자(중간CA/루트)의 thumbprint
    '9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B'   # ← 실제 값으로 바꾼다
)
$expectedSubject = 'CN=Example Software Inc., O=Example Software Inc., C=JP'

# 여기서 다시 구성하는 체인은 「발행자를 따라가기」 위한 것일 뿐이고,
# 신뢰할 수 있는지의 판정은 위의 Status = Valid로 끝나 있다.
# X509ChainPolicy의 VerificationTime은 기본값이 생성자를 호출한 시각
# (=현재)이므로, 그대로 Build하면 서명 인증서가 만료된 시점에
# NotTimeValid로 실패한다. 타임스탬프가 붙은 서명은 만료 후에도 Valid인
# 채로 남으므로, 여기를 기본값으로 두면 「올바르게 검증된 과거 릴리스를,
# 인증서 갱신을 넘긴 순간에 전부 걸러내는」 updater가 된다.
# 서명 시각을 알면 그것을 VerificationTime에 넣는다. 모르면
# 기한 판정은 위에서 끝나 있으므로, 이 Build에서는 보지 않는다
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.VerificationFlags = 'IgnoreNotTimeValid'
$chain.ChainPolicy.RevocationMode    = 'NoCheck'
try {
    $built = $chain.Build($sig.SignerCertificate)

    # 서명자부터 루트까지 어딘가에, 기대하는 발행자가 있을 것.
    # Build에 실패하면 체인이 중간까지만 채워지므로,
    # 그 경우에도 이 판정에서 실패한다(fail-closed)
    $chainThumbprints = @($chain.ChainElements | ForEach-Object { $_.Certificate.Thumbprint })
    if (-not ($expectedIssuers | Where-Object { $chainThumbprints -contains $_ })) {
        throw "unexpected issuing chain (built=$built): $($chainThumbprints -join ' / ')"
    }
}
finally {
    $chain.Dispose()
}

if ($sig.SignerCertificate.Subject -ne $expectedSubject) {
    throw "unexpected signer: $($sig.SignerCertificate.Subject)"
}

이때 잡아 두고 싶은 제약이 5가지 있습니다.

  • StatusValid여도, 그것은 「서명으로서 올바르다」는 의미일 뿐입니다. 누구의 서명인지는 따로 확인합니다
  • Subject 일치는 「본인임」의 증명이 되지 않습니다. 식별명은 고유 식별자가 아니기 때문입니다. 사내 CA에서도 공개 CA에서도 CN=Example Software Inc., O=Example Software Inc., C=JP라는 같은 식별명의 코드 서명 인증서를 발급할 수 있습니다. 그 CA를 클라이언트가 신뢰하고 있으면, 다른 키·다른 발행자로 서명된 교체본이 Status = Valid이면서 Subject 일치로 통과합니다. 고정하는 것은 발행자 체인(기대하는 중간 CA / 루트의 thumbprint가 서명자부터 루트까지의 경로에 포함될 것)이거나 공개키로 하고, Subject는 그 위에서 좁히는 용도로 씁니다
  • 고정한 이상, 갱신 절차를 먼저 정해 둡니다. 인증서나 CA를 전환하는 날에 전 단말의 업데이트가 멈춘다는 깨짐이 나오기 때문입니다. 기댓값은 반드시 「배열」로 두고, 이전 값과 새 값을 나란히 받을 수 있는 상태를 만든 뒤에 전환합니다(새 발행자를 허용 목록에 추가한 updater를 먼저 배포한다 → 서명을 전환한다 → 이전 값을 뺀다,의 3단계). 이 「먼저 배포」가 가능한 것은 updater 자신을 업데이트할 수 있을 때뿐이므로, 6장 서두에 적은 「updater 자신의 업데이트 전략」과 한 세트로 설계하십시오
  • timestamp가 붙지 않은 서명은 인증서 유효 기간이 끝난 시점에 검증을 통과하지 못하게 됩니다. 릴리스 시에는 반드시 timestamp를 붙입니다. 4.5에서 timestamp가 붙은 서명을 든 것은 이 이유입니다
  • 발행자를 따라가기 위한 X509Chain은 현재 시각으로 유효 기간을 다시 판정시키지 마십시오. X509ChainPolicy.VerificationTime은 기본값이 생성자를 호출한 시각, 즉 현재입니다. 문서도 「서명된 메시지를 검증하는 경우, 서명은 검증 시가 아니라 서명 시에 유효해야 하므로 이 속성이 중요해진다」고 명시합니다. 여기를 기본값 그대로 두면, timestamp가 통해 Status = Valid가 된 패키지를 바로 뒤에서 「인증서가 만료됐다」고 걸러내는 결과가 됩니다. timestamp를 붙인 의미가 사라질 뿐 아니라, 인증서를 갱신한 순간에 과거 릴리스가 전부 실패합니다. 서명 시각을 알면 그것을 VerificationTime에 넣고, 모르면 IgnoreNotTimeValid를 붙입니다. 약하게 만드는 것이 아닙니다. 유효 기간과 신뢰 판정은 한 단계 위의 Status = Valid로 끝나 있고, 이 Build는 「누가 발급했는가」를 조사하기 위한 것뿐이기 때문입니다. 같은 이유로 해지 확인도 여기서는 하지 않습니다(만료된 인증서의 CRL은 애초에 계속 발행된다고 단정할 수 없습니다). 키가 새었을 때 멈추는 수단은 CRL이 아니라 blocklist와 minimum allowed version(4.8)입니다

또한 검증 결과는 그 단말의 인증서 저장소와 신뢰 설정에 의존합니다. 클라이언트 측 신뢰 설정이 느슨한 환경에서는 Status = Valid의 의미도 느슨해집니다. 위처럼 updater 자신이 기대하는 발행자를 갖고 있으면, 단말 측 신뢰 설정이 넓어져도 영향을 받지 않습니다.

Windows에서 흔한 위험한 형태는 DownloadFile -> unzip -> kill process -> overwrite -> restart의 일직선입니다. 이것은 동작은 하지만, 보안과 복구성 둘 다 약합니다.

SmartScreen이나 UAC 경고를 「상세 정보 → 실행」으로 넘기게 하는 운영은 업데이트 설계가 아니라 경고 길들이기입니다. 올바른 업데이트 경로를 만들려면 경고에 익숙해지게 할 것이 아니라, 경고가 나오기 어려운 배포와 검증 구성으로 가져와야 합니다.

배포 방식 비교 자체는 다음 기사에서도 정리하고 있습니다. Windows 앱 배포 방식 고르기 - MSI/MSIX/ClickOnce/xcopy/자체 업데이트

7. 최소한의 체크리스트

자체 updater를 내기 전에 최소한 이 정도는 확인하고 싶습니다.

  • 업데이트 metadata는 서명되어 있다
  • metadata에는 version / hash / size / channel / expiry가 들어 있다
  • 클라이언트 측에서 서명과 version을 검증하고 있다
  • 서명자 고정을 식별명(Subject)이 아니라 발행자 체인 또는 공개키로 하고 있다
  • 고정한 값의 전환 절차(이전 값과 새 값을 나란히 받는 기간)가 정해져 있다
  • artifact의 hash와 platform 서명을 검증하고 있다
  • 운영 서명 키는 개발 환경에서 분리되어 있다
  • 키 이용 로그와 승인 기록이 남는다
  • timestamp가 붙은 서명을 쓰고 있다
  • staging 업데이트로 이전 버전을 남긴 채 전환한다
  • rollback의 조건과 절차가 있다
  • 검증 실패 시 fail-closed로 멈춘다
  • updater 자체의 업데이트 방침이 있다
  • blocklist / minimum allowed version을 배포할 수 있다
  • 단계 배포를 멈추는 kill switch가 있다
  • 실패율, rollback 율, 서명 검증 실패를 관측할 수 있다

이 체크리스트에 빈칸이 많다면, 먼저 updater UI를 만들기보다 배포 신뢰 모델을 다지는 편이 효과가 있습니다.

8. 정리

자동 업데이트 기능의 보안에서 생각할 것은 결국 이것에 모입니다.

업데이트의 편리함이 아니라, 누구를 신뢰하고, 그 신뢰를 클라이언트가 어떻게 검증할지를 설계한다.

그 위에서 실무용 판단을 거칠게 말하면 이렇습니다.

  • 기존 기반으로 충분하면 우선 그것을 쓴다
  • 자체 updater를 만든다면 HTTPS보다 먼저 서명된 metadata와 키 관리를 넣는다
  • 실패 시 복구와 rollback을 설계하지 않는 updater는 운영에서 힘들다
  • updater는 배포 기능이 아니라 제품의 보안 경계 그 자체다

만약 지금 구성이 latest.json + zip 교체에 가깝다면, 먼저 고쳐야 할 것은 다운로드 처리보다 신뢰를 어디에 둘 것인가입니다. 여기를 고치는 것만으로 위험도는 상당히 바뀝니다.

9. 참고 자료

관련 토픽

이 주제와 가까운 토픽 페이지입니다. 기사를 기점으로 관련 서비스나 다른 기사로 나아갈 수 있습니다.

Windows 기술 토픽

Windows 개발, 장애 조사, 기존 자산 활용의 기술 토픽을 모은 입구입니다.

이 주제가 이어지는 서비스

Windows 앱 개발

자동 업데이트는 UI만의 이야기가 아니라 배포 방식, 권한, 복구, 운영까지 포함하는 설계입니다. Windows 앱의 신규 개발이나 기존 소프트웨어 재검토에서, 업데이트 방식 정리부터 대응할 수 있습니다.

기술 상담·설계 리뷰

「독자 updater가 필요한가」「MSIX / ClickOnce로 충분한가」「지금 업데이트 설계의 어디가 위험한가」 같은 정리 단계부터 상담할 수 있습니다.

저자 프로필

小村 豪

합동회사 고무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로, 기존 자산이 남은 프로젝트나 원인이 잘 보이지 않는 장애 조사에 강점이 있습니다.

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

Windows 앱 개발

Windows 앱의 배포, 업데이트, 서명, rollback, MSIX / ClickOnce 선정은 구현뿐 아니라 배포 방식과 운영 설계까지 포함해 생각해야 하기 때문입니다.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

자동 업데이트는 HTTPS로 통신하고 있으면 안전한 것 아닌가요?
TLS는 필요하지만 충분 조건은 아닙니다. TLS로 지킬 수 있는 것은 주로 통신 경로와 연결 대상의 정당성이며, 업데이트 서버 자체가 침해된 경우나, 잘못된 artifact가 정규 CDN에 놓인 경우, 미서명 manifest가 교체된 경우에는 대응하지 못합니다. 업데이트 판정은 「서버가 그렇게 말하고 있으니까」가 아니라 「클라이언트가 서명된 metadata를 검증해 올바르다고 판단할 수 있었으니까」로 해야 합니다.
업데이트용 metadata에는 무엇을 넣어 서명해야 하나요?
업데이트 판단에 쓰는 재료를 전부 signed metadata에 모으는 것이 기본입니다. 구체적으로는 release version, artifact의 URL과 파일명, hash와 size, channel(stable / beta 등), 대상 OS나 아키텍처, minimum updater version, metadata의 유효 기간(expires_at), 필수 업데이트인지 여부 플래그 등입니다. 바이너리만 서명하고 manifest를 미서명인 채로 두면, URL이나 버전이나 필수 업데이트 플래그를 변조할 여지가 남습니다.
롤백 공격이란 무엇인가요? 어떻게 막나요?
서명된 정규 버전이라도, 알려진 취약점이 있는 오래된 버전을 공격자가 재배포해 취약한 버전으로 되돌리는 공격입니다. 서명 자체는 올바르므로 서명 검증만으로는 막을 수 없습니다. 대책으로, 클라이언트는 지금까지 본 최고 release version을 유지하고 그보다 오래된 것을 거부합니다. 함께 metadata에 유효 기간을 두어 새 버전을 보여 주지 않는 freeze 공격을 막고, manifest에 artifact의 hash·size·version을 고정해 mix-and-match 공격도 막아 두어야 합니다.
자체 updater를 만들어야 하나요, 아니면 기존 메커니즘을 써야 하나요?
요건이 맞다면 먼저 MSIX App Installer나 ClickOnce 같은 기존 업데이트 기반을 우선하는 것이 안전합니다. 업데이트 책임 범위를 플랫폼 쪽으로 옮길 수 있기 때문입니다. 자체 updater가 필요해지는 것은 여러 채널의 엄밀한 제어나 단계 배포처럼 기존 기반에 맞지 않는 요건이 있는 경우입니다. 그 경우에도 먼저 넣어야 할 것은 UI가 아니라 서명 검증과 실패 시 복구이며, 검증에 실패하면 진행하지 않는 fail-closed를 지키고, 이전 버전을 남긴 채 전환해 롤백할 수 있게 합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기