민간 건축사업자가 건물을 신축할 때, 인접한 국가유산 보존지역이나 군사시설 비행안전구역의 높이제한을 사전에 검토해야 합니다. 문제는 군사시설의 정확한 고도제한 수치는 안보상 이유로 비공개라는 점입니다.
ZeCret은 이 수치(Z)를 공개하지 않고도 "계획 건물이 기준을 위반하는지" 이진 판정만 제공하는 서비스입니다. 일조권·문화재 기준도 내부적으로는 margin을 계산하지만, 화면에는 모든 카테고리 공통으로 위반/적합 결과만 노출합니다.
| 항목 | 내용 |
|---|---|
| 개발 기간 | 2주 |
| UI | Streamlit |
| 배포 | AWS EC2 (GitHub Actions CI/CD) |
- 군사시설 등 공개제한구역은 정밀 높이제한 정보 공개가 규정상 제한됨
- 민간 사업자는 정확한 기준을 모른 채 위반 여부를 사전 확인해야 함 → **"안보상 비공개"**와 **"민간의 사전 확인 필요성"**의 구조적 충돌
- 해법: 높이제한 기준값(Z)을 복호화하지 않고 비교 연산 자체를 암호화된 상태로 수행하는 동형암호(CKKS) — 군사시설 카테고리에 실제 TenSEAL 연산으로 구현
① 건축사업자가 신축 예정 건물 정보(위치 X,Y + 계획 높이 + 인접대지경계선 이격거리) 입력
② 계획 위치 기준 반경 내 국가유산·군사시설 참조 데이터 자동 조회
③ 3개 카테고리(일조권 / 국가유산 / 군사시설)를 각각 독립 판정
④ 군사시설 기준값은 암호문 상태로만 존재 — 서비스는 암호문-평문 동형 뺄셈까지만 수행(TenSEAL CKKS).
최종 복호화(부호 비트)는 관리기관 HSM 역할의 오프라인 스크립트(scripts/mock_authority_verify.py)가 수행,
서비스는 이진 결과만 전달받음
⑤ 모든 카테고리 공통으로 위반/적합 이진 결과만 표시 — 지도에는 계획 건물 위치 1개만 마커로 표시
- 건축사업자 입력(평문, 판정 대상): 위치(X,Y)·계획 높이·인접대지경계선 이격거리
- 참조 데이터: 인접 국가유산/군사시설 위치·기준값 — 현재
src/compliance/config.py에 하드코딩된 샘플, 실 데이터 연동은 이후 단계
법정 수치 기준이 없는 항목(예: 조망권)은 오판정 리스크가 있어 범위에서 제외했습니다.
| # | 카테고리 | 근거 | 규칙 | margin(내부) | 화면 노출 |
|---|---|---|---|---|---|
| 1 | 일조권 사선제한 | 건축법 제61조, 시행령 제86조 | 9m 이하 → 1.5m 이상 이격 / 9m 초과 → 높이의 1/2 이상 이격 | 실제 수치 | 이진 결과만 |
| 2 | 국가유산 경관보호 | 문화재보호법, 유산별 개별 고시 | 계획 높이가 허용 높이 초과 여부 | 실제 수치 | 이진 결과만 |
| 3-1 | 군사시설 제한보호구역 고도제한 | 군사기지 및 군사시설 보호법 제9조 | 계획 높이가 비공개 기준값 초과 여부 | 항상 None |
이진 결과만 |
| 3-2 | 군사시설 비행안전구역 기본표면 | 군사기지 및 군사시설 보호법 제10조 | 계획 높이가 비공개 기본표면 초과 여부 | 항상 None |
이진 결과만 |
군사시설은 규정 테마(regulation_theme) 단위로 독립 판정합니다 — 같은 시설(서울공항)도 제9조·제10조가 중첩 적용되어 서로 다른 비공개 기준값(Z)을 가지며, 계획 높이에 따라 테마별로 위반 여부가 갈릴 수 있습니다.
세 카테고리 모두 evaluate_height_compliance(facility_type, plan_height, reference_value) -> dict(src/compliance/rules.py)로 구현되며, LangGraph 파이프라인(src/graph)의 plain_compute_node(일조권/국가유산) · he_compute_node+authority_verify_node(군사시설, 테마별 1회씩)가 실행을 담당합니다. Streamlit UI(app.py)는 run_full_compliance_check()가 반환한 결과를 렌더링만 하며, margin은 데이터 계층에는 실수치가 채워져도 화면에는 절대 렌더링하지 않습니다.
CKKS로 Z값을 암호화해도, 판정 결과(위반/적합 1비트)를 반복 질의하면 이진탐색으로 원본 기준값을 역산할 수 있습니다. 이는 특정 암호 스킴의 결함이 아니라, "비교 결과 1비트를 반환하는 인터페이스" 자체가 갖는 근본적 한계입니다(Dinur–Nissim reconstruction theorem, Sparse Vector Technique과 동일한 구조의 문제).
대응: authority_verify_node가 복호화 직전 (facility_id, regulation_theme) 조합의 누적 질의 횟수에 하드 캡을 걸고, 예산을 초과하면 exceeds_limit을 지어내지 않고 판정 자체를 거부합니다(src/security/query_budget.py, 기본값 HE_QUERY_BUDGET_PER_REGULATION=50).
authority_verifyLangfuse span에query_count/query_budget을 구조화된 필드로 남겨, 예산 초과 시 span이ERROR로 마킹되고 정상 조회와 오라클 probing 시도를 실시간으로 구분할 수 있습니다.- CKKS 근사 연산 자체의 한계도 있습니다: 계획 높이가 기준값과 부동소수점 단위로 완전히 같은 극단 케이스는 노이즈가 부호를 임의로 결정할 수 있습니다. 파라미터를 키우거나 부트스트래핑을 붙여도 근본적으로 해결되지 않는, 근사 연산 고유의 한계입니다. 실사용자는 비공개 기준값을 정확히 맞힐 수 없으므로 실질적 영향은 제한적이라 판단했습니다.
- 향후 개선 방향: 현재는 요청자 구분 없는 전역 카운터이므로, 요청자별 독립 질의예산 적용을 계획하고 있습니다.
근거와 잔여 위험에 대한 상세 내용은 docs/oracle_defense.md에서 다룹니다.
원 체크포인트는 "좌표(X,Y)를 CKKS로 암호화"를 전제로 하지만, ZeCret은 좌표는 평문, 높이(Z)만 암호화하는 방향으로 재해석했습니다 — 서울공항·남한산성의 위치는 이미 공개 정보이고, 안보상 비공개인 것은 그 구역의 고도제한 수치뿐이기 때문입니다(CLAUDE.md 원칙 3). 상세 근거는 docs/checkpoint_mapping.md 참고.
| 체크포인트 | 적용 방식 |
|---|---|
| ① 암호화 저장 | 군사시설 기준값을 HeightLimitCiphertext(실제 TenSEAL CKKS, src/he/encryption.py)로 저장. 암호화는 오프라인 스크립트(scripts/generate_mock_ciphertexts.py)가 사전 수행 → 암호문 캐시(src/db/ciphertext_cache.py)에 저장, 서비스는 조회만 함. 비밀키는 scripts/keys/에만 있고 서비스 코드는 이를 import하지 않음 |
| ② 범위검색 | 계획 위치 기준 반경(ADJACENCY_RADIUS_M) 내 인접 시설만 판정 대상으로 검색 — 좌표는 공개 정보이므로 평문 반경검색, 추려진 시설에 대해서만 Z를 동형암호로 비교 |
| ③ 시각화 대안 | 모든 카테고리가 이진 결과만 표시(정밀 수치 UI 없음). 지도에는 계획 건물 위치 1개만 표시, 인접 시설 위치는 올리지 않음 |
| ④ 토큰 참조값 | HE:{facility_id}:{regulation_theme} 형식 토큰(src/tokens.py::issue_token) — 클라이언트에는 ciphertext hex/바이트 길이 대신 참조 토큰만 전달 |
- ⚖️ 3종 높이 컴플라이언스 판정 — 일조권 / 국가유산 / 군사시설(제9조·제10조 각각)
- 🔒 군사시설 기준값 비공개 처리 (실제 TenSEAL CKKS) — 서비스는 비밀키 미보유, 동형 뺄셈까지만 수행. 최종 복호화는 관리기관 HSM 역할의 오프라인 스크립트가 수행
- 🔑 Mock 관리기관 사전 준비 (
scripts/generate_mock_ciphertexts.py) — 키쌍 생성, 샘플 Z값 암호화·캐시 저장. 비밀키는 서비스 코드가 절대 import하지 않음 - 🔍 반경 검색 (
src/compliance/search.py) — 국가유산은 1km, 군사시설은 시설 유형별 반경(전술항공작전기지 5km, 군사기지법 제5조 근거)을 적용. 시설명·거리는 반환하되 높이(Z)는 어디에도 등장하지 않음 - 🖥️ 관제센터형 대시보드 UI — 판정 현황 요약, 카테고리·규정 테마별 이진 결과 패널, 계획 건물 위치 지도. 좌측 폼에 4개 값을 입력 후 "🔍 검색"을 눌러야 실행됨(자동 재계산 없음)
- 🗺️ 지도 시각화 (
src/geo) — 계획 건물 마커, 반경 원, 격자 단위 위험도(개별 시설의 정밀 위치는 그리지 않음), VWorld WFS 기반 실제 건물 footprint 2.5D 배경 - 🕸️ LangGraph 파이프라인 — 군사시설:
he_compute_node→authority_verify_node/ 그 외:plain_compute_node→ 공통으로rag_check_node→llm_summarize_node - 🗄️ 구조화 기준값 DB (SQLite) — facility_id 기반 정확 대조로 재검증 (벡터 검색 아님)
- 📚 RAG 근거 인용 (ChromaDB) — 조문 청크를 facility_id로 정확 조회, 판정에는 관여하지 않음
- 🗣️ LLM 판정 설명 — 이미 확정된 결과(bool)와 RAG 근거만으로 설명문 생성, 재판단/임의 수치 언급 금지(CLAUDE.md 원칙 5). API 실패 시 결정론적 템플릿으로 자동 대체
- 🔭 Langfuse 트레이싱 — 계획 높이·좌표·암호문은 절대 기록하지 않고
facility_id/regulation_type/latency_ms/exceeds_limit만 allowlist 기록 - 🤖 AI Agent 채팅 (
src/agent) — 판정 결과·반경검색 결과를 그라운딩 컨텍스트로 사용. 군사시설 실제 기준값/margin은 어떤 형태로 캐물어도 추측하지 않고 거절(위반/적합 여부는 항상 공개). CLOVA Studio(HyperCLOVA X) function-calling 지원, 실패 시 3단계 폴백(tool-calling → 단발 LLM 호출 → 규칙 기반)
| 영역 | 사용 기술 |
|---|---|
| 언어 | Python 3.10+ |
| UI | Streamlit |
| 동형암호 | TenSEAL (CKKS), poly_modulus_degree=8192, coeff_mod_bit_sizes=[60,40,60], global_scale=2**40 |
| LLM | 네이버클라우드 CLOVA Studio(HyperCLOVA X), 기본 모델 HCX-005, 키 미설정 시 템플릿 폴백 |
| RAG | ChromaDB — 법령 조문 청크, facility_id 정확 조회 전용 |
| 트레이싱 | Langfuse — 노드별 span, 민감 필드 allowlist 마스킹 |
| 오케스트레이션 | LangGraph — search_zone_node → he_compute_node/plain_compute_node → authority_verify_node(military) → rag_check_node → llm_summarize_node |
| 구조화 DB | SQLite — facility_id·regulation_type·height_limit_m 등 |
| 배포 | 단일 Dockerfile → AWS EC2, GitHub Actions CI/CD |
| 구분 | 출처 | 비고 |
|---|---|---|
| 신축 예정 건물 | 화면 직접 입력 | 위치·계획 높이 모두 평문, 별도 저장소 없음 |
| 인접 국가유산 | src/compliance/config.py 하드코딩 |
남한산성 — 좌표는 실좌표 근사, 허용높이는 placeholder |
| 인접 군사시설 | src/compliance/config.py 하드코딩 |
서울공항(성남비행장·K-16) — 좌표·법적 근거는 실재 기준, Z값은 안보상 비공개라 placeholder |
| 구조화 기준값 DB | src/db(런타임 생성) |
rag_check_node의 정확 대조에 사용 |
| RAG 벡터DB | src/rag(런타임 생성) |
조문 청크 + 메타데이터, 설명문 근거로만 사용 |
| 암호문 캐시 | src/db/ciphertext_cache.db(저장소 커밋) |
원본 Z값은 저장하지 않아 커밋해도 안전 |
| 공개 컨텍스트 | src/he/public_context.bin(저장소 커밋) |
비밀키 미포함 |
| 관리기관 비밀키 | scripts/keys/authority_secret_context.bin |
서비스 코드는 참조하지 않음, git/이미지 어디에도 미포함(gitignore/dockerignore) |
git clone <repo-url>
cd zecret
pip install -r requirements.txt
# [필수, 최초 1회] Mock 관리기관 사전 준비
python scripts/generate_mock_ciphertexts.py
streamlit run app.py테스트 (판정 스키마 검증, 정밀 수치 비노출 검증, RAG 조회, LLM 노드 판정 불변성, Langfuse redaction, 실제 CKKS 결과의 baseline 일치, 질의예산 오라클 방어 등):
pytestPhase 7 벤치마크(Mock 대 실제 CKKS 속도 비교):
python scripts/benchmark_he.pyDocker:
docker build -t zecret .
docker run --rm -p 8501:8501 --env-file .env \
-v "$(pwd)/scripts/keys:/app/scripts/keys:ro" \
zecretCLOVASTUDIO_API_KEY=your_clovastudio_api_key
CLOVASTUDIO_MODEL=HCX-005
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_HOST=https://cloud.langfuse.com
AWS_ACCESS_KEY_ID=your_aws_key
AWS_SECRET_ACCESS_KEY=your_aws_secret
VWORLD_API_KEY=your_vworld_api_key
HE_QUERY_BUDGET_PER_REGULATION=50
.github/workflows/ci-cd.yml이 CI(test → build)와 CD(deploy)를 함께 관리하며, 대상은 EC2 단일 인스턴스입니다.
| 잡 | 트리거 | 내용 |
|---|---|---|
test |
모든 push/PR | pytest -q — 추가 시크릿 없이 항상 동작 |
build-and-push |
main push |
Docker 이미지 빌드 → GHCR push. 암호문 캐시는 커밋된 고정 산출물 사용(CI가 매번 새 키쌍을 만들지 않음) |
deploy |
build 성공 후 | EC2 SSH 접속 → 이미지 pull → 컨테이너 재시작. EC2_HOST 시크릿 없으면 스킵 |
필요한 GitHub Secrets: EC2_HOST, EC2_USER, EC2_SSH_KEY
EC2에 미리 준비할 것: Docker + docker 그룹 권한, /opt/zecret/.env, /opt/zecret/keys/authority_secret_context.bin(scp로 배치), 8501 포트 인바운드 허용
| 리스크 | 대응 |
|---|---|
| 군사시설 Z값 실데이터 부재 | 시설 정체성·좌표·법적 근거는 실재 기준으로 접지, Z값만 placeholder (PoC 목적 명시) |
| 반복 질의로 인한 Z값 역산 (오라클 문제) | 질의예산 하드캡 + 판정 거부 — 상세는 위 "동형암호의 한계와 오라클 방어" 절 참고 |
| CKKS 성능 | N=8192, scale=2^40으로 노이즈를 ~1e-9m 수준까지 낮춤 |
| LLM API 장애 | 결정론적 템플릿으로 자동 대체, 판정 결과 자체는 불변 |
| 법령 원문 미확보 | 공개 조문을 데모용으로 재구성, 군사시설 조문은 수치 없이 "비공개" 사실만 서술 |
- Phase 5 baseline:
docs/baseline_phase5.json— Mock HE + RAG + LLM 연결 시점 전체 파이프라인 출력 기록 - Phase 6: Mock HE → 실제 TenSEAL CKKS 교체 후, 동일 입력에서
exceeds_limit이 baseline과 정확히 일치함을 확인(tests/test_he_pipeline.py) - Phase 7 벤치마크 (
scripts/benchmark_he.py, steady-state 30회 평균):
| 측정 항목 | 결과 |
|---|---|
he_compute_node (steady-state) |
평균 ~6ms / 중앙값 ~4.5ms |
he_compute_node (콜드 스타트) |
~340ms (프로세스당 1회성) |
plain_compute_node |
평균 ~0.005ms |
| HE ÷ 평문 속도비 | 약 1,300~5,000배 |
authority_verify 배치 처리 |
in-process에서는 절감 효과 없음(네트워크 왕복이 없는 Mock 구조 특성). 실제 HSM API 연동 시 절감 예상, 이 PoC에서는 미검증 |