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

15 KiB
Raw Permalink Blame History

메인 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 핸드셰이크는 소프트웨어로 ~12 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).
    2. POSTContent-Type: application/json + X-Signature: <sig> 헤더, 본문=body.
    3. 본문의 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), 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.phpMETRIC_* 상수가 판정하고, 초과 시 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 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 01 스켈레톤 + UART/LED 부팅.
  3. Phase 23 네트워크/TLS로 sensor_data.php 200 확보.
  4. Phase 4 서명 패리티 테스트 그린.
  5. Phase 5 SHT30 앱 완성 → E2E(+ 서버 임계 SMS 확인).