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 제외.
619 lines
23 KiB
C
619 lines
23 KiB
C
/* =============================================================================
|
|
* tls_mbedtls.c - mbedTLS(TLS 1.2 클라이언트) over LwIP 소켓 전송
|
|
*
|
|
* 역할:
|
|
* - tls.h 구현: tls_init() (부팅 1회), tls_transport_init() (transport_t 바인딩).
|
|
* - transport_t 의 connect/send/recv/close 를 mbedTLS + LwIP 소켓으로 제공.
|
|
* - 엔트로피: STM32 하드웨어 RNG → ctr_drbg 시드 (MBEDTLS_ENTROPY_HARDWARE_ALT).
|
|
* - 서버 인증서: certs/server_ca.h 의 임베드 루트 CA 로 풀 검증(VERIFY_REQUIRED).
|
|
* - SNI: mbedtls_ssl_set_hostname(host).
|
|
*
|
|
* 설계 메모:
|
|
* - 전역 1회 초기화(tls_init)에서 만든 entropy/ctr_drbg/CA(x509_crt) 는 모든
|
|
* 연결이 공유한다(상주). ssl_config 와 ssl_context 는 연결마다 새로 만들고
|
|
* close 에서 해제한다(매 보고가 Connection: close 단발 연결이므로 단순/안전).
|
|
* - LwIP 소켓은 블로킹 모드 + SO_RCVTIMEO 로 타임아웃을 처리한다. mbedTLS BIO
|
|
* 콜백은 EAGAIN/EWOULDBLOCK 을 MBEDTLS_ERR_SSL_WANT_READ/WRITE 로 매핑한다.
|
|
*
|
|
* 폐쇄망(air-gapped): mbedTLS/LwIP 는 third_party 에 벤더링. 런타임 외부 fetch 없음.
|
|
* 인증서는 flash 임베드(런타임 CA 다운로드 불가).
|
|
* ===========================================================================*/
|
|
|
|
#include "tls.h"
|
|
#include "app_config.h"
|
|
#include "applog.h"
|
|
#include "timesync.h" /* timesync_now/timesync_is_set: 인증서 유효기간 검증용 시간 */
|
|
#include "certs/server_ca.h"
|
|
|
|
#include <string.h>
|
|
#include <time.h> /* time_t (mbedTLS PLATFORM_TIME_MACRO 콜백) */
|
|
|
|
/* ── mbedTLS ─────────────────────────────────────────────────────────────── */
|
|
#include "mbedtls/ssl.h"
|
|
#include "mbedtls/entropy.h"
|
|
#include "mbedtls/ctr_drbg.h"
|
|
#include "mbedtls/x509_crt.h"
|
|
#include "mbedtls/error.h"
|
|
#include "mbedtls/platform.h"
|
|
|
|
/* ── LwIP 소켓 ───────────────────────────────────────────────────────────── */
|
|
/* 주의(net 에이전트 lwipopts.h 의존): LWIP_SOCKET=1, LWIP_DNS=1, LWIP_NETCONN=1,
|
|
* LWIP_SO_RCVTIMEO=1, LWIP_SO_SNDTIMEO=1, LWIP_DNS_API_DECLARE_STRUCTS=1(getaddrinfo)
|
|
* 이 켜져 있어야 한다(SO_RCVTIMEO/getaddrinfo 사용). */
|
|
#include "lwip/sockets.h"
|
|
#include "lwip/netdb.h"
|
|
/* errno 값(EWOULDBLOCK/EAGAIN/ECONNRESET 등)은 LwIP 소켓 계층이 set_errno() 로
|
|
* 설정하므로, 표준 <errno.h> 가 아니라 LwIP 의 정의를 사용해 값 일치를 보장한다.
|
|
* (lwipopts.h 에서 LWIP_PROVIDE_ERRNO=1 또는 시스템 errno 매핑 둘 다 호환) */
|
|
#include "lwip/errno.h"
|
|
|
|
/* ── STM32 HAL (하드웨어 RNG) ────────────────────────────────────────────── */
|
|
#include "stm32f4xx_hal.h"
|
|
|
|
/* ── FreeRTOS 힙 (플랫폼 calloc/free 매핑) ───────────────────────────────── */
|
|
#include "FreeRTOS.h"
|
|
#include "task.h"
|
|
|
|
/* =============================================================================
|
|
* 0) 플랫폼 메모리: mbedtls calloc/free → FreeRTOS heap
|
|
* mbedtls_config.h 가 CALLOC_MACRO/FREE_MACRO 로 이 두 함수를 가리킨다.
|
|
* ===========================================================================*/
|
|
void *mbedtls_platform_calloc(size_t n, size_t size)
|
|
{
|
|
size_t total = n * size;
|
|
/* 곱셈 오버플로 방어 (n,size 둘 다 0 이 아니면 검사) */
|
|
if (n != 0 && total / n != size) {
|
|
return NULL;
|
|
}
|
|
void *p = pvPortMalloc(total);
|
|
if (p != NULL) {
|
|
memset(p, 0, total); /* calloc 계약: 0 초기화 */
|
|
}
|
|
return p;
|
|
}
|
|
|
|
void mbedtls_platform_free(void *p)
|
|
{
|
|
if (p != NULL) {
|
|
vPortFree(p);
|
|
}
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 0b) 플랫폼 시간: mbedtls_time → RTC/SNTP 기반 Unix 시간
|
|
* mbedtls_config.h 가 MBEDTLS_PLATFORM_TIME_MACRO 로 이 함수를 가리킨다.
|
|
* X.509 인증서 notBefore/notAfter 검증(MBEDTLS_HAVE_TIME_DATE)에 사용된다.
|
|
* 시간 미동기 시 0 을 반환하지만, tls_connect() 가 timesync_is_set() 으로
|
|
* 선제 차단하므로 0 시각으로 검증이 수행되는 일은 없다(이중 안전장치).
|
|
* ===========================================================================*/
|
|
time_t tls_platform_time(time_t *t)
|
|
{
|
|
time_t now = (time_t)timesync_now();
|
|
if (t != NULL) {
|
|
*t = now;
|
|
}
|
|
return now;
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 1) 하드웨어 RNG (STM32 RNG 주변장치) + 엔트로피 폴 콜백
|
|
* mbedtls_config.h: MBEDTLS_ENTROPY_HARDWARE_ALT → 아래 콜백을 entropy 가 호출.
|
|
* ===========================================================================*/
|
|
static RNG_HandleTypeDef s_hrng; /* TLS 모듈이 소유하는 RNG 핸들 */
|
|
static int s_rng_ready = 0;
|
|
|
|
/* RNG 주변장치 초기화 (tls_init 에서 1회). 0 성공, 음수 실패. */
|
|
static int hw_rng_init(void)
|
|
{
|
|
if (s_rng_ready) {
|
|
return 0;
|
|
}
|
|
__HAL_RCC_RNG_CLK_ENABLE(); /* RNG 클럭 게이팅 ON */
|
|
|
|
s_hrng.Instance = RNG;
|
|
if (HAL_RNG_Init(&s_hrng) != HAL_OK) {
|
|
return -1;
|
|
}
|
|
s_rng_ready = 1;
|
|
return 0;
|
|
}
|
|
|
|
/* mbedTLS 하드웨어 엔트로피 소스.
|
|
* RNG 로 32비트 난수를 반복 생성하여 output[0..len) 을 채운다.
|
|
* 반환: 0 성공(*olen=len), 음수 실패. */
|
|
int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
|
|
{
|
|
(void)data;
|
|
|
|
if (!s_rng_ready) {
|
|
if (hw_rng_init() != 0) {
|
|
return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
|
|
}
|
|
}
|
|
|
|
size_t produced = 0;
|
|
while (produced < len) {
|
|
uint32_t rnd = 0;
|
|
if (HAL_RNG_GenerateRandomNumber(&s_hrng, &rnd) != HAL_OK) {
|
|
/* RNG 클럭/시드 오류(CEIS/SEIS). 핸들 재초기화 후 1회 재시도하지 않고
|
|
* 즉시 실패 보고 — 엔트로피 품질을 절대 가장하지 않는다. */
|
|
return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
|
|
}
|
|
size_t chunk = len - produced;
|
|
if (chunk > sizeof(rnd)) {
|
|
chunk = sizeof(rnd);
|
|
}
|
|
memcpy(output + produced, &rnd, chunk);
|
|
produced += chunk;
|
|
}
|
|
|
|
*olen = produced;
|
|
return 0;
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 2) 전역 TLS 상태 (모든 연결 공유, 상주)
|
|
* ===========================================================================*/
|
|
static mbedtls_entropy_context s_entropy;
|
|
static mbedtls_ctr_drbg_context s_ctr_drbg;
|
|
static mbedtls_x509_crt s_ca_chain;
|
|
static int s_tls_inited = 0;
|
|
|
|
/* ctr_drbg 개인화 라벨 (선택적 추가 엔트로피) */
|
|
static const char *const TLS_DRBG_PERS = "sht30-sms-stm32-tls";
|
|
|
|
/* mbedTLS 오류 코드를 사람이 읽는 문자열로 로그 (MBEDTLS_ERROR_C 활성 시). */
|
|
static void log_mbedtls_err(const char *what, int ret)
|
|
{
|
|
#if defined(MBEDTLS_ERROR_C)
|
|
char buf[96];
|
|
mbedtls_strerror(ret, buf, sizeof(buf));
|
|
LOGE("TLS %s: -0x%04X (%s)", what, (unsigned)(-ret), buf);
|
|
#else
|
|
LOGE("TLS %s: -0x%04X", what, (unsigned)(-ret));
|
|
#endif
|
|
}
|
|
|
|
int tls_init(void)
|
|
{
|
|
if (s_tls_inited) {
|
|
return 0; /* 멱등 */
|
|
}
|
|
|
|
/* 플랫폼 메모리 콜백은 CALLOC_MACRO/FREE_MACRO 로 정적 바인딩되어 별도 set 불필요.
|
|
* (MBEDTLS_PLATFORM_MEMORY + *_MACRO 사용) */
|
|
|
|
/* 하드웨어 RNG 먼저 준비 (엔트로피 폴 콜백이 사용). */
|
|
if (hw_rng_init() != 0) {
|
|
LOGE("TLS hw_rng_init failed");
|
|
return -1;
|
|
}
|
|
|
|
mbedtls_entropy_init(&s_entropy);
|
|
mbedtls_ctr_drbg_init(&s_ctr_drbg);
|
|
mbedtls_x509_crt_init(&s_ca_chain);
|
|
|
|
/* DRBG 시드: entropy(하드웨어 RNG ALT) 를 소스로 사용. */
|
|
int ret = mbedtls_ctr_drbg_seed(
|
|
&s_ctr_drbg, mbedtls_entropy_func, &s_entropy,
|
|
(const unsigned char *)TLS_DRBG_PERS, strlen(TLS_DRBG_PERS));
|
|
if (ret != 0) {
|
|
log_mbedtls_err("ctr_drbg_seed", ret);
|
|
goto fail;
|
|
}
|
|
|
|
/* 임베드 루트 CA(PEM) 파싱. PEM 은 buflen 에 종결 NUL 포함 필수
|
|
* (server_ca.h 계약: SERVER_CA_PEM_LEN == sizeof(SERVER_CA_PEM)). */
|
|
ret = mbedtls_x509_crt_parse(
|
|
&s_ca_chain, (const unsigned char *)SERVER_CA_PEM, SERVER_CA_PEM_LEN);
|
|
if (ret != 0) {
|
|
/* ret > 0 이면 일부 인증서 파싱 실패 개수. 어느 경우든 신뢰체인 불완전. */
|
|
log_mbedtls_err("x509_crt_parse(CA)", ret);
|
|
goto fail;
|
|
}
|
|
|
|
s_tls_inited = 1;
|
|
LOGI("TLS init ok (CA parsed, DRBG seeded via HW RNG)");
|
|
return 0;
|
|
|
|
fail:
|
|
mbedtls_x509_crt_free(&s_ca_chain);
|
|
mbedtls_ctr_drbg_free(&s_ctr_drbg);
|
|
mbedtls_entropy_free(&s_entropy);
|
|
return -2;
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 3) 연결별 컨텍스트 (transport_t.ctx 가 가리킴)
|
|
* ===========================================================================*/
|
|
typedef struct {
|
|
mbedtls_ssl_context ssl;
|
|
mbedtls_ssl_config conf;
|
|
int fd; /* LwIP 소켓 fd (-1 = 미연결) */
|
|
int in_use; /* 진행 중 연결 여부 */
|
|
} tls_ctx_t;
|
|
|
|
/* 단일 동시 TLS 연결 가정(보고는 단발). 정적 1개로 RAM 고정. 필요 시 풀로 확장. */
|
|
static tls_ctx_t s_tls_ctx;
|
|
|
|
/* ── BIO 콜백: LwIP 소켓 send/recv ↔ mbedTLS WANT_* 매핑 ────────────────── */
|
|
static int bio_send(void *vctx, const unsigned char *buf, size_t len)
|
|
{
|
|
tls_ctx_t *c = (tls_ctx_t *)vctx;
|
|
if (c->fd < 0) {
|
|
return MBEDTLS_ERR_NET_INVALID_CONTEXT;
|
|
}
|
|
int n = (int)lwip_send(c->fd, buf, len, 0);
|
|
if (n >= 0) {
|
|
return n;
|
|
}
|
|
if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR) {
|
|
return MBEDTLS_ERR_SSL_WANT_WRITE;
|
|
}
|
|
if (errno == EPIPE || errno == ECONNRESET) {
|
|
return MBEDTLS_ERR_NET_CONN_RESET;
|
|
}
|
|
return MBEDTLS_ERR_NET_SEND_FAILED;
|
|
}
|
|
|
|
static int bio_recv(void *vctx, unsigned char *buf, size_t len)
|
|
{
|
|
tls_ctx_t *c = (tls_ctx_t *)vctx;
|
|
if (c->fd < 0) {
|
|
return MBEDTLS_ERR_NET_INVALID_CONTEXT;
|
|
}
|
|
int n = (int)lwip_recv(c->fd, buf, len, 0);
|
|
if (n > 0) {
|
|
return n;
|
|
}
|
|
if (n == 0) {
|
|
return 0; /* 원격 종료 — mbedTLS 는 0 을 정상 종료로 처리 */
|
|
}
|
|
if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR) {
|
|
/* SO_RCVTIMEO 만료도 여기로 온다(EWOULDBLOCK). 상위 핸드셰이크/read 루프가
|
|
* 자체 타임아웃을 관리하므로 WANT_READ 로 재시도 신호를 준다. */
|
|
return MBEDTLS_ERR_SSL_WANT_READ;
|
|
}
|
|
if (errno == ECONNRESET) {
|
|
return MBEDTLS_ERR_NET_CONN_RESET;
|
|
}
|
|
return MBEDTLS_ERR_NET_RECV_FAILED;
|
|
}
|
|
|
|
/* ── 소켓 정리 헬퍼 ──────────────────────────────────────────────────────── */
|
|
static void close_socket(tls_ctx_t *c)
|
|
{
|
|
if (c->fd >= 0) {
|
|
lwip_close(c->fd);
|
|
c->fd = -1;
|
|
}
|
|
}
|
|
|
|
/* DNS 해석 + TCP connect. 0 성공, 음수 실패. fd 는 c->fd 에 저장. */
|
|
static int tcp_connect(tls_ctx_t *c, const char *host, uint16_t port, uint32_t timeout_ms)
|
|
{
|
|
char portstr[6];
|
|
/* uint16_t 포트 → 문자열 (최대 "65535") */
|
|
{
|
|
unsigned p = port;
|
|
int idx = 5;
|
|
portstr[idx] = '\0';
|
|
if (p == 0) {
|
|
portstr[--idx] = '0';
|
|
} else {
|
|
while (p > 0 && idx > 0) {
|
|
portstr[--idx] = (char)('0' + (p % 10));
|
|
p /= 10;
|
|
}
|
|
}
|
|
/* 좌측 정렬로 복사 */
|
|
memmove(portstr, portstr + idx, (size_t)(6 - idx));
|
|
}
|
|
|
|
struct addrinfo hints;
|
|
struct addrinfo *res = NULL;
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_INET; /* IPv4 (LAN8720 + LwIP 기본 구성) */
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_protocol = IPPROTO_TCP;
|
|
|
|
int gai = lwip_getaddrinfo(host, portstr, &hints, &res);
|
|
if (gai != 0 || res == NULL) {
|
|
LOGE("TLS DNS resolve failed for %s (gai=%d)", host, gai);
|
|
return -1;
|
|
}
|
|
|
|
int fd = lwip_socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
|
if (fd < 0) {
|
|
LOGE("TLS socket() failed");
|
|
lwip_freeaddrinfo(res);
|
|
return -2;
|
|
}
|
|
|
|
/* 수신/송신 타임아웃 설정 (블로킹 + SO_*TIMEO). */
|
|
struct timeval tv;
|
|
tv.tv_sec = (long)(timeout_ms / 1000u);
|
|
tv.tv_usec = (long)((timeout_ms % 1000u) * 1000u);
|
|
lwip_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
lwip_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
|
|
|
if (lwip_connect(fd, res->ai_addr, res->ai_addrlen) != 0) {
|
|
LOGE("TLS TCP connect failed to %s:%u", host, (unsigned)port);
|
|
lwip_close(fd);
|
|
lwip_freeaddrinfo(res);
|
|
return -3;
|
|
}
|
|
|
|
lwip_freeaddrinfo(res);
|
|
c->fd = fd;
|
|
return 0;
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 4) transport_t 콜백 구현
|
|
* ===========================================================================*/
|
|
|
|
/* connect: TCP 연결 → SSL config/setup → 핸드셰이크(+ 인증서 검증). */
|
|
static int tls_connect(void *vctx, const char *host, uint16_t port, uint32_t timeout_ms)
|
|
{
|
|
tls_ctx_t *c = (tls_ctx_t *)vctx;
|
|
int ret;
|
|
|
|
if (!s_tls_inited) {
|
|
LOGE("TLS connect before tls_init()");
|
|
return -1;
|
|
}
|
|
/* 시간이 동기되지 않았으면 인증서 유효기간(notBefore/notAfter) 검증이
|
|
* 무의미해진다(시각 0 이면 모든 인증서가 '아직 유효하지 않음'으로도 보임).
|
|
* 보고 경로는 timesync_wait 로 동기 후 호출하지만, 방어적으로 차단한다. */
|
|
if (!timesync_is_set()) {
|
|
LOGE("TLS connect before time sync (cert validity check would be unsafe)");
|
|
return -1;
|
|
}
|
|
if (c->in_use) {
|
|
/* 이전 연결이 정리되지 않았다면 강제 정리 후 진행. */
|
|
mbedtls_ssl_free(&c->ssl);
|
|
mbedtls_ssl_config_free(&c->conf);
|
|
close_socket(c);
|
|
c->in_use = 0;
|
|
}
|
|
|
|
c->fd = -1;
|
|
mbedtls_ssl_init(&c->ssl);
|
|
mbedtls_ssl_config_init(&c->conf);
|
|
|
|
/* (1) TCP 연결 + DNS */
|
|
ret = tcp_connect(c, host, port, timeout_ms);
|
|
if (ret != 0) {
|
|
goto fail_cleanup;
|
|
}
|
|
|
|
/* (2) SSL config: TLS1.2 클라이언트, 서버 인증 필수, CA, RNG */
|
|
ret = mbedtls_ssl_config_defaults(&c->conf,
|
|
MBEDTLS_SSL_IS_CLIENT,
|
|
MBEDTLS_SSL_TRANSPORT_STREAM,
|
|
MBEDTLS_SSL_PRESET_DEFAULT);
|
|
if (ret != 0) {
|
|
log_mbedtls_err("config_defaults", ret);
|
|
goto fail_cleanup;
|
|
}
|
|
|
|
mbedtls_ssl_conf_authmode(&c->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
|
|
mbedtls_ssl_conf_ca_chain(&c->conf, &s_ca_chain, NULL);
|
|
mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &s_ctr_drbg);
|
|
|
|
/* TLS 1.2 만 허용 (min == max). mbedTLS 3.x API.
|
|
* (config 에 MBEDTLS_SSL_PROTO_TLS1_3 미정의이므로 빌드 자체가 TLS1.2-only 이나,
|
|
* 명시적으로 한 번 더 고정한다.) */
|
|
mbedtls_ssl_conf_min_tls_version(&c->conf, MBEDTLS_SSL_VERSION_TLS1_2);
|
|
mbedtls_ssl_conf_max_tls_version(&c->conf, MBEDTLS_SSL_VERSION_TLS1_2);
|
|
|
|
#if defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH)
|
|
/* RAM 절감: 최대 단편 길이 협상(서버 지원 시). IN_CONTENT_LEN 4096 에 맞춤. */
|
|
mbedtls_ssl_conf_max_frag_len(&c->conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096);
|
|
#endif
|
|
|
|
/* (3) SSL setup */
|
|
ret = mbedtls_ssl_setup(&c->ssl, &c->conf);
|
|
if (ret != 0) {
|
|
log_mbedtls_err("ssl_setup", ret);
|
|
goto fail_cleanup;
|
|
}
|
|
|
|
/* (4) SNI: 서버 호스트명 (인증서 CN/SAN 검증에도 사용) */
|
|
ret = mbedtls_ssl_set_hostname(&c->ssl, host);
|
|
if (ret != 0) {
|
|
log_mbedtls_err("set_hostname", ret);
|
|
goto fail_cleanup;
|
|
}
|
|
|
|
/* (5) BIO: LwIP 소켓 send/recv 콜백 바인딩 */
|
|
mbedtls_ssl_set_bio(&c->ssl, c, bio_send, bio_recv, NULL);
|
|
|
|
/* (6) 핸드셰이크 (WANT_READ/WRITE 재시도). 자체 타임아웃: 벽시계 기반. */
|
|
{
|
|
uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
for (;;) {
|
|
ret = mbedtls_ssl_handshake(&c->ssl);
|
|
if (ret == 0) {
|
|
break; /* 핸드셰이크 완료 */
|
|
}
|
|
if (ret != MBEDTLS_ERR_SSL_WANT_READ &&
|
|
ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
log_mbedtls_err("handshake", ret);
|
|
goto fail_cleanup;
|
|
}
|
|
/* WANT_* : 소켓 타임아웃(SO_RCVTIMEO)이 이미 블로킹을 제한한다.
|
|
* 전체 핸드셰이크 상한도 별도로 강제한다. */
|
|
uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
if ((now - start) >= timeout_ms) {
|
|
LOGE("TLS handshake timeout (%ums)", (unsigned)timeout_ms);
|
|
ret = -1;
|
|
goto fail_cleanup;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* (7) 인증서 검증 결과 확인 (VERIFY_REQUIRED 면 핸드셰이크가 이미 실패하지만,
|
|
* 진단을 위해 명시적으로 로그한다). */
|
|
{
|
|
uint32_t flags = mbedtls_ssl_get_verify_result(&c->ssl);
|
|
if (flags != 0) {
|
|
#if defined(MBEDTLS_X509_REMOVE_INFO)
|
|
LOGE("TLS cert verify failed: flags=0x%08X", (unsigned)flags);
|
|
#else
|
|
char vbuf[160];
|
|
mbedtls_x509_crt_verify_info(vbuf, sizeof(vbuf), " ! ", flags);
|
|
LOGE("TLS cert verify failed (0x%08X):\n%s", (unsigned)flags, vbuf);
|
|
#endif
|
|
ret = -1;
|
|
goto fail_cleanup;
|
|
}
|
|
}
|
|
|
|
c->in_use = 1;
|
|
LOGI("TLS connected: %s:%u (%s, %s)",
|
|
host, (unsigned)port,
|
|
mbedtls_ssl_get_version(&c->ssl),
|
|
mbedtls_ssl_get_ciphersuite(&c->ssl));
|
|
return 0;
|
|
|
|
fail_cleanup:
|
|
mbedtls_ssl_free(&c->ssl);
|
|
mbedtls_ssl_config_free(&c->conf);
|
|
close_socket(c);
|
|
c->in_use = 0;
|
|
return (ret < 0) ? ret : -100;
|
|
}
|
|
|
|
/* send: buf 전체 전송(부분 쓰기 루프). 전송 바이트(>0) / 음수 실패. */
|
|
static int tls_send(void *vctx, const uint8_t *buf, size_t len, uint32_t timeout_ms)
|
|
{
|
|
tls_ctx_t *c = (tls_ctx_t *)vctx;
|
|
if (!c->in_use) {
|
|
return -1;
|
|
}
|
|
|
|
size_t sent = 0;
|
|
uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
|
|
while (sent < len) {
|
|
int ret = mbedtls_ssl_write(&c->ssl, buf + sent, len - sent);
|
|
if (ret > 0) {
|
|
sent += (size_t)ret;
|
|
continue;
|
|
}
|
|
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
if ((now - start) >= timeout_ms) {
|
|
LOGE("TLS send timeout");
|
|
return -2;
|
|
}
|
|
continue; /* 블로킹 소켓이 곧 진행시킨다 */
|
|
}
|
|
log_mbedtls_err("ssl_write", ret);
|
|
return -3;
|
|
}
|
|
return (int)sent;
|
|
}
|
|
|
|
/* recv: 최대 cap 바이트 1회 수신. 바이트(>0) / 0 종료 / 음수 오류. */
|
|
static int tls_recv(void *vctx, uint8_t *buf, size_t cap, uint32_t timeout_ms)
|
|
{
|
|
tls_ctx_t *c = (tls_ctx_t *)vctx;
|
|
if (!c->in_use) {
|
|
return -1;
|
|
}
|
|
if (cap == 0) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
|
|
for (;;) {
|
|
int ret = mbedtls_ssl_read(&c->ssl, buf, cap);
|
|
if (ret > 0) {
|
|
return ret; /* 읽은 바이트 */
|
|
}
|
|
if (ret == 0 ||
|
|
ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
|
|
return 0; /* 정상 종료 */
|
|
}
|
|
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
if ((now - start) >= timeout_ms) {
|
|
LOGE("TLS recv timeout");
|
|
return -2;
|
|
}
|
|
continue;
|
|
}
|
|
log_mbedtls_err("ssl_read", ret);
|
|
return -3;
|
|
}
|
|
}
|
|
|
|
/* close: close_notify → ssl/conf 해제 → 소켓 close. */
|
|
static void tls_close(void *vctx)
|
|
{
|
|
tls_ctx_t *c = (tls_ctx_t *)vctx;
|
|
if (!c->in_use) {
|
|
/* 이미 정리됨 (또는 connect 실패). 소켓만 혹시 남아있으면 닫는다. */
|
|
close_socket(c);
|
|
return;
|
|
}
|
|
|
|
/* close_notify 는 best-effort. 죽은/멈춘 피어에서 무한 대기하지 않도록
|
|
* 짧은 벽시계 상한(CLOSE_NOTIFY_MAX_MS)으로 묶는다. (블로킹 소켓은 매 시도마다
|
|
* SO_SNDTIMEO 까지 블록되므로, 상한이 없으면 reporter 가 TLS 뮤텍스를 쥔 채
|
|
* 여러 차례 길게 멈출 수 있다.) 상한 초과 시 그냥 해제/소켓 close 로 진행한다. */
|
|
{
|
|
const uint32_t CLOSE_NOTIFY_MAX_MS = 1000u;
|
|
uint32_t start = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
int ret;
|
|
do {
|
|
ret = mbedtls_ssl_close_notify(&c->ssl);
|
|
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
|
break; /* 완료 또는 (무시 가능한) 오류 */
|
|
}
|
|
uint32_t now = (uint32_t)xTaskGetTickCount() * (uint32_t)portTICK_PERIOD_MS;
|
|
if ((now - start) >= CLOSE_NOTIFY_MAX_MS) {
|
|
break; /* 상한 초과 → best-effort 포기 */
|
|
}
|
|
} while (1);
|
|
}
|
|
|
|
mbedtls_ssl_free(&c->ssl);
|
|
mbedtls_ssl_config_free(&c->conf);
|
|
close_socket(c);
|
|
c->in_use = 0;
|
|
}
|
|
|
|
/* =============================================================================
|
|
* 5) transport_t 바인딩
|
|
* ===========================================================================*/
|
|
int tls_transport_init(transport_t *t)
|
|
{
|
|
if (t == NULL) {
|
|
return -1;
|
|
}
|
|
if (!s_tls_inited) {
|
|
LOGE("tls_transport_init before tls_init()");
|
|
return -2;
|
|
}
|
|
|
|
/* 정적 연결 컨텍스트 초기 상태로 리셋(이전 잔여 정리). */
|
|
if (s_tls_ctx.in_use) {
|
|
mbedtls_ssl_free(&s_tls_ctx.ssl);
|
|
mbedtls_ssl_config_free(&s_tls_ctx.conf);
|
|
}
|
|
close_socket(&s_tls_ctx);
|
|
memset(&s_tls_ctx, 0, sizeof(s_tls_ctx));
|
|
s_tls_ctx.fd = -1;
|
|
|
|
t->ctx = &s_tls_ctx;
|
|
t->connect = tls_connect;
|
|
t->send = tls_send;
|
|
t->recv = tls_recv;
|
|
t->close = tls_close;
|
|
return 0;
|
|
}
|