포스트

차돈 제작기 #3 - 인증키, 서명 업데이트, 그리고 세 번의 보안 감사

차돈 제작기 #3 - 인증키, 서명 업데이트, 그리고 세 번의 보안 감사

차돈 제작기 #3 - 인증키, 서명 업데이트, 그리고 세 번의 보안 감사

차돈 제작기의 마지막 글이다. 앞선 두 글에서는 정비일지 중심 데이터 모델과, 타 앱 반입·백업·모바일 조회·문자 발송을 정리했다.

이번 글은 “이제 팔아도 되는가?”라는 질문에 답하는 과정이다.

차돈은 판매를 전제로 한 프로그램이고, 남의 돈 장부를 다룬다. 그래서 마지막 단계는 기능이 아니라 세 가지였다.

1
2
3
정품 인증은 어떻게 하는가?
업데이트 통로가 공격 통로가 되지 않는가?
인증키가 유출돼도 남의 장부는 안전한가?

1. 인증키: PC 1대 바인딩과 7일 유예

정품 인증부터. 인증키는 CHDN-XXXX-XXXX-XXXX 형태로, 서버에서 crypto.getRandomValues 기반 12자(31진, 약 59비트)로 발급한다. 열거는 불가능한 공간이다.

  • 첫 실행 시 인증키 입력 창이 뜨고, 인증 없이는 사용할 수 없다
  • 키는 최초 인증한 PC 1대에 자동으로 묶인다
  • 발급/정지/기기 해제는 관리자 도구로 처리한다

여기서 제일 중요한 결정은 오히려 덜 엄격하게 만드는 것이었다.

1
인터넷이 끊겨도 마지막 인증 성공 후 7일간은 정상 동작한다.

정비소 PC는 인터넷이 자주 끊긴다. 공유기가 죽었다고 장부 프로그램이 안 열리면 그날 장사를 망친다. DRM이 정품 사용자를 괴롭히는 순간 프로그램은 버려진다. 그래서 오프라인 유예 7일을 두고, 메인 화면 하단에 인증 상태만 표시하게 했다.


2. 자동 업데이트: 서명 없는 exe는 설치되지 않는다

자동 업데이트는 배포 채널이 곧 공격 통로가 될 수 있어서 제일 조심했다. 업데이트 서버를 장악당하면 전국 정비소 PC에 악성코드를 배포하는 통로가 된다.

그래서 업데이트 검증을 fail-closed로 만들었다.

1
2
3
4
5
앱 시작 5초 후 latest.json 확인
→ (버전 + exe SHA-256)에 대한 Ed25519 서명 검증
→ https 강제 + 다운로드 호스트 allowlist 확인
→ 다운로드한 exe의 SHA-256이 서명된 값과 일치하는지 재확인
→ 전부 통과해야 교체·재시작

공개키만 앱에 내장하고, 개인키는 저장소 밖에 보관한다. 배포 서버가 통째로 털려도 개인키 없이는 유효한 서명을 만들 수 없으니 가짜 업데이트를 설치시킬 수 없다. 미서명/위변조 파일은 설치 자체를 거부한다.

배포는 태그 하나로 끝난다.

1
2
git tag v3.0.3
git push origin v3.0.3

GitHub Actions가 exe를 빌드하고 서명해서 배포 저장소(chadon-release)에 올리면, 몇 분 뒤 사용자 앱이 새 버전을 감지해 원클릭으로 스스로 교체된다.

한글 exe 이름이 만든 업데이트 버그

이 파이프라인에서 제일 어이없는 버그는 암호학이 아니라 인코딩에서 나왔다.

exe 교체는 배치 파일을 띄워서 하는데, 배치 본문에 한글 rem 주석이 섞인 채 encoding="ascii"로 쓰다 보니 UnicodeEncodeError가 터져서 사용자에게는 “설치를 시작하지 못했습니다”만 떴다. v2.0.0부터 v2.0.5까지의 오류다.

해결은 원칙 하나로 했다 — 배치 본문은 rem 주석까지 ASCII만 쓰고, 한글 경로(차돈.exe)는 환경변수로 전달한다. 덤으로 교체 뒤 ie4uinit으로 셸 아이콘 캐시를 새로고침해서, 예전 버전 아이콘이 작업표시줄에 남는 문제도 같이 잡았다. 서명 검증보다 이 배치 파일이 더 오래 걸렸다.


