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 제외.
257 lines
15 KiB
Markdown
257 lines
15 KiB
Markdown
# 메인 MCU 마이그레이션 계획: Raspberry Pi Zero 2 W → STM32F407VGT6 (Cortex-M4)
|
||
|
||
> 대상: SHT30 온습도 모니터링 시스템 펌웨어 이식 (SHT30 단일 보드)
|
||
> 작성일: 2026-06-09 · 구현 완료: 2026-06-09 · 단일 보드 전환: 2026-06
|
||
|
||
## 0. 구현 현황 (✅ 완료)
|
||
|
||
펌웨어가 `firmware/` 에 구현되었고 레거시 RPi 펌웨어(`raspberry_pi/`, `systemd/`)는 제거되었습니다.
|
||
시스템은 **SHT30 온습도 단일 보드**로 전환되었습니다(누수 보드/누수 펌웨어는 제거됨).
|
||
|
||
**산출물**: 검증된 이식 코어(`common/{sha256_sw,hexutil,sig,jsonbody,sht30_convert,httpapi}.c`),
|
||
타깃 모듈(`net/ethernetif/tls_mbedtls/timesync/sht30/reporter/applog/watchdog/bsp/rtos_hooks`),
|
||
보드 앱(`board_sht30/`), 설정(`config/{FreeRTOSConfig,lwipopts,mbedtls_config,stm32f4xx_hal_conf}.h`),
|
||
빌드(`CMakeLists.txt`, `cmake/`, `ld/`, `scripts/`), 인증서(`certs/`), 문서(`README.md`, `docs/`).
|
||
|
||
**검증 (이 환경에서 실행 가능한 범위)**:
|
||
- 이식 코어 ARM 크로스컴파일 게이트: `arm-none-eabi-gcc -mcpu=cortex-m4 ... -Werror -Wconversion` 무경고 통과.
|
||
- wire 계약 패리티: Python 레퍼런스(=C와 동일 알고리즘) → 서버 PHP `verify_signature_raw` 수용 통과(`firmware/test/host/parity_test.py`).
|
||
- PHP 서명/보안 회귀: `tests/raw_body_signature_test.php`, `tests/security_hardening_test.php` 통과.
|
||
- ⚠️ **전체 펌웨어 링크/실행은 미검증**: HAL/FreeRTOS/lwip/mbedtls 는 폐쇄망 빌드 머신에서 벤더링되므로(`firmware/third_party`), 실제 보드 빌드·플래시·HIL 시험은 운영 측에서 수행해야 함.
|
||
|
||
**다중 에이전트 코드리뷰 후 수정된 결함**:
|
||
- (CRITICAL) TLS 인증서 유효기간(notBefore/notAfter) 미검증 → `MBEDTLS_HAVE_TIME_DATE` + RTC/SNTP 시간 공급(`tls_platform_time`), 시간 미동기 시 connect 차단.
|
||
- (CRITICAL) FreeRTOS 정적메모리 콜백 중복정의(커널 제공과 충돌) → `rtos_hooks.c` 에서 제거(커널 소유).
|
||
- (HIGH) LwIP 코어락 위반: PHY 링크 통지 raw API → `netifapi_*` + 콜백 내부 raw `netif_set_up`(중첩락 데드락 회피).
|
||
- (CRITICAL/통합) 로그 UART PA2(=RMII MDIO) 충돌 → USART3(PD8/PD9), FreeRTOS 훅 단일 소유, `-u _printf_float`.
|
||
- (MEDIUM/LOW) `close_notify` 무한루프 상한, `dns_setserver` 코어락, HW RNG `bsp_rand32`, SHT30 태스크 스택 12KB, CA 자리표시자 동작 주석.
|
||
|
||
**배포 전 하드웨어 확정 필요 — TODO(hw)** (상세: `firmware/docs/HARDWARE.md`, `PORTING_NOTES.md`):
|
||
LAN8720 PHY 주소·RMII 50MHz REF_CLK 소스, HSE 주파수(PLLM), RTC 클럭원(LSE 32.768k vs LSI),
|
||
상태 LED 핀, MAC 주소(노드별 고유), SHT30 I2C 핀(PB6/PB7),
|
||
실제 Cafe24 루트 CA(`certs/server_ca.c`), `secrets.h`(APP_API_KEY).
|
||
|
||
---
|
||
|
||
## 1. 결정 사항 (확정)
|
||
|
||
| 항목 | 현재 (RPi) | 신규 (STM32F407VGT6) |
|
||
|---|---|---|
|
||
| 메인 MCU | RPi Zero 2 W (Linux/Python) | STM32F407VGT6 (Cortex-M4F, bare HW) |
|
||
| 네트워크 | WiFi | **유선 Ethernet (LAN8720 RMII PHY)** |
|
||
| TLS/HTTPS | OpenSSL (`requests`) | **온칩 mbedTLS + LwIP** |
|
||
| 보드 구성 | RPi SHT30 노드 | **STM32 SHT30 단일 보드** (SHT30=sensor_id 2) |
|
||
| 임계 판정/SMS | 서버측 처리 | **서버 `config.php` METRIC_* 에서 판정** (펌웨어는 원값만 보고) |
|
||
| 동시성 | Python threads | **FreeRTOS tasks** |
|
||
| 시간원 | NTP (OS) | **SNTP (LwIP) → RTC** |
|
||
| 설정 | env / systemd | **컴파일 타임 `config.h` + (옵션) flash 설정** |
|
||
| 로깅 | rotating file log | **UART 콘솔 + (옵션) 서버 로그** |
|
||
| 프로세스 관리 | systemd 재시작 | **IWDG 워치독 + 재연결 로직** |
|
||
|
||
핵심 원칙: **서버측 PHP API는 변경하지 않는다(가능한 한).** 펌웨어가 기존
|
||
서명 규약·요청 포맷을 바이트 단위로 그대로 재현하여 무중단으로 전환한다.
|
||
|
||
---
|
||
|
||
## 2. 하드웨어 사양 / 제약 (STM32F407VGT6)
|
||
|
||
- Cortex-M4F @ 168 MHz, **Flash 1 MB**, **RAM 192 KB** (112+16 KB SRAM + 64 KB CCM)
|
||
- Ethernet MAC 내장 → 외부 PHY 필요 (**LAN8720, RMII**)
|
||
- 하드웨어 **RNG 있음** (TLS 엔트로피에 사용) — 중요
|
||
- 하드웨어 해시/암호 가속기 **없음** (CRYP/HASH는 F415/417 전용) → **SHA-256/AES는 소프트웨어(mbedTLS sw)**
|
||
- I²C 3채널, USART 다수, EXTI, RTC, IWDG
|
||
|
||
### 메모리 예산 (가장 큰 리스크)
|
||
- mbedTLS TLS1.2 클라이언트(ECDHE-RSA + AES-GCM) 기본 record buffer 2×16 KB = 32 KB.
|
||
→ **`MBEDTLS_SSL_IN/OUT_CONTENT_LEN`을 4~6 KB로 축소** (서버 응답이 작으므로 가능)
|
||
→ LwIP pbuf pool과 합산 시 SRAM 빠듯 → **FreeRTOS task stack을 CCM(64 KB)에 배치**
|
||
- RSA-2048 핸드셰이크는 소프트웨어로 ~1–2 s 소요(허용 가능, 보고 주기가 길다)
|
||
|
||
---
|
||
|
||
## 3. 현재 코드 → 펌웨어 모듈 매핑
|
||
|
||
| 현재 (Python) | 신규 (C 펌웨어 모듈) | 비고 |
|
||
|---|---|---|
|
||
| `sht30_monitor.py` | `app_sht30.c` (Task) | SHT30 보드 메인 |
|
||
| `make_signature()` | `sig.c : sig_raw_body()` | mbedtls_sha256 (raw-body) |
|
||
| 표준 JSON 직렬화 | `jsonbody.c : jb_sht30_event()` | raw-body 서명 → 정규화 불필요 |
|
||
| `post_to_server()` | `api.c : api_post_json()` | TLS + 재시도 루프 |
|
||
| `post_reading()` | `reporter.c : report_sht30_event()` | 본문→서명→POST→재시도 |
|
||
| `run_loop()` / `periodic_reporter()` | `Task_PeriodicReport` | vTaskDelay 기반 |
|
||
| `read_sht30()` | `sht30.c : sht30_read()` | I²C HAL |
|
||
| `crc8_sht3x()` | `sht30.c : sht30_crc8()` | 동일 알고리즘 |
|
||
| `classify_reading()` | reporter 호출부 `metric_status` | plausibility 만. 임계는 서버 |
|
||
| `config.py` | `config.h` (+ flash 설정 옵션) | |
|
||
| `setup_logger()` | `log.c` (UART) | |
|
||
| `systemd/*.service` | IWDG + 메인 재연결 | |
|
||
|
||
---
|
||
|
||
## 4. 펌웨어 아키텍처 (FreeRTOS)
|
||
|
||
```
|
||
main()
|
||
├─ HAL/Clock/RNG/IWDG init
|
||
├─ Ethernet/LwIP init (RMII LAN8720)
|
||
├─ Task_Net : 링크 업 대기 → DHCP(or static) → SNTP로 RTC 설정 → "online" 플래그
|
||
├─ Task_Sensor : SHT30 주기(300s) I2C 측정 → temperature_c/humidity_percent + metric_status
|
||
├─ Task_Report : SHT30_REPORT_INTERVAL(300s) 로 api_post_json() (startup → periodic)
|
||
└─ Task_Watchdog: 각 task 헬스 비트 확인 후 IWDG refresh
|
||
```
|
||
|
||
- SHT30 측정값(원값 + plausibility `metric_status`)을 서버로 POST → **서버가 `config.php` METRIC_* 임계와 비교해 고온/저온/고습/저습 시 SMS 발송**.
|
||
- TLS 컨텍스트는 매 요청마다 connect→handshake→POST→close (메모리 절약). 보고 주기가 길어 성능 무관.
|
||
|
||
---
|
||
|
||
## 5. 단계별 마이그레이션 계획
|
||
|
||
### Phase 0 — 환경/툴체인 (폐쇄망 대응)
|
||
- STM32CubeIDE 또는 `arm-none-eabi-gcc` + CMake + OpenOCD/ST-Link.
|
||
- **오프라인 번들**: 툴체인 설치본, CubeF4 HAL 팩, FreeRTOS·LwIP·mbedTLS 소스를
|
||
벤더링(`third_party/`에 고정 버전 포함). 빌드 시 외부 다운로드 0건.
|
||
- ST-Link/J-Link로 플래시 + UART 콘솔 확보.
|
||
|
||
### Phase 1 — 프로젝트 스켈레톤
|
||
- CubeMX로 클럭(HSE 8 MHz → 168 MHz), RMII, I²C, USART, RNG, IWDG, RTC 핀맵 생성.
|
||
- FreeRTOS(CMSIS-RTOS2), LwIP, mbedTLS 통합. CCM RAM에 task stack 배치(linker script).
|
||
- "Hello UART" + LED blink로 부팅 검증.
|
||
|
||
### Phase 2 — 네트워크 브링업
|
||
- LAN8720 RMII 드라이버 + LwIP. DHCP 우선, 실패 시 static fallback(`config.h`).
|
||
- **SNTP 클라이언트로 시간 동기 → RTC 설정** (TLS 인증서 유효기간 검증·`timestamp` 필드에 필수).
|
||
순서 주의: SNTP는 UDP라 TLS 불필요 → TLS보다 먼저 수행(닭-달걀 회피).
|
||
- 링크 다운/DHCP 실패 시 백오프 재시도.
|
||
|
||
### Phase 3 — TLS + HTTPS 클라이언트
|
||
- mbedTLS config 축소(`mbedtls_config.h`): TLS1.2만, ECDHE-RSA + AES-GCM,
|
||
`MBEDTLS_SSL_IN/OUT_CONTENT_LEN ≈ 4096`, MFL extension.
|
||
- 엔트로피: 하드웨어 RNG 연결.
|
||
- **서버 루트 CA PEM을 flash에 임베드**(Cafe24 인증서 체인 확인 필요).
|
||
- `api_post_json(url_path, body, out_resp)` 구현: connect→handshake(SNI)→
|
||
`POST` 헤더 작성→본문 전송→상태코드 파싱.
|
||
- 검증: `https://your-domain/.../sensor_data.php`로 200 수신.
|
||
|
||
### Phase 4 — 서명 (raw-body 방식, R1 해결로 단순화)
|
||
서버가 **raw-body 서명**을 지원하도록 변경 완료 (PHP `verify_signature_raw`, 본 저장소 적용됨):
|
||
- 서버 검증식: `sha256(API_KEY . 요청_본문_바이트_그대로)`, 값은 HTTP 헤더 **`X-Signature`**로 전송.
|
||
- 펌웨어는 **자신이 전송하는 본문 바이트 그대로**를 SHA-256으로 서명만 하면 된다.
|
||
→ 키 정렬·부동소수 포맷·슬래시 이스케이프 **재현 불필요** (SHT30 float 문제 원천 제거).
|
||
- 클라이언트 절차:
|
||
1. `body` 문자열 1회 생성(필드 순서 자유). 2. `sig = sha256_hex(API_KEY || body)`.
|
||
3. `POST` 시 `Content-Type: application/json` + `X-Signature: <sig>` 헤더, 본문=`body`.
|
||
4. 본문의 `signature` 필드는 불필요(있어도 서버가 무시).
|
||
- 서버는 `X-Signature` 헤더가 없으면 레거시 본문 필드 서명으로 폴백 → **RPi 무중단 공존**.
|
||
- 회귀 테스트: `tests/raw_body_signature_test.php` (통과 확인됨).
|
||
|
||
```c
|
||
// 펌웨어: 전송 바이트 그대로 서명
|
||
sig_sha256_hex(body, sig_hex); // sha256(API_KEY || body)
|
||
// HTTP 헤더에 "X-Signature: <sig_hex>" 추가, 본문은 body 그대로 전송
|
||
```
|
||
|
||
### Phase 5 — SHT30 보드 앱 (sensor_id=2)
|
||
- I²C: 예 `I2C1`(PB6/PB7), addr `0x44`, cmd `0x2C06`(high-repeatability), 20 ms 대기, 6 byte read.
|
||
- `sht30_crc8`(poly 0x31, init 0xFF) 온도/습도 각각 검증 — Python과 동일.
|
||
- 변환: `T=-45+175*raw/65535`, `RH=100*raw/65535`, **소수 2자리 반올림**.
|
||
- 필드: `metric_type="sht30"`, `temperature_c`, `humidity_percent`, `metric_status`, `app_version` (`is_leak` 필드 없음).
|
||
- 이벤트: `startup`(부팅 시 1회) → `periodic`(300 s) 주기 측정 보고.
|
||
- ⚠️ **float 서명 패리티**(아래 리스크 R1) 반드시 처리.
|
||
|
||
### Phase 6 — 서버 임계 판정 / SMS (펌웨어 외부)
|
||
- 펌웨어는 `temperature_c`/`humidity_percent` **원값** + `metric_status`(plausibility) 만 보고한다.
|
||
- 고온/저온/고습/저습 **운영 임계는 서버 `php/config.php` 의 `METRIC_*` 상수**가 판정하고, 초과 시 `sensor_data.php` 가 SMS 게이트웨이로 문자를 발송한다.
|
||
- 폐쇄망 재플래시 비용을 피하려 임계를 서버에 두므로, 임계 변경 시 펌웨어 재빌드/재배포가 불필요하다.
|
||
|
||
### Phase 7 — 신뢰성/운영
|
||
- IWDG 워치독, TLS/네트워크 실패 시 지수 백오프 재연결.
|
||
- 브라운아웃 리셋(BOR) 설정, 부팅 카운터.
|
||
- 로그: UART(개발) + (옵션) 실패 통계 서버 보고.
|
||
- 설정 변경: flash 마지막 섹터에 `device_id/location/static IP/API_KEY` 저장 + UART 명령으로 갱신(옵션). 미구현 시 `config.h` 재빌드.
|
||
|
||
### Phase 8 — 검증
|
||
- §7 호스트 서명 패리티 단위 테스트 통과.
|
||
- 실서버(또는 스테이징 PHP)로 `startup/periodic`(SHT30) E2E + 서버 임계 초과 시 SMS 트리거 확인.
|
||
- 24 h 안정성 + 망 단절 복구 + 워치독 동작 시험.
|
||
|
||
### Phase 9 — 빌드/배포 (폐쇄망)
|
||
- 단일 `.bin`/`.hex` 산출, 오프라인 플래시 절차 문서화.
|
||
- 단일 빌드 타깃 `sht30_fw`(컴파일 플래그 `BOARD_SHT30`).
|
||
- 펌웨어 버전 = `app_version` 필드로 보고(`v2606-sht30`, `firmware/VERSION`=`fw-2.0.0`).
|
||
|
||
---
|
||
|
||
## 6. 핵심 코드 스케치
|
||
|
||
### 6.1 본문 생성 + raw-body 서명 (키 정렬·정규화 불필요)
|
||
```c
|
||
// 필드 순서/포맷 자유 — 서버는 보낸 바이트 그대로 서명을 검증한다.
|
||
// SHT30 본문에는 is_leak 필드가 없다(온습도 원값 + metric_status 만 보고).
|
||
int build_body_sht30(char *out, size_t n, const Sht30Msg *m) {
|
||
return snprintf(out, n,
|
||
"{\"device_id\":\"%s\",\"device_location\":\"%s\","
|
||
"\"sensor_id\":%d,\"sensor_name\":\"%s\",\"event_type\":\"%s\","
|
||
"\"metric_type\":\"sht30\",\"temperature_c\":%.2f,\"humidity_percent\":%.2f,"
|
||
"\"metric_status\":\"%s\",\"app_version\":\"%s\",\"timestamp\":%ld}",
|
||
m->device_id, m->device_location, m->sensor_id, m->sensor_name,
|
||
m->event_type, m->temperature_c, m->humidity_percent,
|
||
m->metric_status, m->app_version, (long)m->timestamp);
|
||
}
|
||
|
||
// sha256(API_KEY || body) → hex. body는 전송 바이트와 100% 동일해야 한다.
|
||
void sig_sha256_hex(const char *body, char hex[65]) {
|
||
uint8_t d[32]; mbedtls_sha256_context c;
|
||
mbedtls_sha256_init(&c); mbedtls_sha256_starts(&c, 0);
|
||
mbedtls_sha256_update(&c, (const uint8_t*)API_KEY, strlen(API_KEY));
|
||
mbedtls_sha256_update(&c, (const uint8_t*)body, strlen(body));
|
||
mbedtls_sha256_finish(&c, d); mbedtls_sha256_free(&c);
|
||
for (int i=0;i<32;i++) sprintf(hex+i*2, "%02x", d[i]);
|
||
}
|
||
// POST 시 헤더에 "X-Signature: <hex>" 추가, 본문은 위 body 그대로 전송.
|
||
```
|
||
주의(유일): 서명한 `body` 바이트와 실제 전송 바이트가 **반드시 동일**해야 한다.
|
||
문자열 필드에 `"`나 `\`가 들어갈 수 있으면 JSON 이스케이프만 적용(서버는 그 바이트로 검증하므로 PHP 포맷 재현 불요).
|
||
|
||
### 6.2 SHT30 읽기 (HAL)
|
||
```c
|
||
HAL_I2C_Master_Transmit(&hi2c1, 0x44<<1, (uint8_t[]){0x2C,0x06}, 2, 100);
|
||
HAL_Delay(20);
|
||
uint8_t b[6]; HAL_I2C_Master_Receive(&hi2c1, 0x44<<1, b, 6, 100);
|
||
if (sht30_crc8(b,2)!=b[2] || sht30_crc8(b+3,2)!=b[5]) return ERR_CRC;
|
||
float t = -45.0f + 175.0f*((b[0]<<8|b[1])/65535.0f);
|
||
float rh = 100.0f*((b[3]<<8|b[4])/65535.0f);
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 리스크 & 대응
|
||
|
||
| ID | 리스크 | 영향 | 대응 |
|
||
|---|---|---|---|
|
||
| **R1** | ~~float 서명 패리티 (SHT30)~~ | ~~SHT30 보고 전건 401 가능~~ | ✅ **해결됨**: PHP를 **raw-body 서명**으로 전환(`verify_signature_raw`, `X-Signature` 헤더). 본문 바이트 그대로 서명 → JSON 재직렬화/float 포맷/슬래시 이스케이프 의존 제거. 레거시 본문 필드 서명은 RPi 호환을 위해 이중 모드로 유지. 상세 §5 Phase 4 참조 |
|
||
| **R2** | TLS RAM 부족(192 KB) | 핸드셰이크 실패/리셋 | content_len 4 KB 축소, CCM에 stack, LwIP pbuf 튜닝 |
|
||
| **R3** | Cafe24 인증서 cipher/체인/만료 | TLS 연결 불가 | 사전 `openssl s_client`로 체인·cipher 확인, 루트 CA 임베드, 만료 시 펌웨어 업데이트 절차 |
|
||
| **R4** | SNTP 실패 → TLS 시간검증·timestamp 오류 | 인증/중복판정 오류 | SNTP 재시도, 실패 시 RTC 보존값 사용 + 경보 |
|
||
| **R5** | `/` 이스케이프 누락 | 서명 불일치(값에 `/` 포함 시) | 정규화기에서 `\/` 처리 |
|
||
| **R6** | 임계 판정을 서버에 위임(폐쇄망 재플래시 회피) | 임계 변경이 서버 운영에 종속 | 경보 임계는 서버 `config.php` METRIC_* 가 소유. 펌웨어는 원값+`metric_status` 만 보고(§5 Phase 6) |
|
||
|
||
---
|
||
|
||
## 8. 산출물 / 디렉터리(제안)
|
||
```
|
||
firmware/
|
||
├─ third_party/ {CMSIS, STM32F4_HAL, FreeRTOS, lwip, mbedtls} # 벤더링(폐쇄망)
|
||
├─ common/ {api.c, sig.c, jsonbody.c, net.c, sntp.c, log.c, config.h}
|
||
├─ board_sht30/ {app_sht30.c, sht30.c, main.c}
|
||
├─ certs/ server_ca.pem.h
|
||
├─ test/ test_signature_parity.c # 호스트 빌드, PHP 기대값과 대조
|
||
└─ CMakeLists.txt
|
||
```
|
||
|
||
## 9. 다음 작업
|
||
1. **R1/R3 사전조사**: Cafe24 TLS 체인 확인, float 서명 정책 결정(PHP raw-body 서명으로 전환 권장).
|
||
2. Phase 0–1 스켈레톤 + UART/LED 부팅.
|
||
3. Phase 2–3 네트워크/TLS로 `sensor_data.php` 200 확보.
|
||
4. Phase 4 서명 패리티 테스트 그린.
|
||
5. Phase 5 SHT30 앱 완성 → E2E(+ 서버 임계 SMS 확인).
|