자동 업데이트, 그리고 서명이 한 겹 더 필요한 이유

1편에서는 Developer ID 인증서와 공증 설정을 마쳤습니다. 이것으로 앱을 사용자에게 처음 전달할 준비는 끝났습니다. 하지만 앱은 한 번 배포하고 끝이 아니라, 버그를 고치고 기능을 더한 새 버전을 계속 내보내야 합니다.

Mac App Store 앱이라면 업데이트를 App Store가 알아서 처리합니다. 직접 배포 앱은 그렇지 않으므로, 앱 안에 자동 업데이트 기능을 직접 넣어야 합니다. macOS에서 이 역할의 사실상 표준은 오픈소스 프레임워크 **Sparkle**입니다. Sparkle을 넣으면 앱이 주기적으로 “업데이트 피드(appcast)“를 확인하고, 새 버전이 있으면 사용자에게 알리고 내려받아 설치해 줍니다.

여기서 한 가지 의문이 생깁니다. 1편에서 만든 Developer ID 인증서로 이미 앱에 서명하는데, 왜 또 다른 키가 필요할까요?

이유는 두 서명이 검증하는 대상이 다르기 때문입니다.

  • Developer ID 인증서 — macOS Gatekeeper가 “이 앱을 설치해도 되는가"를 판단할 때 사용
  • Sparkle EdDSA 키 — 앱 안의 Sparkle이 “방금 내려받은 업데이트 파일이 정말 이 앱의 개발자가 만든 것인가“를 판단할 때 사용

자동 업데이트는 인터넷에서 파일을 내려받아 자기 자신을 덮어쓰는, 보안상 매우 민감한 동작입니다. 만약 누군가 업데이트 서버나 통신 경로를 가로채 가짜 파일을 끼워 넣으면 큰 문제가 됩니다. Sparkle은 이를 막기 위해, 개발자만 가진 비공개 키로 서명한 업데이트만 받아들이고, 서명이 맞지 않으면 설치를 거부합니다. 인증서와는 별개의 검증 층인 셈입니다.

이 글에서는 그 검증에 쓰일 EdDSA(Ed25519) 키 한 쌍을 만듭니다.

1편과 마찬가지로, 모든 이름·경로(FocusTimer, example.com 등)는 예시 값입니다. 실제로는 본인 앱 정보로 바꿔 사용하세요.

전제 — 앱에 Sparkle이 추가돼 있어야 한다

키를 만들기 전에, 앱 프로젝트에 Sparkle 프레임워크가 의존성으로 들어가 있어야 합니다. 아직이라면 Xcode에서 **Swift Package Manager(SPM)**로 추가합니다.

  1. Xcode에서 프로젝트 열기 → File → Add Package Dependencies…
  2. 검색창에 저장소 주소 입력: https://github.com/sparkle-project/Sparkle
  3. 버전 규칙을 2.x(최신 메이저)로 두고 추가

이렇게 한 뒤 프로젝트를 한 번 빌드하면 SPM이 Sparkle 패키지를 내려받습니다. 이때 같이 따라오는 명령줄 도구들이 다음 단계의 핵심입니다.

1단계 — Sparkle 명령줄 도구 찾기

Sparkle 패키지에는 키 생성·서명에 쓰는 명령줄 도구가 함께 들어 있습니다. 이 도구들은 SPM이 패키지를 내려받아 둔 폴더 안에 있는데, 그 위치는 Xcode 버전과 DerivedData 설정에 따라 달라집니다. 그래서 직접 찾아내는 편이 안전합니다.

SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData \
  -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1)
echo "$SPARKLE_BIN"

경로 한 줄이 출력되면 성공입니다. 아무것도 안 나온다면 앞 단계의 “프로젝트를 한 번 빌드” 가 빠진 것이니, Xcode에서 빌드를 한 번 돌린 뒤 다시 실행하세요.

이 폴더 안에는 다음 도구들이 있습니다.

  • generate_keys — 서명 키 생성·백업·복원·확인 (이 글에서 사용)
  • sign_update — 업데이트 파일에 서명 (실제 릴리스 때 사용)
  • generate_appcast — 업데이트 피드(appcast.xml) 생성 (3편에서 등장)

2단계 — 서명 키 생성

이제 키 한 쌍을 만듭니다.

"$SPARKLE_BIN/generate_keys"

다음과 비슷한 출력이 나옵니다.

Generating a new signing key...
A key has been generated and saved in your keychain. Add the SUPublicEDKey
key to the Info.plist of each app for which you intend to use Sparkle...

    <key>SUPublicEDKey</key>
    <string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>

이 한 번의 명령으로 키 두 개가 만들어집니다.

  • 공개 키(public key) — 위 출력에 보이는 SUPublicEDKey 값. 비밀이 아니며, 앱 안에 박아 넣을 키입니다.
  • 비공개 키(private key) — 화면에는 나오지 않습니다. macOS 키체인에 “Private key for signing Sparkle updates"라는 항목으로 자동 저장됩니다. 절대 평문 파일로 디스크에 남기면 안 되는, 진짜 비밀입니다.

공개 키를 앱에 심기