3. 1차 감사: 외부 공격자

공개 배포 직전에 전체 보안 감사를 한 번 돌렸다. 위협 모델은 “밖에서 서버를 두드리는 사람”이었다.

  • 공개 저장소/소스 전역 비밀 스캔 + git 히스토리 전수 스캔 → 유출 없음
  • 관리자 API는 Bearer 토큰 필수, 미설정 시 전면 401 (fail-closed)
  • 모바일 로그인은 인증키 + PIN 2요소, PIN은 PBKDF2-SHA256 해시로만 저장
  • 토큰·PIN 비교는 전부 상수시간 비교로 교체
  • 모바일 페이지가 CDN에서 받는 sql.js/wasm은 SRI + SHA-384 핀 대조를 통과해야만 실행
  • PIN 무차별 대입은 5회 실패 시 KV 공유 잠금 10분

이때 결과는 “출시 가능”이었다. 그런데 진짜 문제는 다음 감사에서 나왔다.


4. 2차 감사: 공격자가 정품 exe를 갖고 있다면

2차 감사에서 위협 모델을 바꿨다.

1
2
정품 exe를 가진 다른 사용자(또는 인증키를 손에 넣은 사람)가
서버 API를 직접 호출해 다른 정비소의 자료를 볼 수 있는가?

여기서 Critical 1건(C1)이 나왔다. IDOR(권한 없는 객체 참조)이 연쇄되는 구조였다.

핵심 원인은 잘못 고른 2차 인증 요소였다. 백업 관련 API는 인증키에 더해 machine_id를 요구했는데, 이 값은 앱이 sha256(MAC주소 | 컴퓨터이름)으로 만드는 비밀이 아닌 값이다. 같은 사무실·같은 랜에 있던 사람이면 계산으로 알아낼 수도 있다. 그런데 백업 정보 조회 응답이 아무 검사 없이 이 값을 그대로 돌려주기까지 했다. 결국 “키가 유출돼도 자료는 안전하다”는 1차 감사의 전제가 성립하지 않았다. 인증키 하나만 알면 백업 조회·복원·모바일 계정 설정·덮어쓰기· 삭제가 줄줄이 뚫리는 경로가 존재했다.

문제의 뿌리는 하나였다 — 비밀이 아닌 값을 인증 요소로 썼다. 그래서 진짜 비밀인 기기 토큰을 새로 도입했다(TOFU, Trust On First Use).

1
2
3
4
최초 인증 때 서버가 난수 32바이트를 딱 한 번 발급한다.
서버는 그 SHA-256 해시만 KV에 보관한다.
앱은 토큰 원본을 device.key 파일에 두고 모든 클라우드 요청에 실어 보낸다.
백업 관련 API는 이 토큰을 상수시간으로 대조해야만 통과시킨다.

설계에서 신경 쓴 부분들.

  • 응답에서 machine_id를 제거하고, 모든 백업 API에 동일한 권한 검사를 적용했다
  • 기기 토큰은 chadon.db 에 둔다 → 클라우드 복원으로 DB를 통째로 바꿔도 기기 등록이 유지된다
  • 토큰이 한 번 등록되면 그 키의 옛 machine_id 폴백은 영구히 닫힌다(다운그레이드 불가)
  • 관리자 reset(기기 해제)이 토큰 해시도 폐기 → PC 교체·기기 분실 시 정상 복구 경로
  • 관리 API 응답은 device_registered(true/false)만 노출하고 해시는 감춘다

검증은 회귀 테스트로 못 박았다. 인증키만 아는 상대가 백업 조회·목록·복원· 업로드·삭제·모바일 계정 설정을 시도하면 전부 거부되고, 정상 사용자는 그대로 동작하는지를 앱↔서버 실연동 E2E로 확인했다.


5. 3차 감사: 구글 검색으로 닿을 수 있는가

세 번째 위협 모델은 코드가 아니라 노출면이었다.

1
2
구글 검색·도킹(intitle:, inurl:, site:)만으로
관리 콘솔이나 자료·배포 정보에 닿을 수 있는가?

