DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
csrf.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file csrf.cpp
6 * @brief Stateless HMAC-signed CSRF token implementation (DETWS_ENABLE_CSRF).
7 *
8 * The token is `<nonce_hex>.<sig_hex>`; the signature is the first CSRF_SIG_BYTES
9 * of HMAC-SHA256(secret, nonce). Verification recomputes the HMAC over the
10 * embedded nonce and constant-time compares - no server-side session state.
11 */
12
13#include "csrf.h"
14
15#if DETWS_ENABLE_CSRF
16
19#include <stdio.h>
20#include <string.h>
21
22namespace
23{
24
25// All CSRF state, owned by one instance (internal linkage): the HMAC secret and the
26// monotonic nonce counter, grouped so it is one named owner, unreachable cross-TU.
27struct CsrfCtx
28{
29 uint8_t secret[32] = {};
30 size_t secret_len = 0;
31 uint64_t counter = 0;
32};
33CsrfCtx s_csrf;
34
35// hex encode/decode now via the shared hex.h primitive.
36
37// Constant-time compare of n characters (no early exit on mismatch).
38bool ct_equal(const char *a, const char *b, size_t n)
39{
40 uint8_t diff = 0;
41 for (size_t i = 0; i < n; i++)
42 diff |= (uint8_t)(a[i] ^ b[i]);
43 return diff == 0;
44}
45
46// Hex of the truncated HMAC-SHA256(secret, nonce) into sig_hex (2*CSRF_SIG_BYTES + 1).
47void sign_nonce(const CsrfCtx &c, const uint8_t *nonce, size_t nlen, char *sig_hex)
48{
49 uint8_t mac[SSH_HMAC_SHA256_LEN];
50 ssh_hmac_sha256(c.secret, c.secret_len, nonce, nlen, mac);
51 det_hex_encode(mac, CSRF_SIG_BYTES, sig_hex); // truncate the MAC to CSRF_SIG_BYTES
52}
53
54} // namespace
55
56void csrf_set_secret(const uint8_t *secret, size_t len)
57{
58 if (!secret)
59 {
60 s_csrf.secret_len = 0;
61 return;
62 }
63 s_csrf.secret_len = len > sizeof(s_csrf.secret) ? sizeof(s_csrf.secret) : len;
64 memcpy(s_csrf.secret, secret, s_csrf.secret_len);
65}
66
67int csrf_issue(char *out, size_t cap)
68{
69 if (s_csrf.secret_len == 0 || !out || cap < CSRF_TOKEN_BUF)
70 return 0;
71
72 uint8_t nonce[CSRF_NONCE_BYTES];
73 uint64_t c = ++s_csrf.counter;
74 for (size_t i = 0; i < CSRF_NONCE_BYTES; i++)
75 nonce[i] = (uint8_t)(c >> (8 * i));
76
77 char nhex[CSRF_NONCE_BYTES * 2 + 1];
78 char shex[CSRF_SIG_BYTES * 2 + 1];
79 det_hex_encode(nonce, CSRF_NONCE_BYTES, nhex);
80 sign_nonce(s_csrf, nonce, CSRF_NONCE_BYTES, shex);
81
82 int n = snprintf(out, cap, "%s.%s", nhex, shex);
83 return (n > 0 && (size_t)n < cap) ? n : 0;
84}
85
86bool csrf_verify(const char *token)
87{
88 if (s_csrf.secret_len == 0 || !token)
89 return false;
90
91 const char *dot = strchr(token, '.');
92 if (!dot)
93 return false;
94
95 size_t nhexlen = (size_t)(dot - token);
96 if (nhexlen != CSRF_NONCE_BYTES * 2)
97 return false;
98
99 uint8_t nonce[CSRF_NONCE_BYTES];
100 if (det_hex_decode(token, nhexlen, nonce, sizeof(nonce)) != CSRF_NONCE_BYTES)
101 return false;
102
103 const char *sig = dot + 1;
104 if (strnlen(sig, CSRF_SIG_BYTES * 2 + 1) != CSRF_SIG_BYTES * 2)
105 return false;
106
107 char expect[CSRF_SIG_BYTES * 2 + 1];
108 sign_nonce(s_csrf, nonce, CSRF_NONCE_BYTES, expect);
109 return ct_equal(sig, expect, CSRF_SIG_BYTES * 2);
110}
111
112void csrf_reset(void)
113{
114 memset(s_csrf.secret, 0, sizeof(s_csrf.secret));
115 s_csrf.secret_len = 0;
116 s_csrf.counter = 0;
117}
118
119#endif // DETWS_ENABLE_CSRF
Stateless HMAC-signed CSRF token (DETWS_ENABLE_CSRF).
Tiny no-stdlib hex encode / decode / nibble helpers (one shared copy).
int det_hex_decode(const char *in, size_t hexlen, uint8_t *out, size_t out_cap)
Decode exactly hexlen hex chars into out (hexlen/2 bytes).
Definition hex.h:61
void det_hex_encode(const uint8_t *in, size_t n, char *out, bool upper=false)
Encode n bytes as 2*n hex chars + NUL into out (needs cap >= 2*n+1).
Definition hex.h:46
void ssh_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[SSH_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
#define SSH_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.