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

13 KiB
Raw Blame History

PORTING_NOTES.md — Raspberry Pi(Python) → STM32F407(C/FreeRTOS) 이식 노트

원본 동작 기준: RPi sht30_monitor.py(SHT30 온습도, sensor_id=2) — 현재 저장소에는 없음; 동작만 이식. 근거 계획: 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 sht30_measure(*t, *rh) HAL I2C 로 0x2C06 → 20ms → 6B read
crc8_sht3x(values) 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 sig_raw_body(key, body, len, hex) raw-body 서명 — float 서명 패리티 문제 원천 제거(§3)
post_reading(reading, event) reporter.h report_sht30_event(event, t, rh, status) 내부 본문 생성→서명→TLS POST→재시도
(본문 dict 생성) 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 api_post_once() + reporter 재시도 TLS 전송은 transport.h/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 applog_init() / LOGI/LOGW/LOGE 파일 로테이션 → UART 콘솔만
env 설정(env_*) / config.py import app_config.h + board_config.h + secrets.h 컴파일 타임 고정(런타임 env 없음)
SimGPIO / SIMULATION (해당 없음) 실제 하드웨어 타깃. 순수 로직은 test/host/ 게이트로 검증

1.2 인프라/공통 매핑

RPi/OS 기능 STM32 모듈
WiFi 유선 Ethernet(LAN8720 RMII) + LwIP net.h
OS NTP SNTP(LwIP) → RTC timesync.h
OpenSSL(requests) mbedTLS(TLS1.2, ECDHE-RSA+AES-GCM, 소프트웨어 SHA/AES) tls.h
Python threads FreeRTOS tasks/queue/timer (네이티브 API) board_sht30/main.c
systemd 재시작 IWDG 워치독 + 재연결 백오프 watchdog.h
로테이팅 파일 로그 UART 콘솔 로그 applog.h
클럭/부팅 HAL_Init + SystemClock(168MHz) + LED bsp.h

2. 동작 패리티 (값/타이밍/이벤트)

서버가 보는 동작은 RPi 와 동일하게 유지한다. 주요 상수는 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_percentconfig.phpMETRIC_* 임계와 비교해 고온/저온/고습/저습 시 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).
  • 식별 헤더: RPi 는 X-Device-Id / User-Agent 를 보냈다. 펌웨어는 httpapi.h http_build_post()X-Device-IdX-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.phpis_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 필드에 넣었다. 펌웨어는 자신이 실제 전송하는 본문 바이트 그대로 서명만 하면 된다:

/* 펌웨어 절차 (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.phpMETRIC_* 임계와 비교해 고온/저온/고습/저습 조건이면 SMS 게이트웨이로 문자를 발송한다.

이 분리의 이유: 폐쇄망에서는 펌웨어 재플래시 비용이 크다. 임계값을 서버 config.php 에 두면 현장에서 보드를 다시 굽지 않고도 경보 기준을 조정할 수 있다. 따라서 펌웨어 APP_SHT30_* 범위는 물리 타당성 한계로만 쓰고, 운영 경보 임계는 서버가 소유한다.


5. 열린 하드웨어/운영 결정 항목 — TODO(hw)

코드는 합리적 기본값으로 컴파일되지만, 아래는 사람이 사이트/보드에 맞춰 확정해야 한다. 핀 상세는 HARDWARE.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.phpAPI_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.phpsha256(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).