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 제외.
15 KiB
메인 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_*+ 콜백 내부 rawnetif_set_up(중첩락 데드락 회피). - (CRITICAL/통합) 로그 UART PA2(=RMII MDIO) 충돌 → USART3(PD8/PD9), FreeRTOS 훅 단일 소유,
-u _printf_float. - (MEDIUM/LOW)
close_notify무한루프 상한,dns_setserver코어락, HW RNGbsp_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.phpMETRIC_ 임계와 비교해 고온/저온/고습/저습 시 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 문제 원천 제거).
- 클라이언트 절차:
body문자열 1회 생성(필드 순서 자유). 2.sig = sha256_hex(API_KEY || body).POST시Content-Type: application/json+X-Signature: <sig>헤더, 본문=body.- 본문의
signature필드는 불필요(있어도 서버가 무시).
- 서버는
X-Signature헤더가 없으면 레거시 본문 필드 서명으로 폴백 → RPi 무중단 공존. - 회귀 테스트:
tests/raw_body_signature_test.php(통과 확인됨).
// 펌웨어: 전송 바이트 그대로 서명
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), addr0x44, cmd0x2C06(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 서명 (키 정렬·정규화 불필요)
// 필드 순서/포맷 자유 — 서버는 보낸 바이트 그대로 서명을 검증한다.
// 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)
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 | ✅ 해결됨: 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. 다음 작업
- R1/R3 사전조사: Cafe24 TLS 체인 확인, float 서명 정책 결정(PHP raw-body 서명으로 전환 권장).
- Phase 0–1 스켈레톤 + UART/LED 부팅.
- Phase 2–3 네트워크/TLS로
sensor_data.php200 확보. - Phase 4 서명 패리티 테스트 그린.
- Phase 5 SHT30 앱 완성 → E2E(+ 서버 임계 SMS 확인).