ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
auth.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 auth.h
6 * @brief HTTP authentication: Basic (RFC 7617) and stateless Digest (RFC 7616, SHA-256, qop=auth).
7 *
8 * Digest carries no per-nonce server state. A nonce is `<issue_ms_hex>.<mac_hex>` where the MAC is
9 * SHA-256(secret || issue_ms) truncated to 128 bits, so a returned nonce is authenticated by
10 * recomputing it and aged by reading the issue time back out of it. That is what makes the scheme
11 * safe under the shared-nothing worker model: the keying secret is set once at begin() and is
12 * read-only afterwards, and no worker owns a nonce table another worker has to see.
13 *
14 * The module exports one symbol, @ref Auth. Everything in auth.c has internal linkage.
15 *
16 * @author Douglas Quigg (dstroy0)
17 * @date 2026
18 */
19
20#ifndef PROTOCORE_AUTH_H
21#define PROTOCORE_AUTH_H
22
23#include "protocore_config.h" // the entry point: protocore_types.h for the widths
24
25#if PROTOCORE_ENABLE_AUTH
26
28
29// Named, not defined: the request is the parser's and the route is the table's, and this module only
30// reads them. Declaring them here rather than including their headers is what keeps auth.h includable
31// from protocore.h, which is where both of those types come from.
32struct HttpReq;
33
34/** @brief The id a route carries when it needs no credentials. */
35#define PROTOCORE_AUTH_NONE 0xFFu
36
37/**
38 * @brief The authentication module.
39 *
40 * @var AuthNs::add
41 * Record one credential set and return the id that names it, or @ref PROTOCORE_AUTH_NONE when the table is
42 * full. A route stores that id; it never stores the credential, so no route slot carries key
43 * material and the same set can serve several routes.
44 *
45 * @var AuthNs::check
46 * True when the request carries credentials matching the set @c id names. Which scheme applies is
47 * the set's own property, so the caller does not choose between Basic and Digest.
48 *
49 * For Basic the decoded credential is split on its first colon and BOTH halves are compared in
50 * constant time at their stated lengths - never as strings, because an embedded NUL must not
51 * truncate a submitted password into a shorter one that happens to match.
52 *
53 * For Digest, @c stale comes back true
54 * for credentials that verify against a nonce this server minted but whose issue time falls outside
55 * the nonce lifetime: that is a reissue, not a rejection, so the caller re-challenges with
56 * `stale=true` and the client retries without prompting the user again (RFC 7616 3.3). It is left
57 * untouched on a credential mismatch or a forged nonce, where the only correct answer is no.
58 *
59 * @var AuthNs::challenge
60 * Write the 401 and its `WWW-Authenticate` header for the set @c id names, Basic or Digest as that
61 * set says. @c stale marks the transparent-retry case above.
62 *
63 * @var AuthNs::rekey
64 * (Re)seed the Digest keying secret from the CSPRNG. Runs once per begin(); every nonce the server
65 * mints afterwards is keyed by it, and it never leaves this module.
66 *
67 * @var AuthNs::mint_nonce
68 * Mint a fresh nonce into @c out, which needs a capacity of at least 48.
69 *
70 * @var AuthNs::verify_nonce
71 * True when @c nonce carries a MAC this server could have produced. @c expired is set when the MAC
72 * is authentic but the issue time falls outside the nonce lifetime - authentic and fresh are
73 * separate answers, and only the pair of them justifies trusting the credential that arrived with it.
74 *
75 * @var AuthNs::reset
76 * Empty the credential table. An id names a row by index and a route holds that id, so the table
77 * empties with the routes it is indexed from: protocore_server_reset() calls both. A table that kept its
78 * rows across a reset would reach @ref PROTOCORE_AUTH_NONE after MAX_ROUTES registrations and hand every
79 * later route an id that guards nothing.
80 *
81 * The keying secret is the module's own storage and is not a member: it is written at
82 * @ref AuthNs::rekey and read by nothing outside auth.c, so exposing a handle to it would widen the
83 * surface without giving any caller something it can use.
84 */
85/** @brief RFC 7616 sec 3.2.1 / RFC 7617: one credential row's realm and secret. */
86typedef struct
87{
88 const char *realm; ///< the realm a row is added under
89 const char *user; ///< its username
90 const char *pass; ///< its password
91 proto_bool digest; ///< the row is Digest rather than Basic
92} AuthCredArgs;
93
94/** @brief RFC 7616 sec 3.3: the nonce a challenge carries, and where a mint writes one. */
95typedef struct
96{
97 proto_bool stale; ///< in: the challenge marks a transparent retry; out: the check found one
98 const char *nonce; ///< the nonce a verify judges
99 char *out; ///< where a mint writes
100 size_t cap; ///< how much room it has; at least 48
101} AuthNonceArgs;
102
103/**
104 * A caller sets the members a call takes, invokes it through ::Auth, and reads the outcome off the
105 * same handle.
106 *
107 * @var AuthNs::slot the connection a challenge or a check acts on
108 * @var AuthNs::req the parsed request a check reads its credential from
109 * @var AuthNs::id the credential set a call names
110 * @var AuthNs::cred one credential row: realm, user, pass, and whether it is Digest
111 * @var AuthNs::nonce_args the nonce a mint writes and a verify judges, and the stale flag
112 * @var AuthNs::ok a call's true/false outcome
113 * @var AuthNs::expired the MAC is authentic but the issue time falls outside the nonce lifetime
114 * @var AuthNs::u8 the id an add reports, or ::PROTOCORE_AUTH_NONE when the table is full
115 */
116typedef struct
117{
118 uint8_t slot; ///< the connection a challenge or a check acts on
119 struct HttpReq *req; ///< the parsed request a check reads its credential from
120 uint8_t id; ///< the credential set a call names
121 AuthCredArgs cred; ///< one credential row
122 AuthNonceArgs nonce_args; ///< the nonce a mint writes and a verify judges
123 proto_bool ok;
124 proto_bool expired;
125 uint8_t u8;
126} AuthVars;
127
128/** @brief The operands and the outcome. */
129extern AuthVars AuthV;
130
131/** @brief The entries. */
132typedef struct
133{
134 void (*const add)(uint8_t *work);
135 void (*const check)(uint8_t *work);
136 void (*const challenge)(uint8_t *work);
137 void (*const rekey)(uint8_t *work);
138 void (*const mint_nonce)(uint8_t *work);
139 void (*const verify_nonce)(uint8_t *work);
140 void (*const reset)(uint8_t *work);
141} AuthNs;
142
143// What the table binds, defined once in the .c and taking one parameter each: everything
144// else an entry needs is an operand in AuthV or a region of the borrow at a fixed offset.
145void protocore_auth_add(uint8_t *work);
146void protocore_auth_check(uint8_t *work);
147void protocore_auth_challenge(uint8_t *work);
148void protocore_auth_rekey(uint8_t *work);
149void protocore_auth_mint_nonce(uint8_t *work);
150void protocore_auth_verify_nonce(uint8_t *work);
151void protocore_auth_reset(uint8_t *work);
152
153// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
154// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
155// `Auth.add(work)` resolves to a named function and becomes a DIRECT call. An extern table
156// leaves the call indirect and the symbol live at every level, -O2 -flto included.
157static const AuthNs Auth __attribute__((unused)) = {
158 .add = protocore_auth_add,
159 .check = protocore_auth_check,
160 .challenge = protocore_auth_challenge,
161 .rekey = protocore_auth_rekey,
162 .mint_nonce = protocore_auth_mint_nonce,
163 .verify_nonce = protocore_auth_verify_nonce,
164 .reset = protocore_auth_reset,
165};
166
167/**
168 * @brief The bytes every entry here runs out of: the credential table and the SHA-256 behind it.
169 *
170 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where that
171 * borrow comes from. One table serves every route and every connection, so the bytes belong to this
172 * module rather than to any one caller, and the hash scratch a Digest nonce needs is a second region
173 * of the same span. Taken once from the end of the secure pool, which no mark and no release walks.
174 *
175 * @return the span.
176 */
177uint8_t *protocore_http_auth_span(void);
178
180
181#endif // PROTOCORE_ENABLE_AUTH
182
183#endif // PROTOCORE_AUTH_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