POSA_LEAKSMS/tests/raw_body_signature_test.php
유창욱 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

75 lines
3 KiB
PHP

<?php
declare(strict_types=1);
// raw-body 서명(verify_signature_raw)과 레거시 서명(verify_signature)의
// 동작 및 하위 호환을 검증한다. (STM32 마이그레이션 R1 대응)
$root = dirname(__DIR__);
require_once $root . '/php/config.php';
$failures = [];
function check(bool $condition, string $message): void {
global $failures;
if (!$condition) {
$failures[] = $message;
}
}
check(function_exists('verify_signature_raw'), 'verify_signature_raw() should exist.');
// ── raw-body 서명: 본문 바이트 그대로 HMAC 서명하면 통과 (부동소수 포함) ──
// temperature_c 24.0 처럼 float 표기 차이가 문제되던 케이스도 raw 방식에선 무관.
$body = '{"device_id":"stm32-sht30-01","device_location":"서버실",'
. '"sensor_id":2,"sensor_name":"2번 센서 (SHT30)","event_type":"periodic",'
. '"timestamp":1700000000,"metric_type":"sht30",'
. '"temperature_c":24.0,"humidity_percent":48.5,'
. '"metric_status":"normal","app_version":"v2606-sht30"}';
// 서버/펌웨어 규약: HMAC-SHA256(key=API_KEY, msg=raw_body)
$_SERVER['HTTP_X_SIGNATURE'] = hash_hmac('sha256', $body, API_KEY);
check(verify_signature_raw($body) === true, 'valid raw-body signature should pass.');
// 본문이 1바이트라도 바뀌면 실패해야 한다.
check(verify_signature_raw($body . ' ') === false, 'tampered raw body should fail.');
// 잘못된 서명은 실패해야 한다.
$_SERVER['HTTP_X_SIGNATURE'] = str_repeat('0', 64);
check(verify_signature_raw($body) === false, 'wrong signature should fail.');
// 키 접두 sha256(API_KEY . body) 옛 방식은 더 이상 통과하면 안 된다(HMAC 전환 검증).
$_SERVER['HTTP_X_SIGNATURE'] = hash('sha256', API_KEY . $body);
check(verify_signature_raw($body) === false, 'deprecated secret-prefix sha256 signature must no longer pass.');
// X-Signature 헤더가 없으면 실패해야 한다.
unset($_SERVER['HTTP_X_SIGNATURE']);
check(verify_signature_raw($body) === false, 'missing X-Signature header should fail.');
// 빈 X-Signature 헤더는 실패해야 한다.
$_SERVER['HTTP_X_SIGNATURE'] = '';
check(verify_signature_raw($body) === false, 'empty X-Signature header should fail.');
// ── 레거시 본문 필드 서명은 계속 동작해야 한다 (RPi 하위 호환) ──
$legacy = [
'device_id' => 'stm32-sht30-01',
'sensor_id' => 2,
'event_type' => 'startup',
'timestamp' => 1700000000,
];
ksort($legacy);
$legacy['signature'] = hash('sha256', API_KEY . json_encode($legacy, JSON_UNESCAPED_UNICODE));
check(verify_signature($legacy) === true, 'legacy body-field signature should still pass.');
$tampered = $legacy;
$tampered['event_type'] = 'periodic';
check(verify_signature($tampered) === false, 'tampered legacy payload should fail.');
if ($failures) {
fwrite(STDERR, "Raw-body signature test failures:\n");
foreach ($failures as $failure) {
fwrite(STDERR, "- {$failure}\n");
}
exit(1);
}
echo "Raw-body signature checks passed.\n";