31const char *mem_find(
const char *hs,
const char *he,
const char *needle)
33 size_t nl = strnlen(needle, (
size_t)(he - hs) + 1);
36 if (nl == 0 || (
size_t)(he - hs) < nl)
40 for (
const char *p = hs; p + nl <= he; p++)
42 if (memcmp(p, needle, nl) == 0)
53bool find_field(
const char *s,
const char *e,
const char *name,
const char **vstart,
size_t *vlen,
char *type)
56 pc_sb sb_needle = {needle,
sizeof(needle), 0,
true};
63 if (nn <= 0 || nn >= (
int)
sizeof(needle))
67 const char *p = mem_find(s, e, needle);
73 while (p < e && (*p ==
' ' || *p ==
'\t' || *p ==
':' || *p ==
'\n' || *p ==
'\r'))
85 while (q < e && *q !=
'"')
87 if (*q ==
'\\' && q + 1 < e)
98 *vlen = (size_t)(q - p);
105 while (q < e && *q !=
']')
114 *vlen = (size_t)(q - p + 1);
118 if (*p ==
'-' || (*p >=
'0' && *p <=
'9'))
125 while (q < e && *q >=
'0' && *q <=
'9')
130 *vlen = (size_t)(q - p);
139bool get_str(
const char *s,
const char *e,
const char *name,
char *out,
size_t cap)
144 if (!find_field(s, e, name, &v, &vl, &t) || t !=
's')
159 if (ch ==
'\\' && i + 1 < vl)
176bool get_int64(
const char *s,
const char *e,
const char *name, int64_t *out)
183 if (!find_field(s, e, name, &v, &vl, &t) || t !=
'n' || vl == 0)
187 bool neg = (*v ==
'-');
188 size_t i = neg ? 1 : 0;
192 val = val * 10 + (v[i] -
'0');
194 *out = neg ? -val : val;
199bool aud_contains(
const char *s,
const char *e,
const char *want)
204 if (!find_field(s, e,
"aud", &v, &vl, &t))
208 size_t wl = strnlen(want, vl + 1);
211 return vl == wl && memcmp(v, want, wl) == 0;
216 const char *end = v + vl;
222 const char *q = (
const char *)memchr(p,
'"', (
size_t)(end - p));
223 const char *r = q ? (
const char *)memchr(q + 1,
'"', (
size_t)(end - (q + 1))) : nullptr;
229 if ((
size_t)(r - q) == wl && memcmp(q, want, wl) == 0)
241bool split3(
const char *tok,
size_t len,
const char **seg,
size_t *seglen)
243 const char *d1 = (
const char *)memchr(tok,
'.', len);
248 size_t rem = len - (size_t)(d1 + 1 - tok);
249 const char *d2 = (
const char *)memchr(d1 + 1,
'.', rem);
254 size_t rem2 = len - (size_t)(d2 + 1 - tok);
255 if (memchr(d2 + 1,
'.', rem2))
260 seglen[0] = (size_t)(d1 - tok);
262 seglen[1] = (size_t)(d2 - d1 - 1);
265 return seglen[0] && seglen[1] && seglen[2];
271bool right_align(
const uint8_t *src,
size_t len, uint8_t *dst,
size_t width)
275 if (len == width + 1 && src[0] == 0)
285 memset(dst, 0, width);
286 memcpy(dst + (width - len), src, len);
290bool parse_rsa_jwk(
const char *s,
const char *e, pc_oidc_key *key)
293 if (!get_str(s, e,
"n", b64,
sizeof(b64)))
297 uint8_t tmp[PC_OIDC_RSA_BYTES + 8];
299 if (nlen == 0 || !right_align(tmp, nlen, key->n, PC_OIDC_RSA_BYTES))
304 if (!get_str(s, e,
"e", b64,
sizeof(b64)))
310 if (elen == 0 || !right_align(e_tmp, elen, key->e, 4))
319bool pc_oidc_token_kid(
const char *token,
size_t token_len,
char *kid_out,
size_t kid_cap)
321 if (!token || !kid_out || kid_cap == 0)
327 if (!split3(token, token_len, seg, seglen))
338 return get_str((
const char *)hdr, (
const char *)hdr + hn,
"kid", kid_out, kid_cap);
341bool pc_oidc_jwks_find(
const char *jwks_json,
const char *kid, pc_oidc_key *key)
343 if (!jwks_json || !key)
347 const char *all_end = jwks_json + strnlen(jwks_json, PC_OIDC_JWKS_MAX);
348 const char *p = mem_find(jwks_json, all_end,
"\"keys\"");
349 p = p ? (
const char *)memchr(p,
'[', (
size_t)(all_end - p)) : nullptr;
358 const char *obj = (
const char *)memchr(p,
'{', (
size_t)(all_end - p));
363 const char *end = (
const char *)memchr(obj,
'}', (
size_t)(all_end - obj));
371 char this_kid[PC_OIDC_KID_LEN];
372 bool has_kid = get_str(obj, end,
"kid", this_kid,
sizeof(this_kid));
375 want = has_kid && strcmp(this_kid, kid) == 0;
382 if (want && parse_rsa_jwk(obj, end, key))
386 if (want && kid && *kid)
395pc_oidc_result pc_oidc_verify_with_key(
const char *token,
size_t token_len,
const pc_oidc_key *key,
396 const char *expected_iss,
const char *expected_aud, uint32_t now_unix,
397 pc_oidc_claims *claims)
399 if (!token || !key || !key->loaded || token_len == 0 || token_len >
PC_OIDC_MAX_LEN)
401 return pc_oidc_result::PC_OIDC_ERR_FORMAT;
406 if (!split3(token, token_len, seg, seglen))
408 return pc_oidc_result::PC_OIDC_ERR_FORMAT;
418 uint8_t *sig = (uint8_t *)
scratch_alloc(PC_OIDC_RSA_BYTES, 1);
421 if (!hdr || !sig || !pl || !iss)
423 return pc_oidc_result::PC_OIDC_ERR_FORMAT;
430 return pc_oidc_result::PC_OIDC_ERR_FORMAT;
434 if (!get_str((
const char *)hdr, (
const char *)hdr + hn,
"alg", alg,
sizeof(alg)) || strcmp(alg,
"RS256") != 0)
436 return pc_oidc_result::PC_OIDC_ERR_ALG;
442 return pc_oidc_result::PC_OIDC_ERR_FORMAT;
446 size_t signing_len = (size_t)(seg[1] + seglen[1] - token);
447 if (
pc_rsa_verify(key->n, key->e, (
const uint8_t *)token, signing_len, sig, PC_OIDC_RSA_BYTES,
450 return pc_oidc_result::PC_OIDC_ERR_SIGNATURE;
457 return pc_oidc_result::PC_OIDC_ERR_FORMAT;
460 const char *ps = (
const char *)pl;
461 const char *pe = ps + pn;
463 if (expected_iss && *expected_iss)
465 if (!get_str(ps, pe,
"iss", iss, PC_OIDC_ISS_LEN) || strcmp(iss, expected_iss) != 0)
467 return pc_oidc_result::PC_OIDC_ERR_ISS;
470 if (expected_aud && *expected_aud)
472 if (!aud_contains(ps, pe, expected_aud))
474 return pc_oidc_result::PC_OIDC_ERR_AUD;
479 if (!get_int64(ps, pe,
"exp", &exp) || (int64_t)now_unix >= exp)
481 return pc_oidc_result::PC_OIDC_ERR_EXPIRED;
484 if (get_int64(ps, pe,
"nbf", &nbf) && (int64_t)now_unix < nbf)
486 return pc_oidc_result::PC_OIDC_ERR_NOT_YET;
491 claims->sub[0] =
'\0';
492 claims->email[0] =
'\0';
495 get_str(ps, pe,
"sub", claims->sub,
sizeof(claims->sub));
496 get_str(ps, pe,
"email", claims->email,
sizeof(claims->email));
497 get_int64(ps, pe,
"iat", &claims->iat);
499 return pc_oidc_result::PC_OIDC_OK;
502pc_oidc_result pc_oidc_verify(
const char *token,
size_t token_len,
const char *jwks_json,
const char *expected_iss,
503 const char *expected_aud, uint32_t now_unix, pc_oidc_claims *claims)
505 char kid[PC_OIDC_KID_LEN];
506 if (!pc_oidc_token_kid(token, token_len, kid,
sizeof(kid)))
512 if (!pc_oidc_jwks_find(jwks_json, kid[0] ? kid : nullptr, &key))
514 return pc_oidc_result::PC_OIDC_ERR_KEY;
516 return pc_oidc_verify_with_key(token, token_len, &key, expected_iss, expected_aud, now_unix, claims);
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).
#define PC_SCRATCH_ARENA_SIZE
RAII scope guard for transient scratch borrows.
OpenID Connect ID-token verification, RS256 (PC_ENABLE_OIDC).
#define PC_OIDC_MAX_LEN
Max accepted OIDC ID-token length (also sizes the Authorization buffer).
int pc_rsa_verify(const uint8_t n_be[PC_RSA_KEY_BYTES], const uint8_t e_be4[4], const uint8_t *msg, size_t msg_len, const uint8_t *sig, size_t sig_len, pc_rsa_hash hash)
Verify an RSA-2048 PKCS#1 v1.5 signature over msg.
RSA-2048 PKCS#1 v1.5 signature primitive (RFC 8017) - verify + software sign.
@ SHA256
RSASSA-PKCS1-v1.5 with SHA-256.
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
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.
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.
Bump-append target; ok latches false once an append would overflow cap.