ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
jwt.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 jwt.cpp
6 * @brief JWT HS256 verification + claim extraction (base64url over base64).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_JWT
13
16#include <stdio.h>
17#include <string.h>
18
19// Constant-time equality over @p n bytes (no early-out timing oracle).
20static bool ct_eq(const char *a, const char *b, size_t n)
21{
22 uint8_t diff = 0;
23 for (size_t i = 0; i < n; i++)
24 {
25 diff |= (uint8_t)(a[i] ^ b[i]);
26 }
27 return diff == 0;
28}
29
30// base64url encode/decode are shared with OIDC in the base64 module
31// (pc_base64url_encode / pc_base64url_decode).
32
33// Split a compact JWT into header.payload (signing input) and the signature.
34// Requires exactly two '.' separators. Returns false on a malformed shape.
35static bool pc_jwt_split(const char *token, size_t token_len, size_t *signing_len, const char **sig, size_t *sig_len)
36{
37 const char *d1 = (const char *)memchr(token, '.', token_len);
38 if (!d1)
39 {
40 return false;
41 }
42 size_t rem = token_len - (size_t)(d1 + 1 - token);
43 const char *d2 = (const char *)memchr(d1 + 1, '.', rem);
44 if (!d2)
45 {
46 return false;
47 }
48 size_t rem2 = token_len - (size_t)(d2 + 1 - token);
49 if (memchr(d2 + 1, '.', rem2)) // a third '.' is not a valid JWT
50 {
51 return false;
52 }
53 *signing_len = (size_t)(d2 - token);
54 *sig = d2 + 1;
55 *sig_len = rem2;
56 return true;
57}
58
59// RFC 7515 §5.2: the algorithm used MUST be the one named by the JWS header "alg".
60// Decode the header segment and require alg == "HS256" - this rejects "none",
61// RS256, HS384, and any other algorithm-substitution attempt before the HMAC check.
62static bool pc_jwt_header_alg_is_hs256(const char *header, size_t hlen)
63{
64 uint8_t buf[96];
65 size_t n = pc_base64url_decode(header, hlen, buf, sizeof(buf) - 1);
66 if (n == 0)
67 {
68 return false;
69 }
70 buf[n] = '\0';
71 const char *p = strstr((const char *)buf, "\"alg\"");
72 if (!p)
73 {
74 return false;
75 }
76 p += 5;
77 while (*p == ' ' || *p == ':' || *p == '\t')
78 {
79 p++;
80 }
81 if (*p != '"')
82 {
83 return false;
84 }
85 p++;
86 return strncmp(p, "HS256", 5) == 0 && p[5] == '"';
87}
88
89bool pc_jwt_verify_hs256(const char *token, size_t token_len, const uint8_t *secret, size_t secret_len)
90{
91 if (!token || token_len < 5 || token_len > PC_JWT_MAX_LEN)
92 {
93 return false;
94 }
95
96 size_t signing_len;
97 size_t sig_len;
98 const char *sig;
99 if (!pc_jwt_split(token, token_len, &signing_len, &sig, &sig_len))
100 {
101 return false;
102 }
103
104 // Validate the declared algorithm matches what we verify (RFC 7515 §5.2).
105 const char *d1 = (const char *)memchr(token, '.', token_len);
106 if (!pc_jwt_header_alg_is_hs256(token, (size_t)(d1 - token)))
107 {
108 return false;
109 }
110
111 // HS256 -> 32-byte MAC -> 43 base64url chars (no padding).
112 if (sig_len != 43)
113 {
114 return false;
115 }
116
117 uint8_t mac[PC_HMAC_SHA256_LEN];
118 pc_hmac_sha256(secret, secret_len, (const uint8_t *)token, signing_len, mac);
119
120 char computed[48];
121 // PC_HMAC_SHA256_LEN is a fixed 32 bytes, and unpadded base64url of 32 bytes is always
122 // 43 characters, so this length check can never fail.
123 if (pc_base64url_encode(mac, sizeof(mac), computed) != 43) // GCOVR_EXCL_LINE
124 {
125 return false; // GCOVR_EXCL_LINE
126 }
127 return ct_eq(computed, sig, 43);
128}
129
130bool pc_jwt_bearer_valid(const char *auth_header, const uint8_t *secret, size_t secret_len)
131{
132 if (!auth_header || strncasecmp(auth_header, "Bearer ", 7) != 0)
133 {
134 return false;
135 }
136 const char *tok = auth_header + 7;
137 while (*tok == ' ')
138 {
139 tok++;
140 }
141 return pc_jwt_verify_hs256(tok, strnlen(tok, PC_JWT_MAX_LEN + 1), secret, secret_len);
142}
143
144bool pc_jwt_time_valid(const char *token, size_t token_len, long now_epoch, long leeway_s)
145{
146 if (!token || now_epoch <= 0)
147 {
148 return true; // no wall clock -> time claims cannot be evaluated (the signature is the gate)
149 }
150
151 // Subtraction form (not exp + leeway) so a far-future claim cannot overflow `long`.
152 long exp = 0;
153 if (pc_jwt_claim_int(token, token_len, "exp", &exp) && now_epoch - exp > leeway_s)
154 {
155 return false; // expired (RFC 7519 §4.1.4)
156 }
157
158 long nbf = 0;
159 if (pc_jwt_claim_int(token, token_len, "nbf", &nbf) && nbf - now_epoch > leeway_s)
160 {
161 return false; // not yet valid (RFC 7519 §4.1.5)
162 }
163
164 return true;
165}
166
167bool pc_jwt_verify_hs256_at(const char *token, size_t token_len, const uint8_t *secret, size_t secret_len,
168 long now_epoch, long leeway_s)
169{
170 return pc_jwt_verify_hs256(token, token_len, secret, secret_len) &&
171 pc_jwt_time_valid(token, token_len, now_epoch, leeway_s);
172}
173
174bool pc_jwt_bearer_valid_at(const char *auth_header, const uint8_t *secret, size_t secret_len, long now_epoch,
175 long leeway_s)
176{
177 if (!auth_header || strncasecmp(auth_header, "Bearer ", 7) != 0)
178 {
179 return false;
180 }
181 const char *tok = auth_header + 7;
182 while (*tok == ' ')
183 {
184 tok++;
185 }
186 return pc_jwt_verify_hs256_at(tok, strnlen(tok, PC_JWT_MAX_LEN + 1), secret, secret_len, now_epoch, leeway_s);
187}
188
189bool pc_jwt_claim_int(const char *token, size_t token_len, const char *name, long *out)
190{
191 if (!token || !name || !out)
192 {
193 return false;
194 }
195
196 const char *d1 = (const char *)memchr(token, '.', token_len);
197 if (!d1)
198 {
199 return false;
200 }
201 size_t rem = token_len - (size_t)(d1 + 1 - token);
202 const char *d2 = (const char *)memchr(d1 + 1, '.', rem);
203 if (!d2)
204 {
205 return false;
206 }
207 const char *payload = d1 + 1;
208 size_t payload_len = (size_t)(d2 - payload);
209
210 uint8_t buf[PC_JWT_MAX_LEN];
211 size_t n = pc_base64url_decode(payload, payload_len, buf, sizeof(buf) - 1);
212 if (n == 0)
213 {
214 return false;
215 }
216 buf[n] = '\0';
217
218 char key[40];
219 pc_sb sb_key = {key, sizeof(key), 0, true};
220 pc_sb_put(&sb_key, "\"");
221 pc_sb_put(&sb_key, name);
222 pc_sb_put(&sb_key, "\"");
223 int kn = (int)pc_sb_finish(&sb_key);
224 // kn <= 0 is unreachable: snprintf on a plain "%s" format into a valid buffer cannot report an
225 // encoding error, and the two quotes make the would-be length at least 2.
226 if (kn <= 0 || kn >= (int)sizeof(key)) // GCOVR_EXCL_LINE
227 {
228 return false;
229 }
230 const char *p = strstr((const char *)buf, key);
231 if (!p)
232 {
233 return false;
234 }
235 p += kn;
236 while (*p == ' ' || *p == ':' || *p == '\t')
237 {
238 p++;
239 }
240 bool neg = false;
241 if (*p == '-')
242 {
243 neg = true;
244 p++;
245 }
246 if (*p < '0' || *p > '9')
247 {
248 return false;
249 }
250 unsigned long v = 0; // accumulate unsigned: signed overflow (a huge claim value) is UB
251 while (*p >= '0' && *p <= '9')
252 {
253 v = v * 10UL + (unsigned long)(*p++ - '0');
254 }
255 *out = neg ? (long)(0UL - v) : (long)v; // two's-complement reinterpret, no negation UB
256 return true;
257}
258
259bool pc_jwt_claim_str(const char *token, size_t token_len, const char *name, char *out, size_t out_cap)
260{
261 if (!token || !name || !out || out_cap == 0)
262 {
263 return false;
264 }
265 out[0] = '\0';
266
267 const char *d1 = (const char *)memchr(token, '.', token_len);
268 if (!d1)
269 {
270 return false;
271 }
272 size_t rem = token_len - (size_t)(d1 + 1 - token);
273 const char *d2 = (const char *)memchr(d1 + 1, '.', rem);
274 if (!d2)
275 {
276 return false;
277 }
278 const char *payload = d1 + 1;
279 size_t payload_len = (size_t)(d2 - payload);
280
281 uint8_t buf[PC_JWT_MAX_LEN];
282 size_t n = pc_base64url_decode(payload, payload_len, buf, sizeof(buf) - 1);
283 if (n == 0)
284 {
285 return false;
286 }
287 buf[n] = '\0';
288
289 char key[40];
290 pc_sb sb_key2 = {key, sizeof(key), 0, true};
291 pc_sb_put(&sb_key2, "\"");
292 pc_sb_put(&sb_key2, name);
293 pc_sb_put(&sb_key2, "\"");
294 int kn = (int)pc_sb_finish(&sb_key2);
295 // kn <= 0 is unreachable for the same reason as in pc_jwt_claim_int() above.
296 if (kn <= 0 || kn >= (int)sizeof(key)) // GCOVR_EXCL_LINE
297 {
298 return false;
299 }
300 const char *p = strstr((const char *)buf, key);
301 if (!p)
302 {
303 return false;
304 }
305 p += kn;
306 while (*p == ' ' || *p == ':' || *p == '\t')
307 {
308 p++;
309 }
310 if (*p != '"') // not a string-valued claim
311 {
312 return false;
313 }
314 p++;
315 size_t i = 0;
316 while (*p && *p != '"' && i + 1 < out_cap)
317 {
318 if (*p == '\\' && p[1]) // minimal unescape: drop the backslash, copy the next char
319 {
320 p++;
321 }
322 out[i++] = *p++;
323 }
324 if (*p != '"') // unterminated string or value too long for out
325 {
326 out[0] = '\0';
327 return false;
328 }
329 out[i] = '\0';
330 return true;
331}
332
333bool pc_jwt_scope_allows(const char *scope_claim, const char *required)
334{
335 if (!scope_claim || !required || !*required)
336 {
337 return false;
338 }
339 size_t rlen = strnlen(required, PC_JWT_MAX_LEN + 1);
340 const char *p = scope_claim;
341 while (*p)
342 {
343 while (*p == ' ')
344 {
345 p++;
346 }
347 const char *start = p;
348 while (*p && *p != ' ')
349 {
350 p++;
351 }
352 if ((size_t)(p - start) == rlen && memcmp(start, required, rlen) == 0)
353 {
354 return true;
355 }
356 }
357 return false;
358}
359
360#endif // PC_ENABLE_JWT
size_t pc_base64url_decode(const char *src, size_t src_len, uint8_t *dst, size_t dst_cap)
Decode src_len characters of base64url (RFC 4648 section 5, '-'/'_' alphabet; an '=' ends the input).
Definition base64.cpp:333
size_t pc_base64url_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes as base64url (RFC 4648 section 5): '-' / '_' in place of '+' / '/',...
Definition base64.cpp:304
Base64 encoder/decoder.
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
Zero-heap JWT (JSON Web Token) bearer-auth verification, HS256.
#define PC_JWT_MAX_LEN
Maximum accepted JWT length in bytes (header.payload.signature).
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