POSA_LEAKSMS/firmware/certs/README.md
유창욱 90f121e14c chore: import codebase with security hardening
SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트).
전체 코드리뷰에서 도출된 보안 하드닝 10건 반영:
- 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시)
- 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php)
- 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점)
- CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php)
- 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화

_backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
2026-06-20 09:37:40 +09:00

6.1 KiB

서버 루트 CA 인증서 (certs/)

STM32F407 펌웨어는 폐쇄망(air-gapped)에서 동작하므로 런타임에 CA 인증서를 다운로드할 수 없다. 따라서 서버(Cafe24) TLS 검증에 사용할 루트 CA PEM 을 flash 에 임베드한다(server_ca.c). mbedTLS(tls_mbedtls.c)가 부팅 시 mbedtls_x509_crt_parse() 로 이 PEM 을 파싱하고, 매 핸드셰이크마다 서버 인증서 체인을 검증한다.

파일 역할
server_ca.h SERVER_CA_PEM[] / SERVER_CA_PEM_LEN 선언
server_ca.c PEM 본문(현재 PLACEHOLDER, 반드시 교체)
gen_ca_header.ps1 PEM 파일 → server_ca.c 재생성 스크립트

⚠️ 현재 server_ca.c 는 자리표시자(PLACEHOLDER)다. 컴파일은 되지만 실제 서버 인증서 검증은 실패한다. 배포 전 반드시 실제 Cafe24 루트 CA 로 교체할 것.


1. 서버 인증서 체인 추출

배포 대상 호스트(app_config.hAPP_API_HOST, 기본 your-domain.example)에 대해 빌드 머신에서 다음을 실행한다. (-servername 으로 SNI 지정 필수 — 가상호스팅 환경에서 올바른 인증서를 받기 위함.)

HOST=your-domain.example      # = APP_API_HOST 와 동일하게

# 전체 체인(leaf + intermediate + root) 보기
openssl s_client -showcerts -connect "$HOST:443" -servername "$HOST" </dev/null

출력의 -----BEGIN CERTIFICATE----- ~ -----END CERTIFICATE----- 블록들이 체인이다. 보통 순서는 [0] leaf(서버) → [1] intermediate → [2] root 다.

펌웨어 검증의 신뢰 앵커(trust anchor)는 root CA 다. 단, Cafe24 가 root 를 체인에 포함하지 않거나 cross-signed 인 경우가 있어, 확실한 검증을 위해 intermediate + root 를 한 PEM 파일에 연결해 두는 것을 권장한다(아래 2번).


2. 트러스트 PEM 만들기 (intermediate + root 연결)

가장 안전한 방법은 발급기관(CA)의 공식 root/intermediate PEM 을 받아 연결하는 것이다. 체인에서 직접 뽑으려면 아래처럼 블록을 분리/연결한다.

# s_client 출력에서 인증서 블록만 추려 파일로 저장(예: certs.pem)
openssl s_client -showcerts -connect "$HOST:443" -servername "$HOST" </dev/null \
  | sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' > chain_all.pem

# chain_all.pem 에서 intermediate + root 를 골라 cafe24_root.pem 으로 연결.
# (leaf[0] 는 제외. intermediate/root 만 trust 파일에 둔다.)
#   - 가장 확실: CA 벤더 공식 사이트에서 root/intermediate PEM 다운로드 후 cat 으로 연결
cat intermediate.pem root.pem > cafe24_root.pem

팁: root 만으로 검증이 되면 root 한 장이면 충분하다. intermediate 까지 넣으면 서버가 intermediate 를 안 보내는 경우에도 검증이 안정적이다(여분은 무해).


3. 체인 / cipher 사전 검증 (위험 R3 사전조사)

펌웨어에 넣기 전에, 추출한 trust PEM 으로 검증이 실제로 통과하는지, 그리고 mbedTLS 클라이언트가 쓸 TLS1.2 ECDHE-RSA-AES-GCM 을 서버가 받아주는지 확인한다.

# (a) 우리 trust PEM 으로 체인 검증 통과 여부
openssl s_client -connect "$HOST:443" -servername "$HOST" \
  -CAfile cafe24_root.pem </dev/null 2>/dev/null \
  | grep -E "Verify return code|Verification"
# 기대: "Verify return code: 0 (ok)"

# (b) mbedTLS 가 협상할 cipher/프로토콜 강제 확인 (TLS1.2 + ECDHE-RSA-AES-GCM)
openssl s_client -connect "$HOST:443" -servername "$HOST" \
  -tls1_2 -cipher 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384' \
  -CAfile cafe24_root.pem </dev/null 2>/dev/null \
  | grep -E "Protocol|Cipher|Verify return code"
# 기대: Protocol = TLSv1.2, Cipher = ECDHE-RSA-AES(128|256)-GCM-...

# (c) 인증서 만료일 확인 (leaf + CA)
openssl s_client -connect "$HOST:443" -servername "$HOST" </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer
openssl x509 -in cafe24_root.pem -noout -dates -subject

펌웨어 mbedtls_config.h 가 ECDHE-RSA + AES-GCM(TLS1.2)만 켜므로, 위 (b) 가 실패하면(서버가 해당 cipher 미지원) TLS 연결 불가다 → 서버 측 cipher 설정을 맞추거나 mbedtls_config.h 의 허용 cipher/곡선을 조정해야 한다.


4. server_ca.c 재생성

검증을 통과한 cafe24_root.pem 으로 C 소스를 재생성한다(빌드 머신, PowerShell).

# firmware/certs 디렉터리에서
./gen_ca_header.ps1 -PemPath cafe24_root.pem
# 또는 출력 경로 지정:
./gen_ca_header.ps1 -PemPath C:\path\cafe24_root.pem -OutPath .\server_ca.c

스크립트는 PEM 각 라인을 "...\n" C 리터럴로 변환하고, SERVER_CA_PEM_LEN = sizeof(SERVER_CA_PEM) 로 정의한다(종결 NUL 포함 — mbedTLS PEM 파싱 요구사항). 생성 후 재빌드하면 새 CA 가 적용된다.

생성 결과를 빠르게 sanity-check:

# 생성된 .c 안의 PEM 이 다시 openssl 로 파싱되는지(따옴표/escape 깨짐 검출용)
grep -oE 'BEGIN CERTIFICATE|END CERTIFICATE' server_ca.c

5. 만료 → 펌웨어 업데이트 (위험 R3)

폐쇄망이라 인증서를 OTA 로 갱신할 수 없다. 임베드한 root/intermediate CA 가 만료되거나 서버가 발급기관/체인을 교체하면 TLS 핸드셰이크가 실패하고 보고가 중단된다.

대응:

  1. 만료일 모니터링: 위 3-(c) 로 CA notAfter 를 기록·달력 알림 설정.
  2. 사전 교체: 만료 1~3개월 전 새 CA 로 gen_ca_header.ps1 재생성 → 재빌드.
  3. 재배포 절차: 폐쇄망 반입 절차(USB/오프라인 번들)로 새 .bin/.hex 플래싱(sht30_fw 단일 타깃). firmware/VERSION 갱신.
  4. 여유 신뢰 앵커: 가능하면 차기 root 까지 함께 임베드(여러 CA 연결)하여 교체 윈도를 넓힌다(여분 CA 는 검증에 무해).

참고: docs/stm32f407_migration_plan.md 위험표 R3 — "Cafe24 인증서 cipher/체인/만료 → TLS 연결 불가 → 사전 openssl s_client 로 체인·cipher 확인, 루트 CA 임베드, 만료 시 펌웨어 업데이트 절차".