KC미래기술이 주관한 프로젝트로, 산업용 게이트웨이 장비에 배포되는 Python 클라이언트의 실행 권한을 검증하는 서버다. 장비가 물리적으로 현장에 놓여 있어 공격자가 파일을 자유롭게 읽고 고칠 수 있고, Python이라 디컴파일도 쉽다는 전제에서 출발했다. 그래서 '서버 DB가 통째로 유출돼도 위조가 불가능해야 한다'를 설계 기준으로 삼아, 기기 비밀값을 어디에도 저장하지 않는 구조와 클라이언트 코드 변조를 서버가 잡아내는 이중 방어선을 세웠다. 검증 및 확인(V&V) 대상 시스템이라 시험 절차·테스트 데이터·에러코드 전수 검증까지 함께 만들었다.
Problem
라이선스 검증은 클라이언트가 보낸 값을 서버가 믿는 순간 무의미해진다. 클라이언트는 공격자 손에 있고 서버 DB도 유출될 수 있다. 초기 설계는 기기 비밀값을 DB에 평문 저장했는데, 그 방식은 DB 유출 한 번으로 전 기기 위조가 가능해진다. 또 코드 무결성을 클라이언트가 스스로 검사하게 하면 검사 목록(manifest)째로 바꿔치기가 되므로, 최종 대조 기준을 공격자가 바꿀 수 없는 곳에 둘 필요가 있었다.
My Role & Technical Decisions
비밀값 DB 저장 방식의 기존 설계를 폐기하고 재설계한 뒤 서버를 단독 구현 — 해시·HMAC 직렬화 규칙 확정, /activate·/verify 엔드포인트, secret 무저장 재계산 구조, manifest 정규화 해시(file_hash) 기반 서버 측 무결성 대조, last_count 원자적 갱신, error code 단일 출처 체계, 비밀값 로그 차단, KAT 벡터·403 전수 통합 테스트, Docker·TLS 배포 구성까지 설계·구현했다. 클라이언트(Python·PyArmor)는 이 저장소 범위 밖이며, Ed25519 서명 검증은 클라이언트 몫으로 분리했다.
Architecture
KC License Server 아키텍처발급, 런타임 검증, 폐쇄망 활성화 흐름을 분리했습니다.
원인: 검증마다 재계산하는 비용을 아끼려 저장한 것인데, 이 값은 proof를 위조하는 데 필요한 유일한 재료다 — 저장하는 순간 DB가 곧 마스터키가 된다
해결: 저장을 없애고 매 요청 SHA256(licenseKey ‖ macHash ‖ randomValue) 로 재계산하는 구조로 재설계했다. 세 재료는 DB에 있지만 조합 결과는 어디에도 남지 않는다. 그리고 information_schema.columns를 %secret% 으로 조회해 그런 컬럼이 존재하지 않음을 통합 테스트로 단정했다
배운 점: 비밀값 비저장은 설계 문서로 지켜지지 않는다 — '그 컬럼이 없다'를 테스트가 단정해야 다음 사람이 성능을 이유로 되돌리지 못한다
원인: manifest.json은 검사 대상 목록이지 대상 자체가 아니다. 등록 해시를 새 값으로 고치면 코드와 manifest가 서로 일치해, 로컬에서는 어긋남이 보이지 않는다
해결: manifest 전체의 정규화 해시(file_hash)를 서버 releases에 등록해두고 proof의 입력으로 쓰게 했다. 클라이언트가 계산한 값이 등록값과 다르면 sig가 어긋나 서버가 차단한다. 코드만 변조한 경우와 코드+manifest를 변조한 경우를 각각 재현해, 차단 지점이 클라이언트와 서버로 갈리는 것을 확인했다
배운 점: 무결성 목록을 검사 주체가 들고 있으면 목록째로 바꿔치기가 된다 — 최종 대조 기준은 공격자가 바꿀 수 없는 쪽에 둬야 방어선이 된다
원인: 저장된 카운터를 SELECT로 읽어 비교하고 save() 하는 패턴은 읽기와 쓰기 사이에 다른 트랜잭션이 끼어든다 — 같은 값으로 온 두 요청이 모두 '아직 안 쓰인 값'으로 보인다
해결: 조건부 UPDATE 한 문장(WHERE last_count <> :n)으로 검사와 갱신을 합치고, 영향 행 수 0을 재전송으로 판정하게 했다. 검증 절차도 응답 코드만 보지 않고 카운터가 1차 값에서 멈추는 것을 함께 기록하도록 바꿨다
배운 점: 재전송 방지는 비교의 문제가 아니라 갱신의 원자성 문제다. 그리고 403 응답만으로는 카운터가 덮이지 않았음을 증명할 수 없어, 검증은 상태값을 함께 봐야 한다
Metrics
저장 컬럼 0개
secret 비저장 — information_schema 검사로 테스트가 강제
로컬 통과 → 서버 차단
manifest 동반 변조 이중 방어선 실증
9 / 9
403 error code 전수 통합 테스트 (메타 테스트로 누락 차단)
1.86×
테스트가 프로덕션의 1.86배 (2,361 / 1,267줄 · 95개 테스트)
Infra & Deploy
Java 21 · Spring Boot 3.x · MySQL 8(Flyway 스키마 관리) · Docker Compose · TLS 1.3(자체 서명). crypto 패키지는 Spring 의존성을 갖지 않는 static 유틸로 두어, KAT 벡터 13종(device_secret 4 · proof 3 · file_hash 6)이 애플리케이션 컨텍스트 없이 돈다. file_hash 벡터에는 경로 정렬·널구분자·필드 순서를 각각 어긋뜨린 negative 케이스를 넣었다. 통합 테스트는 H2 대신 MySQL Testcontainers를 쓴다 — CHAR 패딩과 대소문자 collation이 달라 Base64 라이선스 키 조회가 프로덕션과 다르게 동작하기 때문이다. 컨테이너는 싱글턴으로 띄운다(static 컨테이너는 서브클래스마다 종료되는데 스프링 컨텍스트는 캐시되어, 통합 테스트 클래스가 둘 이상이면 죽은 컨테이너를 가리킨다). 릴리스 등록 엔드포인트는 빌드 파이프라인 전용이라 범위 밖으로 두고 수동 등록으로 운영한다. V&V 서버 실측 최대 처리량은 약 330 TPS 이고 지연의 절반 이상이 TLS 핸드셰이크다.