POSA_LEAKSMS/firmware/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

175 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SHT30 온습도 모니터링 STM32F407 펌웨어
> Raspberry Pi SHT30 온습도 노드를 **STM32F407VGT6** 베어메탈 펌웨어로 대체한 프로젝트.
> 기존 서버측 PHP API / 서명 규약 / 요청 포맷을 **바이트 단위로 그대로 재현**하여
> 서버 무중단으로 RPi → STM32 전환을 목표로 한다.
- 대상 MCU: STM32F407VGT6 (Cortex-M4F @168MHz, Flash 1MB, SRAM 192KB + CCM 64KB)
- 네트워크: **유선 Ethernet** (LAN8720 RMII PHY) → LwIP + mbedTLS (HTTPS)
- 런타임: **FreeRTOS** (네이티브 API — `task.h`/`queue.h`/`timers.h`/`semphr.h`)
- 빌드: `arm-none-eabi-gcc` 15.2 + CMake + Ninja
- 배포: **폐쇄망(air-gapped)** — 모든 서드파티 의존성은 `third_party/`에 벤더링, 빌드·런타임에 외부 다운로드 0건
상세 이식 근거는 [`../docs/stm32f407_migration_plan.md`](../docs/stm32f407_migration_plan.md) 참고.
---
## 1. 시스템 아키텍처
물리 센서에서 SMS 발송까지의 전체 경로. **굵게** 표시한 구간이 본 펌웨어가 대체하는 부분이다.
```
┌──────────────┐ I2C1 ┌────────────────────────────┐ Ethernet(RMII) ┌──────────────┐
│ SHT30 센서 │ ──────────▶ │ STM32F407VGT6 │ ───────────────▶ │ LAN8720 PHY │
│ (온습도) │ PB6/PB7 │ (FreeRTOS 펌웨어) │ │ (외부 PHY) │
│ 0x44 │ (SCL/SDA) │ │ └──────┬───────┘
│ │ │ Task_Net : 링크/DHCP/SNTP │ │ UTP
│ │ │ Task_Sensor: 5분 주기 측정 │ ▼
│ │ │ Task_Report: TLS POST │ ┌──────────────┐
└──────────────┘ │ Task_Watchdog: IWDG refresh │ │ 스위치/공유기 │
└────────────────────────────┘ └──────┬───────┘
lowercase_hex(sha256(API_KEY || raw_body)) X-Signature 헤더 │ HTTPS/TLS1.2
┌────────────────────────────────────────────────────────┐
│ Cafe24 호스팅 (변경 없음) │
│ sensor_data.php ──▶ MySQL (로그 저장) │
│ │ │
│ └──▶ 서버 임계 판정(config.php METRIC_*) ──▶ SMS │
└────────────────────────────────────────────────────────┘
```
- 펌웨어는 매 보고마다 `connect → TLS 핸드셰이크 → POST → close` 를 수행한다(메모리 절약, 보고 주기가 길어 성능 무관).
- 서명은 본문 바이트 그대로 `sha256(API_KEY || body)` 를 계산해 **`X-Signature`** 헤더로 보낸다. 본문의 키 정렬/float 포맷 재현이 불필요하다(서버 `verify_signature_raw`).
- **임계(고온/저온/고습/저습) 판정과 SMS 발송은 서버(PHP)에서 수행**한다. 펌웨어는 온습도 원값과 plausibility 상태(`metric_status`)만 보고하고, 경보 임계는 서버 `config.php``METRIC_*` 상수가 결정한다(폐쇄망 재플래시 비용을 피하기 위함).
---
## 2. 보드 구성 (SHT30 단일 보드)
이 프로젝트는 **SHT30 온습도 보드 단일 구성**이다. CMake `sht30_fw` 타깃이 `-DBOARD_SHT30` 을 자동으로 주입한다.
| 빌드 매크로 | CMake 타깃 | 산출물 | sensor_id | device_id | 센서 | 동작 |
|----------------|-----------|-----------------|-----------|------------------|---------------|--------------------------------------------|
| `-DBOARD_SHT30`| `sht30_fw`| `sht30_fw.bin` | 2 | `stm32-sht30-01` | SHT30 온습도 | 주기(300s) I2C 측정 + 원값 보고 |
식별값 정의는 [`common/board_config.h`](common/board_config.h)(`BOARD_SHT30` 단일). 공통 튜너블은 [`common/app_config.h`](common/app_config.h).
---
## 3. 기능 패리티 (RPi 대비)
| 기능 | RPi (Python) | STM32 펌웨어 | 상태 |
|----------------------|----------------------------------|-------------------------------------------|-----------|
| SHT30 측정/CRC | `smbus2` + `crc8_sht3x` | HAL I2C + `sht30_crc8`/`sht30_parse` | 유지 |
| 주기 측정/보고 | 300s (최소 30s) | `APP_SHT30_REPORT_INTERVAL_SEC` (300s) | 유지 |
| metric_status 판정 | `classify_reading()` plausibility | reporter 호출부 `metric_status` | 유지 |
| 임계 경보/SMS | (서버측 처리) | **서버 `config.php` METRIC_* 에서 판정** | 유지(서버) |
| HTTP 재시도 | requests + 루프(3회/5s) | `report_*` 내부 재시도(3회/5s) | 유지 |
| TLS/HTTPS | OpenSSL(`requests`) | mbedTLS + LwIP | 변경(이식) |
| 네트워크 | WiFi | 유선 Ethernet(LAN8720 RMII) | 변경(이식) |
| 동시성 | Python threads | FreeRTOS tasks | 변경(이식) |
| 시간원 | OS NTP | SNTP(LwIP) → RTC | 변경(이식) |
| 서명 | 본문 필드 정규화 서명 | raw-body `X-Signature` | 변경(단순화)|
| 프로세스 관리 | systemd 재시작 | IWDG 워치독 + 재연결 | 변경(이식) |
| 로깅 | 로테이팅 파일 로그 | UART 콘솔 | 변경(축소) |
> **임계 판정의 위치:** 펌웨어는 `temperature_c`/`humidity_percent` 원값과 `metric_status`(센서 물리/타당성 범위 기준 `normal`/`out_of_range`)만 보고한다. 고온/저온/고습/저습 경보와 SMS 트리거는 **서버 `config.php` 의 `METRIC_*` 임계**가 담당한다. 폐쇄망에서 임계값을 조정할 때 펌웨어 재플래시가 불필요하도록 의도된 분리다.
자세한 함수 단위 매핑은 [`docs/PORTING_NOTES.md`](docs/PORTING_NOTES.md) 참고.
---
## 4. 빠른 시작 (빌드/플래시)
> 폐쇄망 전체 절차는 [`docs/BUILD_OFFLINE.md`](docs/BUILD_OFFLINE.md). 핀맵/배선은 [`docs/HARDWARE.md`](docs/HARDWARE.md).
```bash
# 0) 비밀값 준비 (한 번)
cp firmware/common/secrets.h.example firmware/common/secrets.h
# secrets.h 의 APP_API_KEY 를 서버 php/config.php 의 API_KEY 와 동일하게 채운다.
# 1) 서버 루트 CA 임베드 (한 번)
# 실제 Cafe24 인증서 체인의 루트 CA PEM 으로 certs/server_ca.c 를 생성한다.
# (certs/server_ca.h 가 SERVER_CA_PEM / SERVER_CA_PEM_LEN 을 extern 으로 선언함)
# 2) 빌드 (sht30_fw 단일 타깃)
cmake -S firmware -B firmware/build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=firmware/cmake/arm-none-eabi.cmake
cmake --build firmware/build --target sht30_fw # → sht30_fw.bin/.hex/.elf
# 3) 플래시 (ST-Link) — 보드 파라미터 없이 sht30_fw 고정
pwsh firmware/scripts/flash.ps1
# 대안: st-flash write firmware/build/sht30_fw.bin 0x08000000
# 대안: openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
# -c "program sht30_fw.elf verify reset exit"
# 4) 콘솔 확인 (USART3 PD8/PD9, 115200 8N1)
# 부팅 로그 → 링크업/DHCP/SNTP → 첫 startup POST 200 확인
```
> 참고: `cmake/`, `third_party/`, `board_sht30/`, `certs/server_ca.c` 는 별도 작업으로 생성/벤더링된다(아래 디렉터리 맵 참고). 위 명령은 그 산출물이 갖춰진 뒤의 표준 절차다.
---
## 5. `firmware/` 디렉터리 맵
```
firmware/
├─ README.md ← (이 문서)
├─ CMakeLists.txt 빌드 루트: sht30_fw 타깃, third_party 통합
├─ cmake/
│ └─ arm-none-eabi.cmake 크로스 툴체인 파일 (gcc 15.2)
├─ scripts/
│ ├─ vendor.* 빌드 머신에서 고정 버전 의존성 벤더링(폐쇄망 준비)
│ ├─ build.ps1 cmake configure + sht30_fw 빌드
│ └─ flash.ps1 sht30_fw 플래싱(보드 파라미터 없이 고정)
├─ common/ 공유 코드/계약 헤더
│ ├─ app_config.h 튜너블: API 호스트/포트/경로, 주기, 재시도, SNTP, 망주소, 워치독, 핀, SHT30 범위
│ ├─ board_config.h BOARD_DEVICE_ID/LOCATION/SENSOR_ID/SENSOR_NAME (BOARD_SHT30 단일)
│ ├─ secrets.h.example APP_API_KEY 템플릿 (실 secrets.h 는 운영자가 생성, 미커밋)
│ ├─ bsp.{h,c} 클럭/HAL/LED 초기화 (HSE 8MHz → 168MHz)
│ ├─ net.{h,c} ETH MAC + LAN8720 RMII + LwIP, DHCP/static
│ ├─ timesync.{h,c} SNTP → RTC
│ ├─ tls.{h,c} mbedTLS over LwIP socket (transport_t 제공)
│ ├─ transport.h 바이트 스트림 전송 추상화(connect/send/recv/close)
│ ├─ httpapi.{h,c} HTTP/1.1 POST 작성 + X-Signature + 상태 파싱 (검증된 코어)
│ ├─ sig.{h,c} raw-body 서명 sig_raw_body() (검증된 코어)
│ ├─ jsonbody.{h,c} 본문 JSON 생성 jb_sht30_event (검증된 코어)
│ ├─ sha256_backend.h SHA-256 추상 인터페이스
│ ├─ sha256_sw.c 자급식 SHA-256 (공개 도메인) (검증된 코어)
│ ├─ hexutil.{h,c} 바이트 ↔ 소문자 hex (검증된 코어)
│ ├─ sht30.h SHT30 I2C 드라이버 계약
│ ├─ sht30_convert.{h,c} SHT30 CRC8/변환 (이식성 순수 로직) (검증된 코어)
│ ├─ reporter.h 이벤트 보고 헬퍼(본문→서명→POST→재시도) 계약
│ ├─ watchdog.h IWDG 계약
│ └─ applog.h UART 로그 계약
├─ board_sht30/ SHT30 보드(sensor_id=2) 진입점 + 드라이버
│ ├─ main.c FreeRTOS 태스크 구성 + 부팅
│ ├─ app_sht30.c 주기 측정 → metric_status → 보고
│ └─ sht30.c HAL I2C 측정
├─ certs/
│ ├─ server_ca.h SERVER_CA_PEM / SERVER_CA_PEM_LEN extern 선언
│ └─ server_ca.c 실제 Cafe24 루트 CA PEM (운영자가 교체) ← TODO(hw)
├─ third_party/ 벤더링(폐쇄망): CMSIS, STM32F4_HAL, FreeRTOS-Kernel, lwip, mbedtls
├─ test/host/ 호스트 빌드 패리티 테스트 (PHP 기대값 대조)
│ ├─ parity_test.py
│ ├─ php_verify.php
│ └─ reference.py
├─ build/ CMake/Ninja 산출물 (armgate: 호스트 컴파일 게이트 .o)
└─ docs/
├─ HARDWARE.md SHT30 보드 핀맵/배선/핀 충돌 해소
├─ BUILD_OFFLINE.md 폐쇄망 빌드·전달·플래시·롤백 절차
└─ PORTING_NOTES.md Python → C 매핑 / 동작 패리티 / 변경점 / TODO(hw)
```
> 위 맵 중 `CMakeLists.txt`, `cmake/`, `scripts/`, `board_sht30/`, `third_party/`, `certs/server_ca.c` 는 별도 작업으로 채워진다. 본 문서·`common/`·`certs/server_ca.h`·`test/`·`build/` 산출물은 이미 존재한다.
---
## 6. 핵심 제약 (반드시 인지)
- **하드웨어 해시 가속기 없음**: STM32F407 에는 CRYP/HASH 블록이 없다(F415/F417 전용). SHA-256/AES 는 **소프트웨어**로 수행한다. RSA-2048 핸드셰이크는 ~12s 소요(보고 주기가 길어 허용).
- **하드웨어 RNG 있음**: TLS 엔트로피 소스로 사용한다.
- **SRAM 예산이 최대 리스크**: mbedTLS record buffer 를 `MBEDTLS_SSL_IN/OUT_CONTENT_LEN ≈ 4096` 으로 축소하고, FreeRTOS 태스크 스택을 CCM(64KB)에 배치한다(linker script).
- **폐쇄망**: 빌드·설치·런타임 모두 인터넷 접근 불가를 가정한다. 모든 의존성·인증서·SNTP 대상 IP 까지 사전에 준비한다(자세히 [`docs/BUILD_OFFLINE.md`](docs/BUILD_OFFLINE.md)).