출력된 공개 키 문자열을 앱의 Info.plist에 넣습니다. 예시 앱 기준으로 FocusTimer-Info.plist에 다음 키들을 추가합니다.

<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
  • SUPublicEDKey — 방금 생성한 공개 키. 앱은 이 키로 내려받은 업데이트의 서명을 검증합니다.
  • SUFeedURL — 업데이트 피드의 주소. 아직 이 도메인은 없습니다. 3편에서 만듭니다. 지금은 자리만 잡아 두는 셈입니다.

공개 키가 앱에 박혀 있고 비공개 키는 개발자만 가지고 있으므로, 비공개 키로 서명한 업데이트만 그 앱이 받아들이게 됩니다. 이것이 Sparkle 업데이트 검증의 핵심 구조입니다.

3단계 — 비공개 키 백업 (반드시!)

이 단계를 건너뛰면 나중에 크게 후회할 수 있습니다.

비공개 키는 키체인에 저장돼 있으므로 지금 쓰는 컴퓨터에서는 문제없습니다. 하지만 컴퓨터를 잃어버리거나, 디스크가 망가지거나, macOS를 새로 설치하면 이 키도 함께 사라집니다.

비공개 키가 사라지면 어떻게 될까요? 새 키로 서명한 업데이트는 기존 사용자의 앱(예전 공개 키가 박힌 앱)이 검증을 거부합니다. 즉 이미 앱을 쓰고 있는 사용자들에게 자동 업데이트를 영영 보낼 수 없게 됩니다. 사용자에게 일일이 “새 버전을 직접 받아 다시 설치하세요"라고 안내하는 수밖에 없습니다.

그러니 키를 만든 직후 바로 백업합니다.

"$SPARKLE_BIN/generate_keys" -x ~/focustimer-sparkle-private.key
cat ~/focustimer-sparkle-private.key

cat이 출력한 한 줄짜리 base64 문자열이 비공개 키입니다. 예시:

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=

이 문자열을 1Password 같은 비밀번호 관리자에 보안 메모(secure note)로 저장합니다. 메모 이름은 나중에 찾기 쉽게 FocusTimer Sparkle EdDSA Private Key 같은 식으로 붙여 둡니다.

저장을 확인한 직후, 디스크에 남은 평문 파일을 삭제합니다.

rm ~/focustimer-sparkle-private.key

비공개 키를 평문 파일로 디스크에 방치하지 않는 것이 원칙입니다. 백업본은 암호화된 비밀번호 관리자 안에만 두세요.

핵심 함정 — % 기호는 키가 아니다

cat으로 키를 출력할 때, 터미널(특히 zsh)이 줄 끝에 % 기호를 붙여 보여줄 수 있습니다.

Hn4Kp9Lr2Qs5Tv8Wx1Yz3Ab6Cd0Ef7Gh4Ij5Kl8MnQ0=%

%는 “줄바꿈 없이 출력이 끝났다"는 것을 알려주는 셸의 표시일 뿐, 키의 일부가 아닙니다. 백업본에 이 %까지 복사해 넣으면 나중에 복원할 때 키가 망가집니다. base64 문자열은 보통 =로 끝나므로, = 뒤의 %는 빼고 저장하세요.

4단계 — 다른 컴퓨터에서 복원하기

새 컴퓨터에서 앱을 빌드해야 할 때, 백업해 둔 비공개 키를 키체인에 다시 넣습니다.

echo "백업해둔_base64_문자열" > ~/focustimer-sparkle-private.key
"$SPARKLE_BIN/generate_keys" -f ~/focustimer-sparkle-private.key
rm ~/focustimer-sparkle-private.key

-f 옵션은 “파일에 든 키를 키체인으로 불러오기"를 뜻합니다. 복원이 끝나면 여기서도 평문 파일을 바로 삭제합니다.

5단계 — 확인

키가 제대로 들어가 있는지 확인합니다.

"$SPARKLE_BIN/generate_keys" -p

공개 키 한 줄이 출력됩니다. 이 값이 2단계에서 Info.plistSUPublicEDKey에 넣은 값과 정확히 일치해야 합니다. 다르다면 앱이 박고 있는 공개 키와 실제 서명 키가 어긋난 것이므로, 업데이트 검증이 실패하게 됩니다.

2편 정리

여기까지 따라왔다면 이제 다음을 갖췄습니다.

  • ✅ Sparkle EdDSA 키 한 쌍 생성 (공개 키 + 비공개 키)
  • ✅ 공개 키를 앱의 Info.plist(SUPublicEDKey)에 심음
  • ✅ 비공개 키를 비밀번호 관리자에 안전하게 백업
  • ✅ 다른 컴퓨터에서의 복원 방법 숙지

이제 앱은 “내려받은 업데이트가 진짜인지” 검증할 수단을 갖췄습니다. 그런데 한 가지가 비어 있습니다. 2편에서 SUFeedURLhttps://updates.example.com/appcast.xml을 적어 뒀지만, 그 주소에는 아직 아무것도 없습니다.

다음 편에서는 업데이트 피드(appcast.xml)와 .dmg 파일을 올려 둘 공개 저장소를 만들고, 우리가 통제하는 도메인으로 연결하며, 빌드 설정까지 마무리해 사전 설정을 끝냅니다.