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 제외.
231 lines
11 KiB
Markdown
231 lines
11 KiB
Markdown
# BUILD_OFFLINE.md — 폐쇄망(air-gapped) 빌드·전달·플래시 절차
|
|
|
|
> **핵심 원칙 (공통 빌드/배포 규칙):** 모든 산출물은 **완전 자급식(all-inclusive)** 이어야 한다.
|
|
> 배포 대상(설치 호스트)은 **인터넷 접근이 없다** — 빌드 시점에도, 런타임에도. 다운로드는 **오직 빌드 머신에서만**
|
|
> 일어나며, 그 결과물(`.bin`)이 에어갭을 넘어 전달된다. 대상은 어떤 것도 직접 가져오지 않는다.
|
|
>
|
|
> 즉: STM32CubeF4 HAL+CMSIS / FreeRTOS-Kernel / lwip / mbedtls 는 모두 `firmware/third_party/` 에
|
|
> **고정 버전으로 벤더링**되어 빌드·런타임에 외부에서 가져오지 않는다. 인증서·SNTP 대상까지 사전 준비한다.
|
|
|
|
전체 흐름:
|
|
|
|
```
|
|
[인터넷 가능 빌드 머신] [에어갭] [격리된 설치 호스트]
|
|
1) 의존성 벤더링(고정 버전) ─┐
|
|
2) secrets.h 작성 ├─▶ 4) cmake+ninja ─▶ sht30_fw.bin/.hex/.elf ─▶(USB/매체)─▶ 5) ST-Link 플래시
|
|
3) 실제 CA 임베드 ─┘ 6) 망 단절 상태로 검증
|
|
7) 실패 시 롤백
|
|
```
|
|
|
|
---
|
|
|
|
## 0. 사전 준비 (빌드 머신, 1회)
|
|
|
|
| 도구 | 버전 | 비고 |
|
|
|------|------|------|
|
|
| `arm-none-eabi-gcc` | **15.2** | 크로스 컴파일러 (newlib 포함) |
|
|
| CMake | ≥ 3.21 | |
|
|
| Ninja | 최신 | |
|
|
| ST-Link 툴 | `st-flash`/`STM32_Programmer_CLI` 또는 OpenOCD | 플래시용 (설치 호스트에도 필요) |
|
|
| `git` | 임의 | 벤더링 시점에만 사용 |
|
|
| (선택) `openssl` | 임의 | 서버 인증서 체인 확인용 |
|
|
|
|
> 빌드 머신에는 위 도구 일체를 **오프라인 설치본**으로도 갖춰 둔다(추후 빌드 머신도 망에서 분리될 수 있음).
|
|
|
|
---
|
|
|
|
## 1. 의존성 벤더링 (빌드 머신에서, 외부 다운로드는 여기서만)
|
|
|
|
`firmware/third_party/` 에 아래를 **고정 버전(태그/커밋 핀)** 으로 배치한다. 빌드·런타임에 추가 fetch 0건.
|
|
|
|
```
|
|
firmware/third_party/
|
|
├─ CMSIS/ ARM CMSIS Core (STM32CubeF4 동봉본)
|
|
├─ STM32F4_HAL/ STM32CubeF4 HAL + Device 헤더 (stm32f4xx_hal_*, stm32f407xx.h, startup, linker .ld)
|
|
├─ FreeRTOS-Kernel/ FreeRTOS 커널 (네이티브 API; CMSIS-RTOS 래퍼는 미사용)
|
|
├─ lwip/ LwIP (tcpip + apps/sntp 포함)
|
|
└─ mbedtls/ mbedTLS (TLS1.2 클라이언트)
|
|
```
|
|
|
|
### 1.1 벤더 스크립트
|
|
|
|
```bash
|
|
# 빌드 머신(인터넷 가능)에서 1회 실행. 고정 버전을 받아 third_party/ 로 정리한다.
|
|
bash firmware/scripts/vendor_deps.sh # (또는 vendor_deps.ps1)
|
|
```
|
|
|
|
> `TODO(hw):` `firmware/scripts/vendor_deps.{sh,ps1}` 는 별도 작업으로 작성/제공된다. 권장 고정 버전(예시):
|
|
> STM32CubeF4 `v1.28.x`, FreeRTOS-Kernel `V11.x`, lwip `STABLE-2.2.x`, mbedtls `v3.6.x` (LTS).
|
|
> 스크립트 미제공 시: 각 릴리스 tarball 을 수동으로 받아 위 디렉터리 구조로 풀고, **버전을 기록**한다.
|
|
|
|
### 1.2 벤더링 검증 (오프라인성 확인)
|
|
|
|
```bash
|
|
# third_party 가 채워졌고, 빌드가 네트워크 없이 가능한지 확인
|
|
ls firmware/third_party/{CMSIS,STM32F4_HAL,FreeRTOS-Kernel,lwip,mbedtls}
|
|
# 무결성: 받은 tarball 의 해시를 릴리스 페이지 게시값과 대조하여 기록
|
|
sha256sum firmware/third_party/*.tar.gz > firmware/third_party/VENDORED_HASHES.txt # (받은 경우)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 비밀값 준비 — `secrets.h` (빌드 머신, 1회)
|
|
|
|
```bash
|
|
cp firmware/common/secrets.h.example firmware/common/secrets.h
|
|
```
|
|
|
|
`secrets.h` 의 `APP_API_KEY` 를 **서버 `php/config.php` 의 `API_KEY` 와 바이트 단위로 동일**하게 채운다.
|
|
(raw-body 서명: `X-Signature = sha256(APP_API_KEY || body)`.)
|
|
|
|
> - `secrets.h` 는 **저장소에 커밋하지 않는다**(`.gitignore` 등록). 산출 `.bin` 안에는 키가 임베드되므로
|
|
> `.bin` 자체를 비밀로 취급한다(공통 보안 규칙: 산출물에 비밀이 새지 않도록 유통 통제).
|
|
> - `TODO(hw):` 운영 API_KEY 값은 운영자만 알고 있으며 본 작업 범위가 아니다.
|
|
|
|
---
|
|
|
|
## 3. 서버 루트 CA 임베드 — `certs/server_ca.c` (빌드 머신, 1회 / 갱신 시)
|
|
|
|
폐쇄망에서는 런타임 CA 다운로드가 불가하므로 **서버(Cafe24) 인증서 체인의 루트 CA PEM 을 펌웨어에 고정**한다.
|
|
[`certs/server_ca.h`](../certs/server_ca.h) 가 `SERVER_CA_PEM` / `SERVER_CA_PEM_LEN` 을 `extern` 으로 선언하므로,
|
|
실제 PEM 을 담은 **`certs/server_ca.c`** 를 생성해야 한다.
|
|
|
|
### 3.1 인증서 체인 확인 (빌드 머신, 망 가능)
|
|
|
|
```bash
|
|
# 실제 서버에서 체인/cipher/만료 확인 (이식 계획 R3)
|
|
openssl s_client -connect your-domain.example:443 -servername your-domain.example -showcerts </dev/null
|
|
# → 출력 체인에서 "루트 CA" 인증서를 식별하여 PEM 으로 저장
|
|
```
|
|
|
|
### 3.2 `server_ca.c` 생성
|
|
|
|
```c
|
|
/* certs/server_ca.c — 실제 Cafe24 루트 CA PEM (운영자가 채움) */
|
|
#include "server_ca.h"
|
|
|
|
const char SERVER_CA_PEM[] =
|
|
"-----BEGIN CERTIFICATE-----\n"
|
|
"...실제 루트 CA 본문...\n"
|
|
"-----END CERTIFICATE-----\n"; /* 마지막 NUL 포함 */
|
|
|
|
/* mbedtls_x509_crt_parse() 는 PEM 일 때 buflen 에 종결 NUL 을 포함해야 한다 */
|
|
const unsigned int SERVER_CA_PEM_LEN = sizeof(SERVER_CA_PEM);
|
|
```
|
|
|
|
> 중요: `SERVER_CA_PEM_LEN == sizeof(...)` (종결 NUL **포함**). `strlen` 을 쓰면 mbedTLS PEM 파싱이 실패한다
|
|
> ([`certs/server_ca.h`](../certs/server_ca.h) 주석 참고).
|
|
>
|
|
> `TODO(hw):` 실제 PEM 본문은 운영 서버의 인증서 발급기관에 따라 달라진다. 위는 자리표시자.
|
|
> 만료/발급기관 교체 시(R3) 폐쇄망이라 OTA 불가 → **펌웨어 재빌드/재배포**가 유일한 갱신 경로다.
|
|
|
|
---
|
|
|
|
## 4. 빌드 — `cmake + ninja` (sht30_fw 단일 타깃, 빌드 머신)
|
|
|
|
```bash
|
|
# 구성 (크로스 툴체인 파일 지정)
|
|
cmake -S firmware -B firmware/build -G Ninja \
|
|
-DCMAKE_TOOLCHAIN_FILE=firmware/cmake/arm-none-eabi.cmake
|
|
|
|
# SHT30 보드 산출물 빌드 (보드 매크로 -DBOARD_SHT30 은 타깃이 자동 주입)
|
|
cmake --build firmware/build --target sht30_fw # → sht30_fw.elf / .bin / .hex
|
|
```
|
|
|
|
산출물:
|
|
|
|
| 파일 | 보드 | 적재 주소 |
|
|
|------|------|-----------|
|
|
| `firmware/build/sht30_fw.bin` | SHT30(sensor_id=2) | `0x08000000` |
|
|
| `firmware/build/sht30_fw.hex` | SHT30(sensor_id=2) | (주소 내장) |
|
|
| `firmware/build/sht30_fw.elf` | SHT30(sensor_id=2) | (심볼/디버그) |
|
|
|
|
빌드 검증 (오프라인성/메모리):
|
|
|
|
```bash
|
|
arm-none-eabi-size firmware/build/sht30_fw.elf # Flash(1MB)/RAM(192KB) 예산 내 확인
|
|
# CCM 배치/IN/OUT_CONTENT_LEN 축소가 적용되어 .data/.bss 가 SRAM 예산을 넘지 않는지 확인 (이식 계획 R2)
|
|
```
|
|
|
|
> 빌드 중 인터넷 접근이 발생하면 **벤더링 누락**이다 — `third_party/` 를 보강하고 외부 fetch 0건을 보장한다.
|
|
> `TODO(hw):` `firmware/cmake/arm-none-eabi.cmake` / `*.ld`(CCM 스택 배치) 는 별도 작업으로 제공된다.
|
|
|
|
### 4.1 (선택) 호스트 패리티 게이트 — 서버 기대값 대조
|
|
|
|
`.bin` 을 만들기 전, **이식성 순수 로직**(서명/JSON/CRC/변환)을 호스트에서 PHP 기대값과 대조한다.
|
|
|
|
```bash
|
|
# firmware/test/host : sha256(API_KEY||body) 와 PHP verify_signature_raw 결과 일치 확인
|
|
python3 firmware/test/host/parity_test.py
|
|
php firmware/test/host/php_verify.php # 동일 본문에 대한 서버측 서명 기대값
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 에어갭 전달 + 플래시 (격리된 설치 호스트)
|
|
|
|
1. `sht30_fw.bin`(또는 `.hex`/`.elf`) (+ 무결성 해시) 를 **신뢰 매체**(USB 등)로 격리 호스트에 전달.
|
|
```bash
|
|
sha256sum firmware/build/sht30_fw.bin > FW_HASHES.txt # 빌드 머신
|
|
# 격리 호스트에서 재계산하여 대조 (무결성)
|
|
```
|
|
2. ST-Link/J-Link 로 플래시 (sht30_fw 고정):
|
|
```bash
|
|
# 권장: 동봉 스크립트(보드 파라미터 없이 sht30_fw 자동 탐지·플래시)
|
|
pwsh firmware/scripts/flash.ps1
|
|
|
|
# 대안: st-flash
|
|
st-flash write sht30_fw.bin 0x08000000
|
|
# 대안: OpenOCD
|
|
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
|
|
-c "program sht30_fw.elf verify reset exit"
|
|
# 대안: ST 공식 CLI
|
|
STM32_Programmer_CLI -c port=SWD -w sht30_fw.hex -v -rst
|
|
```
|
|
3. 플래시 직후 IWDG 가 동작하므로, 정상 부팅하면 주기 refresh 로 리셋되지 않는다(20s 타임아웃).
|
|
|
|
---
|
|
|
|
## 6. 망 단절 상태 검증 (격리 호스트)
|
|
|
|
> 산출물이 **격리망 안에서만** 동작·검증되는지 확인한다. 외부 인터넷 없이 서버(또는 스테이징 PHP)에만 도달한다.
|
|
|
|
1. **콘솔(USART3 PD8/PD9, 115200 8N1)** 으로 부팅 로그 관찰:
|
|
- `bsp_init` → 링크업 → DHCP(또는 static fallback) → **SNTP 시간 동기** → TLS 핸드셰이크 → 첫 `startup` POST.
|
|
2. 서버에서 수신 확인:
|
|
- SHT30 보드: `startup` → (주기 300s) `periodic`, `temperature_c`/`humidity_percent`/`metric_status` 필드.
|
|
- 서버 임계(`config.php` METRIC_*) 초과 시 SMS 발송 트리거 확인.
|
|
- 응답 **HTTP 200** + 서버 로그/DB 적재 확인. 서명 불일치(401)면 §2(API_KEY)/§3(CA) 재점검.
|
|
3. **회복 시험**: LAN 케이블 분리 → 백오프 재시도 로그 → 재연결 시 정상 보고 재개. SNTP 미동기 시 경보(R4).
|
|
4. **워치독 시험**: (디버그) 의도적으로 헬스 루프 정지 → 약 20s 내 자동 리셋 관찰.
|
|
5. **24h 안정성**: 메모리 누수/리셋 루프 없이 주기 보고 지속(이식 계획 Phase 8).
|
|
|
|
> 검증 시 빌드 머신/인터넷에 의존하지 않음을 명시적으로 확인한다(어떤 fetch 도 발생하면 안 됨).
|
|
|
|
---
|
|
|
|
## 7. 롤백
|
|
|
|
| 시나리오 | 절차 |
|
|
|----------|------|
|
|
| 신규 펌웨어 불량 | 직전 **검증된 `.bin`** 을 동일 주소(`0x08000000`)로 재플래시 (이전 산출물을 항상 보관) |
|
|
| RPi 로 임시 복귀 | 서버 `X-Signature` ↔ 레거시 본문 서명 **이중 모드** 가 유지되므로, 같은 사이트에 기존 RPi 노드를 다시 투입해도 서버는 양쪽을 수용(이식 계획 R1) |
|
|
| 인증서 만료(R3) | 새 루트 CA 로 §3 재수행 → §4 재빌드 → §5 재플래시 (OTA 불가) |
|
|
| 전체 무력화 | 플래시 소거 후 직전 골든 이미지 복원: `st-flash erase` → `st-flash write <golden>.bin 0x08000000` |
|
|
|
|
권장: 검증 통과한 모든 `.bin` 은 **버전 라벨 + 해시 + 빌드 일자**로 보관(롤백 자산). 펌웨어 버전은 보고 본문의
|
|
`app_version="v2606-sht30"`(`firmware/VERSION` = `fw-2.0.0`) 로 서버에서 식별 가능.
|
|
|
|
---
|
|
|
|
## 8. 체크리스트 (배포 전 최종)
|
|
|
|
- [ ] `third_party/` 고정 버전 벤더링 완료 + 버전/해시 기록 (외부 fetch 0건)
|
|
- [ ] `secrets.h` 의 `APP_API_KEY` = 서버 `API_KEY`
|
|
- [ ] `certs/server_ca.c` 에 실제 루트 CA PEM 임베드 (`LEN = sizeof`, NUL 포함)
|
|
- [ ] `sht30_fw.bin` 빌드 + `size` 가 Flash/RAM 예산 내
|
|
- [ ] (선택) 호스트 패리티 게이트 PASS
|
|
- [ ] `.bin` 무결성 해시 생성 → 격리 호스트에서 대조
|
|
- [ ] 망 단절 검증: 부팅 → SNTP → TLS → 200, 회복/워치독 시험
|
|
- [ ] 직전 골든 이미지 + 해시 보관(롤백)
|
|
- [ ] HARDWARE.md §0 의 **PA2(USART2↔MDIO) 충돌 해소(USART3 PD8/PD9)** 반영 확인
|