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

136 lines
6.1 KiB
Markdown

# 서버 루트 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.h` 의 `APP_API_HOST`, 기본 `your-domain.example`)에
대해 **빌드 머신에서** 다음을 실행한다. (`-servername` 으로 SNI 지정 필수 —
가상호스팅 환경에서 올바른 인증서를 받기 위함.)
```sh
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 을 받아 연결하는 것이다.
체인에서 직접 뽑으려면 아래처럼 블록을 분리/연결한다.
```sh
# 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** 을 서버가
받아주는지 확인한다.
```sh
# (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).
```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:
```sh
# 생성된 .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 임베드, 만료 시 펌웨어 업데이트 절차".