cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
https_post.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
10
11/******************************************************************
12 * 1. Included files
13 ******************************************************************/
14
15#include "https_post.h"
16#include "civil_time.h"
17
18#include <string.h>
19#include <strings.h> /* strcasecmp */
20#include <time.h> /* time */
21#include <inttypes.h> /* PRId64 */
22
23/* CW_Utils.h pulls in Arduino.h (via platform_compat.h); it must come before
24 * any lwip-including IDF header (esp_http_client.h, esp_netif.h, ...) so that
25 * IPAddress.h declares INADDR_NONE before lwip defines it as a macro. */
26#include "CW_Utils.h" /* hardened memory primitives (CODING_RULES §1.4) */
27
28#include "esp_log.h"
29#include "esp_http_client.h"
30#include "esp_crt_bundle.h"
31#include "wdt.h" /* wdt_feed() — each request can block for a while */
32
33static const char *const TAG = "https";
34
35/* never dump full RPC responses (they can echo credentials embedded
36 * in the URL) — log at most this many bytes on failures. */
37#define RESP_LOG_MAX 80
38
39/* Tolerated disagreement between our clock and the server's Date header.
40 * The header has 1 s granularity and provider clocks are NTP-synced, so only
41 * request latency sits in between — 5 min is enormously generous while still
42 * catching the real attack (back-dating to revive an expired cert moves the
43 * clock by weeks). Same convention as Kerberos. */
44#define CLOCK_SKEW_MAX_S 300
45
46/******************************************************************
47 * 2. Response headers
48 ******************************************************************/
49
51typedef struct {
52 char date[40];
55
66static esp_err_t http_event_cb(esp_http_client_event_t *evt)
67{
68 if ((evt == NULL) || (evt->event_id != HTTP_EVENT_ON_HEADER) ||
69 (evt->user_data == NULL) || (evt->header_key == NULL)) {
70 return ESP_OK;
71 }
72
73 /* Field names are case-insensitive (RFC 9110 §5.1). */
74 if (strcasecmp(evt->header_key, "Date") == 0) {
75 resp_hdrs_t *hdrs = static_cast<resp_hdrs_t *>(evt->user_data);
76 const char *val = (evt->header_value != NULL) ? evt->header_value : "";
77 (void)strncpy(hdrs->date, val, sizeof(hdrs->date) - 1U);
78 hdrs->date[sizeof(hdrs->date) - 1U] = '\0';
79 }
80 return ESP_OK;
81}
82
105static bool clock_corroborated(const char *date_hdr)
106{
107 int64_t server_epoch = 0;
108 int64_t local_epoch;
109 int64_t skew;
110
111 if (date_hdr[0] == '\0') {
112 ESP_LOGW(TAG, "no Date header - clock not corroborated");
113 return true;
114 }
115
116 if (!civil_parse_http_date(date_hdr, &server_epoch)) {
117 ESP_LOGW(TAG, "unparseable Date header - clock not corroborated");
118 return true;
119 }
120
121 local_epoch = static_cast<int64_t>(time(NULL));
122 skew = local_epoch - server_epoch;
123 if (skew < 0) { skew = -skew; }
124
125 if (skew > CLOCK_SKEW_MAX_S) {
126 ESP_LOGE(TAG, "clock off by %" PRId64 " s vs server (local %" PRId64
127 ", server %" PRId64 ") - refusing (spoofed NTP?)",
128 skew, local_epoch, server_epoch);
129 return false;
130 }
131
132 ESP_LOGD(TAG, "clock corroborated (skew %" PRId64 " s)", skew);
133 return true;
134}
135
136/******************************************************************
137 * 3. Public API
138 ******************************************************************/
139
140bool https_post_json(const char *url, const char *body,
141 char *resp_buf, size_t resp_buf_size,
142 const char *user, const char *pass, const char *ca_pem)
143{
144 bool success = false;
145
146 if ((url == NULL) || (body == NULL) || (resp_buf == NULL) ||
147 (resp_buf_size < 2U)) {
148 return false;
149 }
150 /* Each socket step below may take up to timeout_ms, and the caller is on the
151 * task watchdog: feed once per request so a slow one is not a reset. */
152 wdt_feed();
153
154 bool use_auth = ((user != NULL) && (user[0] != '\0') &&
155 (pass != NULL) && (pass[0] != '\0'));
156
157 resp_hdrs_t hdrs;
158 (void)memset(&hdrs, 0, sizeof(hdrs));
159
160 esp_http_client_config_t cfg;
161 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(&cfg), sizeof(cfg));
162 cfg.url = url;
163 cfg.method = HTTP_METHOD_POST;
164 cfg.timeout_ms = 15000;
165 cfg.event_handler = http_event_cb; /* captures the Date header */
166 cfg.user_data = &hdrs;
167 /* if a cert was pinned by the caller, trust ONLY it — otherwise any of the
168 * ~150 CAs in the Mozilla bundle could MITM the RPC. */
169 if (ca_pem != NULL) {
170 cfg.cert_pem = ca_pem;
171 } else {
172 cfg.crt_bundle_attach = esp_crt_bundle_attach;
173 }
174 if (use_auth) {
175 cfg.username = user;
176 cfg.password = pass;
177 cfg.auth_type = HTTP_AUTH_TYPE_BASIC;
178 }
179
180 esp_http_client_handle_t client = esp_http_client_init(&cfg);
181 if (client == NULL) {
182 ESP_LOGE(TAG, "HTTP client init failed");
183 return false;
184 }
185
186 (void)esp_http_client_set_header(client, "Content-Type", "application/json");
187
188 int body_len = static_cast<int>(strlen(body));
189 esp_err_t err = esp_http_client_open(client, body_len);
190 if (err != ESP_OK) {
191 ESP_LOGE(TAG, "HTTP open: %s", esp_err_to_name(err));
192 goto cleanup;
193 }
194
195 if (esp_http_client_write(client, body, body_len) != body_len) {
196 ESP_LOGE(TAG, "HTTP write incomplete");
197 goto cleanup;
198 }
199
200 {
201 int64_t content_length = esp_http_client_fetch_headers(client);
202 (void)content_length; /* may be -1 for chunked; we read until EOF */
203
204 /* Reject the response before reading the body if the authenticated
205 * Date proves our SNTP-supplied clock was spoofed. Consistent with
206 * the existing no-network-time path: refuse rather than adopt the
207 * header's time, so one wrong provider clock can never silently
208 * redefine what this terminal treats as "now". */
209 if (!clock_corroborated(hdrs.date)) {
210 goto cleanup; /* success stays false */
211 }
212
213 wdt_feed(); /* the handshake and headers are done; the body follows */
214 int total = 0;
215 int read;
216 bool full = false;
217 do {
218 int space = static_cast<int>(resp_buf_size - 1U) - total;
219 if (space <= 0) { full = true; break; }
220 read = esp_http_client_read(client, resp_buf + total, space);
221 if (read > 0) { total += read; }
222 } while (read > 0);
223
224 resp_buf[total] = '\0';
225
226 /* A body that filled the buffer and was not finished is a truncated
227 * response, and used to be reported as success — JSON cut mid-string that
228 * the parsers then had to be lucky about. Failure instead: the callers
229 * already treat "no usable answer" as retry or Unconfirmed. */
230 if (full && !esp_http_client_is_complete_data_received(client)) {
231 ESP_LOGE(TAG, "response larger than %u bytes - dropped",
232 static_cast<unsigned>(resp_buf_size - 1U));
233 goto cleanup; /* success stays false */
234 }
235
236 /* a 4xx/5xx body that happens to contain "result" must not
237 * be mistaken for a successful response. */
238 int status = esp_http_client_get_status_code(client);
239 if (status != 200) {
240 ESP_LOGE(TAG, "HTTP status %d: %.*s", status, RESP_LOG_MAX, resp_buf);
241 }
242 success = ((total > 0) && (status == 200));
243 }
244
245cleanup:
246 esp_http_client_close(client);
247 esp_http_client_cleanup(client);
248 return success;
249}
bool civil_parse_http_date(const char *hdr, int64_t *out)
Parse an HTTP-date in IMF-fixdate form into an epoch value.
TZ-independent calendar arithmetic and date-string parsing.
static const char *const TAG
Definition eth_rpc.cpp:33
#define RESP_LOG_MAX
Definition eth_rpc.cpp:43
bool https_post_json(const char *url, const char *body, char *resp_buf, size_t resp_buf_size, const char *user, const char *pass, const char *ca_pem)
POST a JSON body over HTTPS and read the response.
static esp_err_t http_event_cb(esp_http_client_event_t *evt)
HTTP event hook that captures the response Date header.
static bool clock_corroborated(const char *date_hdr)
Cross-check the system clock against the server's HTTP Date header.
#define CLOCK_SKEW_MAX_S
One HTTPS JSON POST, shared by every RPC client in the firmware.
Response headers captured during fetch (see http_event_cb).
char date[40]
Task-watchdog feed that is safe to call from any task.
static void wdt_feed(void)
Feed the task watchdog if, and only if, the calling task is subscribed.
Definition wdt.h:28