ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
jwt.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file jwt.h
6 * @brief JSON Web Token verification, HS256: RFC 7519 claims carried in an RFC 7515 JWS.
7 *
8 * A token arrives in the JWS Compact Serialization (RFC 7515 sec 7.1),
9 * `BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload) || '.' ||
10 * BASE64URL(JWS Signature)`, normally inside `Authorization: Bearer <token>` (RFC 6750 sec 2.1).
11 * The verifier recomputes the MAC over the JWS Signing Input (RFC 7515 sec 2), compares it against
12 * the signature segment in constant time (RFC 7518 sec 3.2), and reads claims out of the payload
13 * (RFC 7519 sec 4).
14 *
15 * Only HS256 is served: that `alg` value is HMAC using SHA-256 (RFC 7518 sec 3.1), and the JOSE
16 * header's `alg` is required to name it before any MAC is computed (RFC 7515 sec 4.1.1, RFC 8725
17 * sec 3.1), which rejects `none` and every other algorithm substitution. RS256 ID tokens belong to
18 * the OIDC module. Every segment decodes with the URL and filename safe alphabet, padding skipped
19 * (RFC 4648 sec 5).
20 *
21 * The base calls judge the signature alone. The `_at` calls also judge `exp` (RFC 7519 sec 4.1.4)
22 * and `nbf` (sec 4.1.5) against a caller-supplied NumericDate (sec 2) with a skew leeway; `iat`
23 * (sec 4.1.6) is read like any other integer claim. Nothing here allocates: every buffer is a local
24 * of the call that fills it, and the whole path runs on a host build.
25 *
26 * @author Douglas Quigg (dstroy0)
27 * @date 2026
28 */
29
30#ifndef PROTOCORE_JWT_H
31#define PROTOCORE_JWT_H
32
33#include "protocore_config.h" // the entry point: protocore_types.h for the widths
34
35#if PROTOCORE_ENABLE_JWT
36
38
39/** @brief RFC 7515 sec 7.1: the compact serialization a call reads, bare or inside Bearer credentials. */
40typedef struct
41{
42 const char *jws; ///< BASE64URL(header) '.' BASE64URL(payload) '.' BASE64URL(signature)
43 size_t jws_len; ///< readable characters of @c jws, at most PROTOCORE_JWT_MAX_LEN
44 const char *credentials; ///< an Authorization field value: "Bearer" 1*SP b64token (RFC 6750 sec 2.1)
45} JwtTokenArgs;
46
47/** @brief RFC 7518 sec 3.2: the shared key the HMAC-SHA-256 runs under, 256 bits or larger. */
48typedef struct
49{
50 const uint8_t *secret; ///< the key octets
51 size_t secret_len; ///< how many of them there are
52} JwtKeyArgs;
53
54/** @brief RFC 7519 sec 4.1.4 / 4.1.5: the clock `exp` and `nbf` are judged against. */
55typedef struct
56{
57 long now; ///< NumericDate now (RFC 7519 sec 2); 0 or less states there is no wall clock
58 long leeway_s; ///< seconds of clock skew both claims are given
59} JwtTimeArgs;
60
61/** @brief RFC 7519 sec 4: the claim a read names, and where a string claim lands. */
62typedef struct
63{
64 const char *name; ///< the claim's member name inside the JWS Payload, unquoted
65 char *out; ///< where a string claim is written, NUL-terminated
66 size_t out_cap; ///< how many bytes @c out holds
67} JwtClaimArgs;
68
69/** @brief RFC 8693 sec 4.2: the `scope` claim's value and the scope a check demands. */
70typedef struct
71{
72 const char *claim; ///< the claim value: space-delimited, case-sensitive tokens (RFC 6749 sec 3.3)
73 const char *required; ///< the one scope token being looked for
74} JwtScopeArgs;
75
76/**
77 * @brief The HS256 JWT verifier.
78 *
79 * A caller sets the members a call takes, invokes it through ::Jwt, and reads the outcome off the
80 * same handle. No slot member: the verifier owns no table, so a call names its own inputs and
81 * nothing else. No storage member: nothing survives a call, so every buffer is a local of the call
82 * that fills it.
83 *
84 * @var JwtNs::token how the token arrives: the compact serialization, or the Bearer credentials
85 * carrying it
86 * @var JwtNs::key the HS256 shared key (RFC 7518 sec 3.2)
87 * @var JwtNs::time the NumericDate an `_at` call judges the time claims against
88 * @var JwtNs::claim the claim a read names and the buffer a string claim is copied into
89 * @var JwtNs::scope the `scope` claim value and the scope a check demands
90 * @var JwtNs::ok a call's true/false outcome
91 * @var JwtNs::num the integer a claim read returns; a NumericDate for `exp` / `nbf` / `iat`
92 *
93 * @var JwtNs::verify_mac
94 * Validate the JWS Signature over the JWS Signing Input (RFC 7515 sec 5.2). The JOSE header's `alg`
95 * must be HS256, the signature segment must be the 43 characters an unpadded base64url 256-bit MAC
96 * takes, and the compare is constant time (RFC 7518 sec 3.2). Claims are not read.
97 *
98 * @var JwtNs::verify_bearer
99 * ::JwtNs::verify_mac on the b64token inside @c token.credentials (RFC 6750 sec 2.1). The scheme
100 * name is matched without regard to case (RFC 7235 sec 2.1) and @c token.jws is left pointing at
101 * the token that was found.
102 *
103 * @var JwtNs::time_claims_valid
104 * True when the token is inside its validity window: not at or after `exp` (RFC 7519 sec 4.1.4) and
105 * not before `nbf` (sec 4.1.5), each given @c time.leeway_s of skew. An absent claim is not
106 * enforced; a @c time.now of 0 or less states there is no wall clock, so neither claim can be judged
107 * and the answer is true. The signature is not checked here, so pair this with ::JwtNs::verify_mac.
108 *
109 * @var JwtNs::verify_mac_at
110 * ::JwtNs::verify_mac and then ::JwtNs::time_claims_valid.
111 *
112 * @var JwtNs::verify_bearer_at
113 * ::JwtNs::verify_bearer and then ::JwtNs::time_claims_valid.
114 *
115 * @var JwtNs::claim_int
116 * Read the integer claim @c claim.name out of the JWS Payload into @c num. The signature is not
117 * checked, so a verify call comes first.
118 *
119 * @var JwtNs::claim_str
120 * Copy the string claim @c claim.name into @c claim.out, bounded by @c claim.out_cap. A backslash is
121 * dropped and the character after it taken literally, which carries a quoted or backslashed
122 * character through a `sub`, `role` or `scope` value. The signature is not checked.
123 *
124 * @var JwtNs::scope_allows
125 * True when @c scope.required is one whole token of the space-delimited @c scope.claim (RFC 6749
126 * sec 3.3, the syntax RFC 8693 sec 4.2 gives the claim). A prefix of a token never passes.
127 *
128 */
129typedef struct
130{
131 JwtTokenArgs token; ///< how the token arrives
132 JwtKeyArgs key; ///< the HS256 shared key
133 JwtTimeArgs time; ///< the clock the time claims are judged against
134 JwtClaimArgs claim; ///< the claim a read names
135 JwtScopeArgs scope; ///< the scope claim and the scope demanded of it
136 proto_bool ok;
137 long num;
138} JwtVars;
139
140/** @brief The operands and the outcome. */
141extern JwtVars JwtV;
142
143/** @brief The entries. */
144typedef struct
145{
146 void (*const verify_mac)(uint8_t *work);
147 void (*const verify_bearer)(uint8_t *work);
148 void (*const time_claims_valid)(uint8_t *work);
149 void (*const verify_mac_at)(uint8_t *work);
150 void (*const verify_bearer_at)(uint8_t *work);
151 void (*const claim_int)(uint8_t *work);
152 void (*const claim_str)(uint8_t *work);
153 void (*const scope_allows)(uint8_t *work);
154} JwtNs;
155
156// What the table binds, defined once in the .c and taking one parameter each: everything
157// else an entry needs is an operand in JwtV or a region of the borrow at a fixed offset.
158void protocore_jwt_verify_mac(uint8_t *work);
159void protocore_jwt_verify_bearer(uint8_t *work);
160void protocore_jwt_time_claims_valid(uint8_t *work);
161void protocore_jwt_verify_mac_at(uint8_t *work);
162void protocore_jwt_verify_bearer_at(uint8_t *work);
163void protocore_jwt_claim_int(uint8_t *work);
164void protocore_jwt_claim_str(uint8_t *work);
165void protocore_jwt_scope_allows(uint8_t *work);
166
167// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
168// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
169// `Jwt.verify_mac(work)` resolves to a named function and becomes a DIRECT call. An extern table
170// leaves the call indirect and the symbol live at every level, -O2 -flto included.
171static const JwtNs Jwt __attribute__((unused)) = {
172 .verify_mac = protocore_jwt_verify_mac,
173 .verify_bearer = protocore_jwt_verify_bearer,
174 .time_claims_valid = protocore_jwt_time_claims_valid,
175 .verify_mac_at = protocore_jwt_verify_mac_at,
176 .verify_bearer_at = protocore_jwt_verify_bearer_at,
177 .claim_int = protocore_jwt_claim_int,
178 .claim_str = protocore_jwt_claim_str,
179 .scope_allows = protocore_jwt_scope_allows,
180};
181
183
184#endif // PROTOCORE_ENABLE_JWT
185
186#endif // PROTOCORE_JWT_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97