POSA_LEAKSMS/firmware/docs/PORTING_NOTES.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

162 lines
13 KiB
Markdown
Raw Permalink 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.

# PORTING_NOTES.md — Raspberry Pi(Python) → STM32F407(C/FreeRTOS) 이식 노트
원본 동작 기준: RPi `sht30_monitor.py`(SHT30 온습도, sensor_id=2) — 현재 저장소에는 없음; 동작만 이식.
근거 계획: [`docs/stm32f407_migration_plan.md`](../../docs/stm32f407_migration_plan.md).
**핵심 원칙: 서버측 PHP API 는 변경하지 않는다.** 펌웨어가 기존 요청 포맷/서명 규약을 그대로 재현하여
서버 무중단으로 RPi → STM32 전환을 달성한다(서버는 `X-Signature` 와 레거시 본문 서명을 이중 수용).
---
## 1. Python 함수 → C 펌웨어 모듈 매핑
### 1.1 SHT30 보드 (`sht30_monitor.py`)
| Python (sht30_monitor.py) | C 펌웨어 | 비고 |
|---------------------------|----------|------|
| `read_sht30()` (smbus2) | [`sht30.h`](../common/sht30.h) `sht30_measure(*t, *rh)` | HAL I2C 로 `0x2C06` → 20ms → 6B read |
| `crc8_sht3x(values)` | [`sht30_convert.h`](../common/sht30_convert.h) `sht30_crc8()` | 동일 알고리즘(poly 0x31, init 0xFF) |
| (raw → °C/% 변환, round 2) | `sht30_parse(frame, *t, *rh)` | `T=-45+175*raw/65535`, `RH=100*raw/65535`. 소수 2자리는 `jb_sht30_event` 직렬화에서 |
| `classify_reading()` | (reporter 호출부) `metric_status` 계산 | plausibility 범위는 `APP_SHT30_TEMP/RH_LOW/HIGH`. **경보 임계는 서버**(§4) |
| `make_signature(data)` | [`sig.h`](../common/sig.h) `sig_raw_body(key, body, len, hex)` | raw-body 서명 — **float 서명 패리티 문제 원천 제거**(§3) |
| `post_reading(reading, event)` | [`reporter.h`](../common/reporter.h) `report_sht30_event(event, t, rh, status)` | 내부 본문 생성→서명→TLS POST→재시도 |
| (본문 dict 생성) | [`jsonbody.h`](../common/jsonbody.h) `jb_sht30_event(...)` | device_id/location/sensor_id/name/event_type/timestamp + metric_type="sht30", temperature_c, humidity_percent, metric_status, app_version |
| (requests.post + 재시도 루프) | [`httpapi.h`](../common/httpapi.h) `api_post_once()` + reporter 재시도 | TLS 전송은 [`transport.h`](../common/transport.h)/[`tls.h`](../common/tls.h) |
| `run_loop()` (while + sleep) | `Task_Report` (`board_sht30/main.c`, `app_sht30.c`) | 주기 `APP_SHT30_REPORT_INTERVAL_SEC`(300s), 첫 성공=startup |
| `main()` 시작 시 startup POST | `board_sht30/main.c` 부팅 시퀀스 | 부팅 후 1회 `report_sht30_event("startup", ...)` |
| `read_once()` / `--read-once`/`--once` CLI | (해당 없음) | 베어메탈에는 CLI 없음. UART 명령은 옵션(미구현) |
| `setup_logger()` | [`applog.h`](../common/applog.h) `applog_init()` / `LOGI/LOGW/LOGE` | 파일 로테이션 → UART 콘솔만 |
| env 설정(`env_*`) / `config.py` import | [`app_config.h`](../common/app_config.h) + [`board_config.h`](../common/board_config.h) + `secrets.h` | 컴파일 타임 고정(런타임 env 없음) |
| `SimGPIO` / SIMULATION | (해당 없음) | 실제 하드웨어 타깃. 순수 로직은 `test/host/` 게이트로 검증 |
### 1.2 인프라/공통 매핑
| RPi/OS 기능 | STM32 | 모듈 |
|-------------|-------|------|
| WiFi | 유선 Ethernet(LAN8720 RMII) + LwIP | [`net.h`](../common/net.h) |
| OS NTP | SNTP(LwIP) → RTC | [`timesync.h`](../common/timesync.h) |
| OpenSSL(`requests`) | mbedTLS(TLS1.2, ECDHE-RSA+AES-GCM, 소프트웨어 SHA/AES) | [`tls.h`](../common/tls.h) |
| Python threads | FreeRTOS tasks/queue/timer (네이티브 API) | `board_sht30/main.c` |
| systemd 재시작 | IWDG 워치독 + 재연결 백오프 | [`watchdog.h`](../common/watchdog.h) |
| 로테이팅 파일 로그 | UART 콘솔 로그 | [`applog.h`](../common/applog.h) |
| 클럭/부팅 | HAL_Init + SystemClock(168MHz) + LED | [`bsp.h`](../common/bsp.h) |
---
## 2. 동작 패리티 (값/타이밍/이벤트)
서버가 보는 동작은 RPi 와 동일하게 유지한다. 주요 상수는 [`app_config.h`](../common/app_config.h):
| 항목 | RPi 값 | STM32 매크로 | 일치 |
|------|--------|--------------|------|
| 앱 버전(보고) | `v2605-sht30` | `APP_VERSION = "v2606-sht30"` | 의도적 갱신(STM32 식별) |
| SHT30 주기 | 300s (최소 30s) | `APP_SHT30_REPORT_INTERVAL_SEC = 300` | ✔ |
| HTTP 타임아웃 | 10s | `APP_HTTP_TIMEOUT_MS = 10000` | ✔ |
| HTTP 재시도 | 3회 | `APP_HTTP_RETRY_COUNT = 3` | ✔ |
| 재시도 간격 | 5s | `APP_HTTP_RETRY_DELAY_MS = 5000` | ✔ |
| SHT30 주소 | 0x44 | `APP_SHT30_I2C_ADDR = 0x44` | ✔ |
| SHT30 온도 plausibility | -40~125°C | `APP_SHT30_TEMP_LOW/HIGH_C` | ✔ |
| SHT30 습도 plausibility | 0~100% | `APP_SHT30_RH_LOW/HIGH` | ✔ |
### 이벤트 종류 (서버 `event_type`, 변경 없음)
| 이벤트 | 트리거 | RPi | STM32 |
|--------|--------|-----|-------|
| `startup` | 부팅 직후 1회 | `main()` | `board_sht30/main.c` 부팅 시퀀스 |
| `periodic` | 주기 만료(300s) | `run_loop` | `Task_Report` |
SHT30 시나리오: `startup` → (300s 마다) `periodic` 측정 보고. 서버가 매 보고의 `temperature_c`/`humidity_percent` 를
`config.php``METRIC_*` 임계와 비교해 고온/저온/고습/저습 시 SMS 를 발송한다(§4).
### 본문 필드 패리티 (서버가 받는 JSON)
- SHT30: `device_id, device_location, sensor_id, sensor_name, event_type, timestamp` + `metric_type="sht30", temperature_c, humidity_percent, metric_status, app_version` ([`jb_sht30_event`](../common/jsonbody.h)).
- 식별 헤더: RPi 는 `X-Device-Id` / `User-Agent` 를 보냈다. 펌웨어는 [`httpapi.h`](../common/httpapi.h) `http_build_post()``X-Device-Id``X-Signature` 를 넣는다(서버 동작에 영향 없음).
---
## 3. 무엇이 바뀌었나 (의도적 변경)
| 변경 | RPi | STM32 | 이유 |
|------|-----|-------|------|
| **네트워크** | WiFi | 유선 Ethernet(LAN8720 RMII)+LwIP | 베어메탈, 유선 신뢰성 |
| **HTTP 클라이언트** | `requests`(OpenSSL) | mbedTLS + `httpapi.c` | 온칩 TLS 스택 |
| **TLS 해시/암호** | OS OpenSSL | **소프트웨어**(SHA-256/AES) | F407 에 HW 해시 가속기 없음. RSA-2048 핸드셰이크 ~12s 허용 |
| **동시성** | Python threads + Lock | FreeRTOS tasks/queue/timer (네이티브 API) | RTOS. 태스크 스택은 CCM(64KB) 배치 |
| **시간원** | OS NTP | SNTP→RTC (TLS 전에 수행) | UDP SNTP 는 TLS 불필요 → 닭-달걀 회피 |
| **서명 방식** | `sha256(API_KEY + json.dumps(sort_keys, ensure_ascii=False, separators=(',',':')))` (본문에 `signature` 필드) | `sha256(API_KEY ‖ raw_body)`**`X-Signature` 헤더** | **(중요)** 키 정렬/float 포맷/슬래시 이스케이프 재현 불필요. SHT30 float 서명 패리티 문제 원천 제거(R1). 서버는 `verify_signature_raw` + 레거시 폴백 이중 모드 |
| **본문 필드(wire)** | `is_leak` 포함(누수 시스템 잔재) | **`is_leak` 필드 제거** | 단일 SHT30 보드에 무의미. `jb_sht30_event` 가 더 이상 직렬화하지 않으며, 서버 `sensor_data.php``is_leak` 처리 제거 |
| **임계 판정/SMS** | (서버측) | **서버 `config.php` METRIC_* 에서 판정** | 폐쇄망 재플래시 비용 회피. 펌웨어는 원값 + `metric_status` 만 보고(§4) |
| **설정** | env / `config.py` | 컴파일 타임 `app_config.h`/`board_config.h` + `secrets.h` | 런타임 env 없음(옵션: flash 설정/UART 명령 미구현) |
| **프로세스 관리** | systemd | IWDG(20s) + 재연결 백오프 | 베어메탈 |
| **로깅** | 로테이팅 파일 로그 | UART 콘솔만 | 파일시스템 없음 |
| **TLS 연결 수명** | requests 가 관리 | 매 보고 connect→handshake→POST→close | RAM 절약(R2). 보고 주기 길어 성능 무관 |
### 서명 변경의 의미 (가장 중요한 단순화)
RPi 의 `make_signature()` 는 본문을 **정규화(sort_keys, no-space, non-ascii 유지)** 한 뒤 서명하고, 그 값을
본문 `signature` 필드에 넣었다. 펌웨어는 **자신이 실제 전송하는 본문 바이트 그대로** 서명만 하면 된다:
```c
/* 펌웨어 절차 (httpapi.c::api_post_once 가 내부 수행) */
sig_raw_body(APP_API_KEY, body, strlen(body), sig_hex); /* sha256(key || body) */
/* HTTP 헤더에 "X-Signature: <sig_hex>", 본문은 body 바이트 그대로 전송 */
```
**유일한 불변식:** 서명한 `body` 바이트 == 실제 전송 `body` 바이트 (100% 동일).
문자열 필드에 `"`/`\` 가 들어가면 JSON 이스케이프만 적용한다(서버는 그 바이트로 검증하므로 PHP `json_encode`
포맷 재현은 불필요). `jb_sht30_event` 직렬화가 이 불변식을 보장한다.
> RPi 무중단 공존: `X-Signature` 헤더가 없으면 서버가 레거시 본문 필드 서명으로 폴백하므로, 같은 사이트에
> RPi 와 STM32 가 공존 가능(롤백/점진 전환 안전).
---
## 4. 임계 판정 / SMS 흐름 (펌웨어 측정 → 서버 경보)
SHT30 측정에서 SMS 발송까지의 책임 분담:
1. **펌웨어(측정·보고)**: 300s 마다 SHT30 을 읽어 `temperature_c`/`humidity_percent` **원값**(소수 2자리)을 계산하고,
센서 물리/타당성 범위(`APP_SHT30_TEMP/RH_LOW/HIGH`) 기준으로 `metric_status`(`normal`/`out_of_range`)만 부여해
`sensor_data.php` 로 POST 한다. 펌웨어는 **고온/저온/고습/저습 같은 운영 임계를 판정하지 않는다.**
2. **서버(임계·SMS)**: `sensor_data.php` 가 본문을 DB 에 적재하고, `config.php``METRIC_*` 임계와 비교해
고온/저온/고습/저습 조건이면 **SMS 게이트웨이로 문자**를 발송한다.
> **이 분리의 이유:** 폐쇄망에서는 펌웨어 재플래시 비용이 크다. 임계값을 서버 `config.php` 에 두면 현장에서
> 보드를 다시 굽지 않고도 경보 기준을 조정할 수 있다. 따라서 펌웨어 `APP_SHT30_*` 범위는 **물리 타당성**
> 한계로만 쓰고, **운영 경보 임계는 서버가 소유**한다.
---
## 5. 열린 하드웨어/운영 결정 항목 — `TODO(hw)`
> 코드는 합리적 기본값으로 컴파일되지만, 아래는 사람이 사이트/보드에 맞춰 확정해야 한다.
> 핀 상세는 [`HARDWARE.md`](HARDWARE.md), 빌드/인증서/비밀값은 [`BUILD_OFFLINE.md`](BUILD_OFFLINE.md).
| # | TODO(hw) | 기본값/현황 | 결정 필요 |
|---|----------|-------------|-----------|
| 1 | **PA2 핀 충돌 (USART2_TX ↔ ETH_MDIO)** | ✅ 코드 해소: 로그 UART = USART3(PD8/PD9, AF7) (`applog.c`/`applog.h`/`app_config.h` 반영) | (HW만 남음) ST-Link VCP 불가 → **외부 3.3V USB-UART 어댑터를 PD8/PD9 에 연결** (HARDWARE.md §0) |
| 2 | 서버 호스트명/경로 | `APP_API_HOST="your-domain.example"`, `APP_API_PATH="/raspi_leck_detecter/api/sensor_data.php"` | 실제 Cafe24 도메인/경로로 정정 |
| 3 | 서버 루트 CA PEM | `certs/server_ca.c` 자리표시자(헤더만 실제값) | `openssl s_client` 로 체인 확인 후 실제 루트 CA 임베드(R3). `LEN=sizeof`(NUL 포함) |
| 4 | `APP_API_KEY` | `secrets.h.example` = `"change-this-api-key"` | 서버 `php/config.php``API_KEY` 와 동일하게 `secrets.h` 작성(미커밋) |
| 5 | 네트워크 주소 | DHCP 우선, static fallback `192.168.0.50/24` | 사이트 망에 맞춰 static fallback/DNS 조정 |
| 6 | SNTP 대상 | `APP_SNTP_SERVER="pool.ntp.org"` | **폐쇄망에서 외부 NTP 도달 불가** → 내부 NTP 서버 IP/호스트로 변경 필수(R4) |
| 7 | RMII 50MHz REF_CLK 소싱 | RMII 모드 코드 기본 | PHY 50MHz 공급 vs 외부 OSC 중 보드 설계 확정(HARDWARE.md §1.1) |
| 8 | LAN8720 nRST 핀 | 미배정(RC 리셋 가정) | 소프트 리셋 원하면 GPIO 1개 배정(예 PE0) |
| 9 | SHT30 ADDR | 0x44 (ADDR→GND) | 모듈 결선 확인(0x45 면 코드 매크로 변경) |
| 10 | 서버 METRIC_* 임계 | 서버 `config.php` 소유 | 고온/저온/고습/저습 경보 기준을 사이트 운영에 맞춰 설정 |
| 11 | 상태 LED 핀 | PD12(디스커버리 가정) | 커스텀 PCB 면 임의 GPIO |
| 12 | RTC/LSE + VBAT 백업 | LSE 권장, 미실장 시 LSI + 매부팅 SNTP | 정전 시간 보존 필요 여부로 결정(R4) |
| 13 | 전원 급전 | 3.3V 레일 가정 | PoE/12V→3.3V 등 설치 환경 확정 |
| 14 | 벤더링 고정 버전 | `third_party/` 미생성 | CubeF4/FreeRTOS/lwip/mbedtls 버전 핀 + 해시 기록(BUILD_OFFLINE.md §1) |
| 15 | CMake/툴체인/linker | `cmake/`/`*.ld` 확정 | CCM 스택 배치 + 크기 최적화 + `sht30_fw` 타깃 정의 |
---
## 6. 검증 게이트 (이식 정확성 확인)
- **호스트 패리티**: `firmware/test/host/parity_test.py` + `php_verify.php``sha256(API_KEY‖body)` 가 서버
`verify_signature_raw` 기대값과 일치하는지 대조(순수 로직: 서명/JSON/CRC/변환은 호스트·ARM 동일 다이제스트).
- **E2E**: 스테이징 PHP 로 `startup/periodic`(SHT30) 200 수신, `temperature_c`/`humidity_percent`/`metric_status` 적재 +
서버 임계 초과 시 SMS 트리거 확인.
- **안정성**: 24h + 망 단절 회복 + IWDG 동작(상세 BUILD_OFFLINE.md §6, 이식 계획 Phase 8).