여기서 나온 문제들은 심각도는 낮았지만 현실적이었다.

  • 관리자 콘솔 색인: /admin 응답에 색인 차단 장치가 하나도 없었다. workers.dev가 주는 기본 robots.txt는 지시자가 한 줄도 없어 사실상 “크롤 허용”이었다. 모든 응답에 X-Robots-Tag: noindex, HTML에 <meta robots>, Cache-Control: no-store를 넣었다.
  • robots.txt로 전면 차단(Disallow: /)은 일부러 안 했다. 크롤을 막으면 검색엔진이 noindex를 읽지 못해 URL만 색인되는 역효과가 난다.
  • 관리 API 무차별 대입: 주소가 알려진 뒤를 대비해 IP별 5회 실패 시 15분 durable 잠금을 추가했다.
  • 인증서버 주소 유출: 공개 페이지에 workers.dev 주소가 클릭 가능한 링크로 박혀 있었다. 사용자에게 노출되는 주소를 공식 도메인 chadon.mokky.store/review 하나로 고정했다.
  • 평문 비밀: 관리자 토큰이 Cloudflare에 Secret이 아니라 평문 변수로 들어가 있었다. 대시보드·API 응답에 값이 그대로 노출되는 상태라, 값을 보존한 채 Secret으로 재등록했다.
  • 모바일 토큰 서명키를 관리자 토큰과 공유하던 것도 별도 난수로 분리했다.

검색 노출 관점에서 원래부터 잘 돼 있던 것도 확인했다. 인증키·토큰은 전부 POST 본문으로만 오가서 URL·Referer·브라우저 히스토리에 남지 않고, 자료를 주는 엔드포인트는 모두 POST + 토큰이라 크롤러가 색인할 경로 자체가 없었다.


6. 재현 가능한 증거로 마무리

감사를 “했다”로 끝내지 않으려고, 검증을 재현 가능한 스위트로 남겼다.

1
2
3
4
5
# 서버 레드팀 스위트
node server/test_worker_security.mjs   # 115개 보안 체크 전부 통과

# 앱↔서버 실연동 E2E (정상 사용자 vs 인증키 탈취자)
python3 server/test_client_e2e.py      # 정상은 통과, 탈취자는 전부 차단

그리고 배포된 실물의 무결성도 독립 확인했다. 배포 저장소에 올라간 Chadon.exe의 실제 SHA-256이 latest.json의 값과 일치하고, 그 (버전+해시) 메시지에 대한 Ed25519 서명이 앱 내장 공개키로 유효 검증되는지를 직접 대조했다. 자동 업데이트가 정상 동작하고, 위변조 시 fail-closed로 거부된다는 것을 눈으로 확인한 것이다.

물론 완벽하지는 않다. 감사 보고서에는 수용한 잔존 위험도 정직하게 남겼다 — 클라우드 백업 blob은 운영자(Cloudflare 계정 보유자)는 열람 가능하다는 점 (종단간 암호화는 브라우저에서 DB를 여는 모바일 조회 구조와 충돌해서 채택하지 않았다), 무료 한도 소진형 가용성 공격은 코드만으로 완전히 막기 어렵다는 점 등이다. 막은 것과 못 막은 것을 나눠 적는 것까지가 감사였다.


마무리

차돈은 거대한 제품은 아니다. tkinter로 창 하나 띄우는 데서 시작해서, 정비일지 한 줄에 부품값·공임을 나눠 담는 데이터 모델을 세우고, 남의 프로그램에서 갈아타는 통로를 만들고, 장부가 안 죽게 이중 백업을 걸고, 마지막에 세 번의 보안 감사를 거친 프로그램이다.

만들고 나서 보니 결국 이 프로그램의 신뢰는 두 문장으로 요약됐다.

1
2
장부가 안 날아간다.
남이 내 장부를 못 본다.

작은 프로그램이지만, 남의 돈 장부를 다루는 프로그램은 화려한 기능보다 데이터가 안 죽고, 아무나 못 여는 구조가 먼저라는 걸 배운 프로젝트였다. 그리고 그 구조는 코드 한 번으로 완성되는 게 아니라, 위협 모델을 바꿔가며 세 번을 다시 두드려 봐야 겨우 믿을 만해진다는 것도.

이 글은 모든 권리 보유 입니다. 출처를 밝힌 게시글 링크 공유만 가능하며, 전문 복제·전재는 허용되지 않습니다.