ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
oidc.h
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 oidc.h
6 * @brief OpenID Connect ID-token verification, RS256 (PC_ENABLE_OIDC).
7 *
8 * A relying-party verifier for OIDC ID tokens (RFC 7519 JWT, OpenID Connect Core
9 * 3.1.3.7). Given an ID token and the issuer's JWKS, it:
10 * 1. parses the JWT header and requires `alg` == RS256,
11 * 2. selects the signing key by `kid` (or the sole key if the token has none),
12 * 3. verifies the RSASSA-PKCS1-v1.5 SHA-256 signature (via pc_rsa_verify -
13 * real modular exponentiation; mbedTLS-accelerated on ESP32),
14 * 4. checks `iss`, `aud` (string or array), `exp`, and `nbf` against the
15 * caller's expectations and clock,
16 * 5. extracts `sub` / `email` and the times into ::pc_oidc_claims.
17 *
18 * Zero-heap (fixed stack/BSS buffers) and host-tested against real openssl-signed
19 * RS256 vectors. The verifier is pure: it does NOT fetch anything. Fetching the
20 * discovery document / JWKS over HTTPS and caching keys is the caller's job (do it
21 * off the request hot path with the HTTP client, then pass the JWKS JSON here) -
22 * which keeps key rotation, caching policy, and TLS trust in the application's
23 * hands and the verifier deterministic.
24 *
25 * Only RS256 (the OIDC default and by far the most common) is supported; HS256
26 * shared-secret tokens are the JWT module's job (services/security/jwt), ES256 is out of
27 * scope.
28 *
29 * @author Douglas Quigg (dstroy0)
30 * @date 2026
31 */
32
33#ifndef PROTOCORE_OIDC_H
34#define PROTOCORE_OIDC_H
35
36#include "protocore_config.h"
37#include <stddef.h>
38#include <stdint.h>
39
40#if PC_ENABLE_OIDC
41
42/** @brief RSA-2048 modulus size in bytes (the supported key size). */
43#define PC_OIDC_RSA_BYTES 256
44
45/** @brief Decode cap for the JOSE header segment; it carries only `alg`/`typ`/`kid`. */
46#define PC_OIDC_HDR_LEN 512
47
48/** @brief Decode cap for the `iss` claim, compared against the expected issuer URL. */
49#define PC_OIDC_ISS_LEN 256
50
51/**
52 * @brief Scratch this module borrows at once (header + signature + payload + issuer).
53 *
54 * All four buffers are live together across the verify, so the term is their sum. Stated here,
55 * next to the constants it is built from, because a worst case assembled anywhere else would have
56 * to restate them. server/mmgr/scratch_budget.h collects this with every other borrower's term.
57 */
58#define PC_SCRATCH_WORK_OIDC (PC_OIDC_HDR_LEN + PC_OIDC_RSA_BYTES + PC_OIDC_MAX_LEN + PC_OIDC_ISS_LEN)
59
60/** @brief Verification result codes (0 = success, negatives = failure reasons). */
61enum class pc_oidc_result : int32_t
62{
63 PC_OIDC_OK = 0, ///< Token verified and all claims pass.
64 PC_OIDC_ERR_FORMAT = -1, ///< Not a 3-part JWT / bad base64 / oversized.
65 PC_OIDC_ERR_ALG = -2, ///< Header `alg` is not RS256.
66 PC_OIDC_ERR_KEY = -3, ///< No usable RSA key (kid not found / malformed JWK).
67 PC_OIDC_ERR_SIGNATURE = -4, ///< RSA signature verification failed.
68 PC_OIDC_ERR_ISS = -5, ///< `iss` does not match the expected issuer.
69 PC_OIDC_ERR_AUD = -6, ///< `aud` does not contain the expected audience.
70 PC_OIDC_ERR_EXPIRED = -7, ///< `exp` is missing or in the past.
71 PC_OIDC_ERR_NOT_YET = -8, ///< `nbf` is in the future.
72};
73
74/** @brief A parsed RSA public key (from a JWKS entry). */
75struct pc_oidc_key
76{
77 uint8_t n[PC_OIDC_RSA_BYTES]; ///< Modulus, big-endian, right-aligned.
78 uint8_t e[4]; ///< Public exponent, big-endian (4 bytes).
79 bool loaded; ///< True once n/e are populated.
80};
81
82/** @brief Claims extracted from a verified token. */
83struct pc_oidc_claims
84{
85 char sub[PC_OIDC_SUB_LEN]; ///< Subject identifier.
86 char email[PC_OIDC_EMAIL_LEN]; ///< Email (empty if the claim is absent).
87 int64_t iat; ///< Issued-at (0 if absent). 64-bit: epoch seconds outlive 2038.
88 int64_t exp; ///< Expiry (epoch seconds). 64-bit.
89};
90
91/**
92 * @brief Read the `kid` from a token's JWT header.
93 * @return true if a `kid` string is present (copied, null-terminated, into @p out).
94 */
95bool pc_oidc_token_kid(const char *token, size_t token_len, char *kid_out, size_t kid_cap);
96
97/**
98 * @brief Extract an RSA JWK by @p kid from a JWKS JSON document.
99 *
100 * @param jwks_json the JWKS document (`{"keys":[ {RSA JWK}, ... ]}`).
101 * @param kid key id to match; nullptr / "" selects the first RSA key.
102 * @param key receives n/e on success.
103 * @return true if a matching RSA key was found and parsed.
104 */
105bool pc_oidc_jwks_find(const char *jwks_json, const char *kid, pc_oidc_key *key);
106
107/**
108 * @brief Verify an ID token against an already-resolved key.
109 *
110 * @param token,token_len the compact ID token.
111 * @param key the issuer's RSA public key.
112 * @param expected_iss required `iss` value (exact match).
113 * @param expected_aud required `aud` value (string match, or membership of
114 * the `aud` array).
115 * @param now_unix current time (epoch seconds) for exp/nbf checks.
116 * @param claims receives extracted claims on success (may be nullptr).
117 * @return ::PC_OIDC_OK or a negative ::pc_oidc_result.
118 */
119pc_oidc_result pc_oidc_verify_with_key(const char *token, size_t token_len, const pc_oidc_key *key,
120 const char *expected_iss, const char *expected_aud, uint32_t now_unix,
121 pc_oidc_claims *claims);
122
123/**
124 * @brief Verify an ID token, resolving the key from @p jwks_json by the token's kid.
125 *
126 * Convenience wrapper over pc_oidc_jwks_find() + pc_oidc_verify_with_key().
127 * @return ::PC_OIDC_OK or a negative ::pc_oidc_result (ERR_KEY if no key matches).
128 */
129pc_oidc_result pc_oidc_verify(const char *token, size_t token_len, const char *jwks_json, const char *expected_iss,
130 const char *expected_aud, uint32_t now_unix, pc_oidc_claims *claims);
131
132#endif // PC_ENABLE_OIDC
133#endif // PROTOCORE_OIDC_H
User-facing configuration for ProtoCore.