ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 (PC_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#include "shared_primitives/strbuf.h" // pc_sb frame builder
15
16#if PC_ENABLE_CSRF
17
20#include <stdio.h>
21#include <string.h>
22
23namespace
24{
25
26// All CSRF state, owned by one instance (internal linkage): the HMAC secret and the
27// monotonic nonce counter, grouped so it is one named owner, unreachable cross-TU.
28struct CsrfCtx
29{
30 uint8_t secret[32] = {};
31 size_t secret_len = 0;
32 uint64_t counter = 0;
33};
34CsrfCtx s_csrf;
35
36// hex encode/decode now via the shared hex.h primitive.
37
38// Constant-time compare of n characters (no early exit on mismatch).
39bool ct_equal(const char *a, const char *b, size_t n)
40{
41 uint8_t diff = 0;
42 for (size_t i = 0; i < n; i++)
43 {
44 diff |= (uint8_t)(a[i] ^ b[i]);
45 }
46 return diff == 0;
47}
48
49// Hex of the truncated HMAC-SHA256(secret, nonce) into sig_hex (2*CSRF_SIG_BYTES + 1).
50void sign_nonce(const CsrfCtx &c, const uint8_t *nonce, size_t nlen, char *sig_hex)
51{
52 uint8_t mac[PC_HMAC_SHA256_LEN];
53 pc_hmac_sha256(c.secret, c.secret_len, nonce, nlen, mac);
54 pc_hex_encode(mac, CSRF_SIG_BYTES, sig_hex); // truncate the MAC to CSRF_SIG_BYTES
55}
56
57} // namespace
58
59void pc_csrf_set_secret(const uint8_t *secret, size_t len)
60{
61 if (!secret)
62 {
63 s_csrf.secret_len = 0;
64 return;
65 }
66 s_csrf.secret_len = len > sizeof(s_csrf.secret) ? sizeof(s_csrf.secret) : len;
67 memcpy(s_csrf.secret, secret, s_csrf.secret_len);
68}
69
70int pc_csrf_issue(char *out, size_t cap)
71{
72 if (s_csrf.secret_len == 0 || !out || cap < CSRF_TOKEN_BUF)
73 {
74 return 0;
75 }
76
77 uint8_t nonce[CSRF_NONCE_BYTES];
78 uint64_t c = ++s_csrf.counter;
79 for (size_t i = 0; i < CSRF_NONCE_BYTES; i++)
80 {
81 nonce[i] = (uint8_t)(c >> (8 * i));
82 }
83
84 char nhex[CSRF_NONCE_BYTES * 2 + 1];
85 char shex[CSRF_SIG_BYTES * 2 + 1];
86 pc_hex_encode(nonce, CSRF_NONCE_BYTES, nhex);
87 sign_nonce(s_csrf, nonce, CSRF_NONCE_BYTES, shex);
88
89 pc_sb sb_out = {out, cap, 0, true};
90 pc_sb_put(&sb_out, nhex);
91 pc_sb_put(&sb_out, ".");
92 pc_sb_put(&sb_out, shex);
93 int n = (int)pc_sb_finish(&sb_out);
94 // Both guard halves are unreachable given this function's own invariants: nhex/shex are
95 // fixed-length hex strings, so n is always exactly CSRF_NONCE_BYTES*2 + 1 + CSRF_SIG_BYTES*2
96 // (41) and snprintf cannot go negative on this encoding-error-free format; cap is already
97 // guaranteed >= CSRF_TOKEN_BUF (48) by the guard above, which exceeds 41. Kept as the one
98 // place that would otherwise return a bogus length or read out of bounds.
99 return (n > 0 && (size_t)n < cap) ? n : 0; // GCOVR_EXCL_BR_LINE
100}
101
102bool pc_csrf_verify(const char *token)
103{
104 if (s_csrf.secret_len == 0 || !token)
105 {
106 return false;
107 }
108
109 const char *dot = strchr(token, '.');
110 if (!dot)
111 {
112 return false;
113 }
114
115 size_t nhexlen = (size_t)(dot - token);
116 if (nhexlen != CSRF_NONCE_BYTES * 2)
117 {
118 return false;
119 }
120
121 uint8_t nonce[CSRF_NONCE_BYTES];
122 if (pc_hex_decode(token, nhexlen, nonce, sizeof(nonce)) != CSRF_NONCE_BYTES)
123 {
124 return false;
125 }
126
127 const char *sig = dot + 1;
128 if (strnlen(sig, CSRF_SIG_BYTES * 2 + 1) != CSRF_SIG_BYTES * 2)
129 {
130 return false;
131 }
132
133 char expect[CSRF_SIG_BYTES * 2 + 1];
134 sign_nonce(s_csrf, nonce, CSRF_NONCE_BYTES, expect);
135 return ct_equal(sig, expect, CSRF_SIG_BYTES * 2);
136}
137
138void pc_csrf_reset(void)
139{
140 memset(s_csrf.secret, 0, sizeof(s_csrf.secret));
141 s_csrf.secret_len = 0;
142 s_csrf.counter = 0;
143}
144
145#endif // PC_ENABLE_CSRF
Stateless HMAC-signed CSRF token (PC_ENABLE_CSRF).
Base-16 conversion between raw bytes and their ASCII digits.
void pc_hex_encode(const uint8_t *in, uint32_t n, char *out, bool upper=false)
Write n bytes of in into out as 2 * n hex characters plus a NUL.
Definition hex.h:83
int32_t pc_hex_decode(const char *in, uint32_t hexlen, uint8_t *out, uint32_t out_cap)
Read exactly hexlen hex characters from in into out as hexlen / 2 bytes.
Definition hex.h:100
void pc_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[PC_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
#define PC_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.
Definition hmac_sha256.h:30
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30