POSA_LEAKSMS/firmware/common/tls_mbedtls.c
유창욱 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

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;
}