ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
oidc.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 oidc.h
6 * @brief OpenID Connect ID Token validation, RS256 (PROTOCORE_ENABLE_OIDC).
7 *
8 * The Relying Party side of OpenID Connect Core 1.0 sec 3.1.3.7 "ID Token Validation". That
9 * document is an OpenID Foundation specification, not an IETF RFC; the token it carries is an
10 * IETF one, a JWS in the Compact Serialization (RFC 7515 sec 7.1) whose `alg` is RS256, which
11 * JWA (RFC 7518 sec 3.3) defines as RSASSA-PKCS1-v1_5 using SHA-256 over the JWS Signing Input
12 * (RFC 7515 sec 2). Given an ID Token and the OP's JWK Set (RFC 7517 sec 5), a verify:
13 * 1. splits the Compact Serialization and requires `alg` == RS256 (RFC 7515 sec 4.1.1;
14 * OIDC Core sec 3.1.3.7 step 7 makes RS256 the default),
15 * 2. selects the signing key by `kid` (RFC 7515 sec 4.1.4), or the sole RSA key when the
16 * JOSE Header carries no `kid`,
17 * 3. checks the signature over the JWS Signing Input (OIDC Core sec 3.1.3.7 step 6,
18 * RFC 7515 sec 5.2) with protocore_rsa_verify(), which is modular exponentiation on
19 * every target and the part's accelerator where there is one,
20 * 4. matches `iss` (step 2, RFC 7519 sec 4.1.1) and `aud` in both its string and array
21 * forms (step 3, RFC 7519 sec 4.1.3), and reads `exp` (step 9, RFC 7519 sec 4.1.4) and
22 * `nbf` (RFC 7519 sec 4.1.5) against the caller's clock,
23 * 5. reports `sub` (RFC 7519 sec 4.1.2), `email` (OIDC Core sec 5.1) and the times in
24 * ::protocore_oidc_claims.
25 *
26 * Zero heap: the decode buffers come from the per-dispatch arena and everything else is fixed.
27 * The verifier fetches nothing. Retrieving the JWK Set from the OP's `jwks_uri` (OpenID Connect
28 * Discovery 1.0 sec 3) over HTTPS and caching it belongs to the caller, which leaves key
29 * rotation and TLS trust in the application's hands and this module deterministic.
30 *
31 * Only RS256 is verified. A MAC-based `alg` (OIDC Core sec 3.1.3.7 step 8) is the JWT module's
32 * (services/security/jwt); ES256 is out of scope.
33 *
34 * The module exports one symbol, @ref Oidc. Everything in oidc.c has internal linkage.
35 *
36 * @author Douglas Quigg (dstroy0)
37 * @date 2026
38 */
39
40#ifndef PROTOCORE_OIDC_H
41#define PROTOCORE_OIDC_H
42
43#include "protocore_config.h" // the entry point: protocore_types.h for the widths
44
45#if PROTOCORE_ENABLE_OIDC
46
48
49/** @brief RSA modulus and signature size in bytes; RFC 7518 sec 3.3 requires at least 2048 bits. */
50#define PROTOCORE_OIDC_RSA_BYTES 256
51
52/** @brief Decode cap for the JOSE Header (RFC 7515 sec 4); it carries only `alg`, `typ` and `kid`. */
53#define PROTOCORE_OIDC_HDR_LEN 512
54
55/** @brief Decode cap for the `iss` Claim (RFC 7519 sec 4.1.1), compared against the Issuer Identifier. */
56#define PROTOCORE_OIDC_ISS_LEN 256
57
58/**
59 * @brief Scratch this module borrows at once: JOSE Header + signature + JWS Payload + `iss`.
60 *
61 * All four are live together across one verify, so the term is their sum.
62 */
63#define PROTOCORE_PLAINTEXT_WORK_OIDC \
64 (PROTOCORE_OIDC_HDR_LEN + PROTOCORE_OIDC_RSA_BYTES + PROTOCORE_OIDC_MAX_LEN + PROTOCORE_OIDC_ISS_LEN)
65
66/** @brief Validation result codes (0 = the ID Token passes every step of OIDC Core sec 3.1.3.7). */
67typedef enum PROTO_ENUM_PACKED
68{
69 PROTOCORE_OIDC_OK = 0, ///< Signature and Claims all pass.
70 PROTOCORE_OIDC_ERR_FORMAT = -1, ///< Not a 3-part Compact Serialization / bad base64url / oversized.
71 PROTOCORE_OIDC_ERR_ALG = -2, ///< JOSE Header `alg` is not RS256 (RFC 7515 sec 4.1.1).
72 PROTOCORE_OIDC_ERR_KEY = -3, ///< No usable RSA JWK (`kid` not found / malformed `n` or `e`).
73 PROTOCORE_OIDC_ERR_SIGNATURE = -4, ///< The RSASSA-PKCS1-v1_5 check failed (RFC 7518 sec 3.3).
74 PROTOCORE_OIDC_ERR_ISS = -5, ///< `iss` is not the Issuer Identifier (sec 3.1.3.7 step 2).
75 PROTOCORE_OIDC_ERR_AUD = -6, ///< `aud` does not contain the client_id (sec 3.1.3.7 step 3).
76 PROTOCORE_OIDC_ERR_EXPIRED = -7, ///< `exp` is missing or not after now (sec 3.1.3.7 step 9).
77 PROTOCORE_OIDC_ERR_NOT_YET = -8, ///< `nbf` is in the future (RFC 7519 sec 4.1.5).
78} protocore_oidc_result;
79
80/** @brief An RSA public key from one JWK (RFC 7518 sec 6.3.1). */
81typedef struct
82{
83 uint8_t n[PROTOCORE_OIDC_RSA_BYTES]; ///< `n` (Modulus), big-endian, right-aligned (RFC 7518 sec 6.3.1.1).
84 uint8_t e[4]; ///< `e` (Exponent), big-endian, right-aligned (RFC 7518 sec 6.3.1.2).
85 proto_bool loaded; ///< True once `n` and `e` are populated.
86} protocore_oidc_key;
87
88/** @brief The Claims a validated ID Token carries (OIDC Core sec 2). */
89typedef struct
90{
91 char sub[PROTOCORE_OIDC_SUB_LEN]; ///< `sub` (Subject) Claim, RFC 7519 sec 4.1.2.
92 char email[PROTOCORE_OIDC_EMAIL_LEN]; ///< `email` Standard Claim (OIDC Core sec 5.1); empty when absent.
93 int64_t iat; ///< `iat` (Issued At), RFC 7519 sec 4.1.6; 0 when absent. 64-bit: past 2038.
94 int64_t exp; ///< `exp` (Expiration Time), RFC 7519 sec 4.1.4. 64-bit.
95} protocore_oidc_claims;
96
97/** @brief RFC 7517 sec 5: the JWK Set a find scans, and the RSA public key it yields. */
98typedef struct
99{
100 const char *jwks; ///< the JWK Set document, `{"keys":[ ... ]}` (RFC 7517 sec 5.1)
101 const char *kid; ///< the `kid` a find selects on (RFC 7517 sec 4.5); NULL or "" takes the first RSA JWK,
102 ///< and a full verify sets it from the token's JOSE Header
103 protocore_oidc_key rsa; ///< the key: written by a find, read by a verify (RFC 7518 sec 6.3.1)
104} OidcKeyArgs;
105
106/** @brief OIDC Core sec 3.1.3.7: what the ID Token's Claims are checked against. */
107typedef struct
108{
109 const char *iss; ///< the Issuer Identifier `iss` must equal (step 2, RFC 7519 sec 4.1.1); NULL or "" skips it
110 const char *aud; ///< the client_id `aud` must contain (step 3, RFC 7519 sec 4.1.3); NULL or "" skips it
111 uint32_t now_unix; ///< the current time `exp` and `nbf` are read against (step 9, RFC 7519 sec 4.1.4 / 4.1.5)
112} OidcExpectArgs;
113
114/**
115 * @brief The Relying Party verifier: one ID Token, one JWK Set, one verdict.
116 *
117 * A caller sets the members a call takes, invokes it through ::Oidc, and reads the outcome off
118 * the same handle. The validation steps are OIDC Core sec 3.1.3.7's.
119 *
120 * @var OidcNs::token the ID Token as a JWS Compact Serialization (RFC 7515 sec 7.1)
121 * @var OidcNs::token_len how many characters of it there are
122 * @var OidcNs::key the JWK Set a find scans and the RSA public key it yields
123 * @var OidcNs::expect the Issuer Identifier, the client_id and the clock a validation uses
124 * @var OidcNs::ok a find's or a header read's true/false outcome
125 * @var OidcNs::result a validation's ::protocore_oidc_result
126 * @var OidcNs::text the `kid` a header read reports, empty when the JOSE Header carries none
127 * @var OidcNs::claims the Claims a validated ID Token carries, cleared before every validation
128 * @var OidcNs::token_kid read `kid` out of the JOSE Header (RFC 7515 sec 4.1.4)
129 * @var OidcNs::jwks_find take the RSA JWK the `kid` names out of the JWK Set (RFC 7517 sec 5.1)
130 * @var OidcNs::verify_with_key validate the ID Token against the key already in @c key.rsa
131 * @var OidcNs::verify the two above in order: resolve the key by the token's `kid`, then validate
132 */
133typedef struct
134{
135 const char *token; ///< the ID Token every call but a find names
136 size_t token_len; ///< how many characters of it there are
137 OidcKeyArgs key; ///< the JWK Set and the key it yields (RFC 7517 sec 5)
138 OidcExpectArgs expect; ///< what the Claims are checked against (OIDC Core sec 3.1.3.7)
139 proto_bool ok;
140 protocore_oidc_result result;
141 char text[PROTOCORE_OIDC_KID_LEN];
142 protocore_oidc_claims claims;
143} OidcVars;
144
145/** @brief The operands and the outcome. */
146extern OidcVars OidcV;
147
148/** @brief The entries. */
149typedef struct
150{
151 void (*const token_kid)(uint8_t *work);
152 void (*const jwks_find)(uint8_t *work);
153 void (*const verify_with_key)(uint8_t *work);
154 void (*const verify)(uint8_t *work);
155} OidcNs;
156
157// What the table binds, defined once in the .c and taking one parameter each: everything
158// else an entry needs is an operand in OidcV or a region of the borrow at a fixed offset.
159void protocore_oidc_token_kid(uint8_t *work);
160void protocore_oidc_jwks_find(uint8_t *work);
161void protocore_oidc_verify_with_key(uint8_t *work);
162void protocore_oidc_verify(uint8_t *work);
163
164// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
165// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
166// `Oidc.token_kid(work)` resolves to a named function and becomes a DIRECT call. An extern table
167// leaves the call indirect and the symbol live at every level, -O2 -flto included.
168static const OidcNs Oidc __attribute__((unused)) = {
169 .token_kid = protocore_oidc_token_kid,
170 .jwks_find = protocore_oidc_jwks_find,
171 .verify_with_key = protocore_oidc_verify_with_key,
172 .verify = protocore_oidc_verify,
173};
174
176
177#endif // PROTOCORE_ENABLE_OIDC
178
179#endif // PROTOCORE_OIDC_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#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