| /* Copyright (C) 2015-2026 maClara, LLC <info@maclara-llc.com> |
| This file is part of the JWT C Library |
| |
| SPDX-License-Identifier: MPL-2.0 |
| This Source Code Form is subject to the terms of the Mozilla Public |
| License, v. 2.0. If a copy of the MPL was not distributed with this |
| file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
| #include <stdlib.h> |
| #include <string.h> |
| #include <stdio.h> |
| |
| #include <jwt.h> |
| |
| #include "jwt-private.h" |
| |
| static jwt_json_t *jwt_base64uri_decode_to_json(char *src) |
| { |
| jwt_json_t *js; |
| char *buf; |
| int len; |
| |
| buf = jwt_base64uri_decode(src, &len); |
| |
| if (buf == NULL) |
| return NULL; // LCOV_EXCL_LINE |
| |
| buf[len] = '\0'; |
| |
| /* @rfc{8725,2.4} Reject duplicate members in the token header/payload so |
| * a peer that selects a different occurrence cannot be made to disagree |
| * with us about a claim/header. Supported by the Jansson backend; json-c |
| * cannot reject duplicates (it keeps the last), a documented limitation. */ |
| js = jwt_json_parse(buf, JWT_JSON_REJECT_DUPLICATES, NULL); |
| |
| jwt_freemem(buf); |
| |
| return js; |
| } |
| |
| static int jwt_parse_payload(jwt_t *jwt, char *payload) |
| { |
| if (jwt->claims) |
| jwt_json_releasep(&(jwt->claims)); |
| |
| jwt->claims = jwt_base64uri_decode_to_json(payload); |
| if (!jwt->claims) { |
| jwt_write_error(jwt, "Error parsing payload"); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static int jwt_parse_head(jwt_t *jwt, char *head) |
| { |
| jwt_json_t *jalg; |
| |
| if (jwt->headers) |
| jwt_json_releasep(&(jwt->headers)); |
| |
| jwt->headers = jwt_base64uri_decode_to_json(head); |
| if (!jwt->headers) { |
| jwt_write_error(jwt, "Error parsing header"); |
| return 1; |
| } |
| |
| jwt->alg = JWT_ALG_NONE; |
| |
| jalg = jwt_json_obj_get(jwt->headers, "alg"); |
| if (jalg && jwt_json_is_string(jalg)) { |
| const char *alg = jwt_json_str_val(jalg); |
| |
| jwt->alg = jwt_str_alg(alg); |
| |
| if (jwt->alg >= JWT_ALG_INVAL) { |
| jwt_write_error(jwt, "Invalid ALG: [%s]", alg); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| jwt_write_error(jwt, "Missing or invalid \"alg\" header"); |
| |
| return 1; |
| } |
| |
| /* @rfc{7515,4.1.11} Process the "crit" (Critical) header parameter. |
| * |
| * If present, "crit" must be a non-empty array of strings. Each string |
| * names a header parameter that must (a) actually be present in the header |
| * and (b) be understood by the recipient. LibJWT understands no extension |
| * header parameters on its own, so the application must declare the ones it |
| * handles via jwt_checker_understands() (passed in as @understood, a |
| * NULL-terminated list which may itself be NULL). Any listed parameter not |
| * in that list makes the JWS invalid. |
| */ |
| int jwt_check_crit(jwt_t *jwt, char * const *understood) |
| { |
| jwt_json_t *crit, *ent; |
| size_t i; |
| |
| crit = jwt_json_obj_get(jwt->headers, "crit"); |
| if (crit == NULL) |
| return 0; |
| |
| if (!jwt_json_is_array(crit)) { |
| jwt_write_error(jwt, "\"crit\" header must be an array"); |
| return 1; |
| } |
| |
| if (jwt_json_arr_size(crit) == 0) { |
| jwt_write_error(jwt, "\"crit\" header must not be empty"); |
| return 1; |
| } |
| |
| jwt_json_arr_foreach(crit, i, ent) { |
| const char *name; |
| int found = 0; |
| |
| if (!jwt_json_is_string(ent)) { |
| jwt_write_error(jwt, |
| "\"crit\" header entries must be strings"); |
| return 1; |
| } |
| |
| name = jwt_json_str_val(ent); |
| |
| /* Must actually appear in the header. */ |
| if (jwt_json_obj_get(jwt->headers, name) == NULL) { |
| jwt_write_error(jwt, |
| "\"crit\" lists \"%s\" which is not in the header", |
| name); |
| return 1; |
| } |
| |
| /* @rfc{7797} The library handles "b64" itself, so it is always |
| * understood (and is enforced separately during parsing). */ |
| if (!strcmp(name, "b64")) |
| found = 1; |
| |
| /* Must be understood by the application. */ |
| if (!found && understood) { |
| size_t j; |
| |
| for (j = 0; understood[j] != NULL; j++) { |
| if (!strcmp(understood[j], name)) { |
| found = 1; |
| break; |
| } |
| } |
| } |
| |
| if (!found) { |
| jwt_write_error(jwt, |
| "Unsupported critical header: \"%s\"", name); |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* @rfc{7797,6} Read the "b64" header (default true). A false value is the |
| * RFC 7797 unencoded payload option and is ONLY accepted if "b64" is also |
| * present in "crit" (else an attacker could flip the signing-input meaning). |
| * Sets *b64; returns 1 on the crit violation or a non-boolean "b64". */ |
| static int jwt_payload_b64(jwt_t *jwt, int *b64) |
| { |
| jwt_json_t *jb64, *crit, *ent; |
| size_t i; |
| int found = 0; |
| |
| *b64 = 1; |
| |
| jb64 = jwt_json_obj_get(jwt->headers, "b64"); |
| if (jb64 == NULL) |
| return 0; |
| |
| if (!jwt_json_is_bool(jb64)) { |
| jwt_write_error(jwt, "\"b64\" header must be a boolean"); |
| return 1; |
| } |
| |
| if (jwt_json_is_true(jb64)) |
| return 0; |
| |
| *b64 = 0; |
| |
| crit = jwt_json_obj_get(jwt->headers, "crit"); |
| if (crit != NULL && jwt_json_is_array(crit)) { |
| jwt_json_arr_foreach(crit, i, ent) { |
| if (jwt_json_is_string(ent) && |
| !strcmp(jwt_json_str_val(ent), "b64")) { |
| found = 1; |
| break; |
| } |
| } |
| } |
| if (!found) { |
| jwt_write_error(jwt, |
| "\"b64\":false requires \"b64\" in \"crit\" (RFC 7797)"); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| int jwt_parse(jwt_t *jwt, const char *token, unsigned int *len) |
| { |
| char_auto *buf = NULL; |
| char *payload, *sig, *dot, *lastdot = NULL; |
| int buf_len = strlen(token) + 1; |
| int b64; |
| |
| buf = jwt_malloc(buf_len); |
| if (!buf) { |
| // LCOV_EXCL_START |
| jwt_write_error(jwt, "Error allocating memory"); |
| return 1; |
| // LCOV_EXCL_STOP |
| } |
| |
| /* buf_len includes nil */ |
| memcpy(buf, token, buf_len); |
| |
| /* Header: everything up to the first '.'. */ |
| for (payload = buf; payload[0] != '.'; payload++) { |
| if (payload[0] == '\0') { |
| jwt_write_error(jwt, |
| "No dot found looking for end of header"); |
| return 1; |
| } |
| } |
| payload[0] = '\0'; |
| payload++; |
| |
| /* Parse the header now so we know the "b64" setting. */ |
| if (jwt_parse_head(jwt, buf)) |
| return 1; |
| |
| /* @rfc{7797} Determine the payload encoding (and enforce crit). */ |
| if (jwt_payload_b64(jwt, &b64)) |
| return 1; |
| jwt->b64 = b64; |
| |
| if (b64) { |
| /* Standard: the payload is between the 1st and 2nd '.'. */ |
| for (sig = payload; sig[0] != '.'; sig++) { |
| if (sig[0] == '\0') { |
| jwt_write_error(jwt, |
| "No dot found looking for end of payload"); |
| return 1; |
| } |
| } |
| sig[0] = '\0'; |
| sig++; /* point past the dot, as the b64=false branch does */ |
| |
| if (jwt_parse_payload(jwt, payload)) |
| return 1; |
| } else { |
| /* @rfc{7797,5.2} Unencoded: the signature is after the LAST '.' |
| * and the raw payload (which may itself contain '.') is between |
| * the 1st and last '.'. The raw payload is not JSON claims. */ |
| for (dot = payload; dot[0] != '\0'; dot++) { |
| if (dot[0] == '.') |
| lastdot = dot; |
| } |
| if (lastdot == NULL) { |
| jwt_write_error(jwt, |
| "No dot found looking for signature"); |
| return 1; |
| } |
| *lastdot = '\0'; |
| sig = lastdot + 1; |
| } |
| |
| /* @rfc{7515,5.1}/@rfc{7797,3} The signing input is buf[0 .. sig's dot], |
| * i.e. BASE64URL(header) "." (base64url or raw) payload. */ |
| *len = (sig - 1) - buf; |
| |
| return 0; |
| } |
| |
| /* @rfc{7519,4.1.3} "aud" may be a single string OR an array of strings. Return |
| * 1 if the expected audience is among the array elements, 0 otherwise. */ |
| static int __aud_matches_array(jwt_t *jwt, const char *want) |
| { |
| jwt_json_t *aud, *ent; |
| size_t i, n; |
| |
| aud = jwt_json_obj_get(jwt->claims, "aud"); |
| if (aud == NULL || !jwt_json_is_array(aud)) |
| return 0; |
| |
| n = jwt_json_arr_size(aud); |
| for (i = 0; i < n; i++) { |
| ent = jwt_json_arr_get(aud, i); |
| if (ent && jwt_json_is_string(ent) && |
| !strcmp(want, jwt_json_str_val(ent))) |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static int __check_str_claim(jwt_t *jwt, jwt_claims_t claim, char *claim_str) |
| { |
| jwt_checker_t *checker = jwt->checker; |
| jwt_value_t jval; |
| const char *str; |
| jwt_value_error_t err; |
| |
| if (!(checker->c.claims & claim)) |
| return 0; |
| |
| str = jwt_checker_claim_get(checker, claim); |
| if (str == NULL) |
| return 1; // LCOV_EXCL_LINE |
| // Check above makes this nearly impossible to hit |
| |
| jwt_set_GET_STR(&jval, claim_str); |
| err = jwt_claim_get(jwt, &jval); |
| |
| if (err == JWT_VALUE_ERR_NONE) |
| return strcmp(str, jval.str_val) ? 1 : 0; |
| |
| /* A type error on "aud" means it is an array (RFC 7519 4.1.3 allows |
| * that): accept the token if the expected audience is among the |
| * elements. Any other claim, or a non-matching array, fails. */ |
| if (claim == JWT_CLAIM_AUD && err == JWT_VALUE_ERR_TYPE && |
| __aud_matches_array(jwt, str)) |
| return 0; |
| |
| return 1; |
| } |
| |
| static jwt_claims_t __verify_claims(jwt_t *jwt) |
| { |
| jwt_checker_t *checker = jwt->checker; |
| jwt_value_t jval; |
| time_t now = time(NULL); |
| jwt_value_error_t err; |
| jwt_claims_t failed = 0; |
| |
| /* expiration in past */ |
| if (checker->c.claims & JWT_CLAIM_EXP) { |
| jwt_set_GET_INT(&jval, "exp"); |
| err = jwt_claim_get(jwt, &jval); |
| |
| if (err == JWT_VALUE_ERR_NONE) { |
| if (jval.int_val <= (now - checker->c.exp)) { |
| failed |= JWT_CLAIM_EXP; |
| } |
| } else if (err != JWT_VALUE_ERR_NOEXIST) |
| failed |= JWT_CLAIM_EXP; // LCOV_EXCL_LINE |
| } |
| |
| /* not valid before now */ |
| if (checker->c.claims & JWT_CLAIM_NBF) { |
| jwt_set_GET_INT(&jval, "nbf"); |
| err = jwt_claim_get(jwt, &jval); |
| |
| if (err == JWT_VALUE_ERR_NONE) { |
| if (jval.int_val > (now + checker->c.nbf)) { |
| failed |= JWT_CLAIM_NBF; |
| } |
| } else if (err != JWT_VALUE_ERR_NOEXIST) |
| failed |= JWT_CLAIM_NBF; // LCOV_EXCL_LINE |
| } |
| |
| /* issuer doesn't match */ |
| if (__check_str_claim(jwt, JWT_CLAIM_ISS, "iss")) |
| failed |= JWT_CLAIM_ISS; |
| |
| /* subject doesn't match */ |
| if (__check_str_claim(jwt, JWT_CLAIM_SUB, "sub")) |
| failed |= JWT_CLAIM_SUB; |
| |
| /* audience doesn't match */ |
| if (__check_str_claim(jwt, JWT_CLAIM_AUD, "aud")) |
| failed |= JWT_CLAIM_AUD; |
| |
| return failed; |
| } |
| |
| /* @rfc{7519,4.1.7} jti: hand the id to the application callback, which |
| * validates/consumes it (e.g. replay protection). A registered callback means |
| * a token with no jti is rejected. |
| * |
| * This is intentionally kept separate from __verify_claims() and run only |
| * after the signature has verified: the callback typically mutates external |
| * state (records/burns the jti for replay protection), so it must not fire on |
| * a token whose signature has not been validated. Running it pre-signature |
| * would let an attacker who knows a victim's jti poison the replay cache or |
| * burn the id with forged, unauthenticated tokens. */ |
| static jwt_claims_t __verify_jti(jwt_t *jwt) |
| { |
| jwt_checker_t *checker = jwt->checker; |
| jwt_value_t jval; |
| jwt_value_error_t err; |
| |
| if (!checker->c.jti_check) |
| return 0; |
| |
| JWT_CONFIG_DECLARE(jti_config); |
| |
| jwt_set_GET_STR(&jval, "jti"); |
| err = jwt_claim_get(jwt, &jval); |
| |
| if (err != JWT_VALUE_ERR_NONE) |
| return JWT_CLAIM_JTI; |
| |
| jti_config.ctx = checker->c.jti_ctx; |
| if (checker->c.jti_check(jwt, &jti_config, jval.str_val)) |
| return JWT_CLAIM_JTI; |
| |
| return 0; |
| } |
| |
| /* This is after parsing and possibly a user callback. */ |
| static int __verify_config_post(jwt_t *jwt, const jwt_config_t *config, |
| unsigned int sig_len) |
| { |
| /* Yes, we do this before checking a signature. @rfc{7797} An unencoded |
| * (b64=false) payload is opaque, not JSON claims, so skip claim checks. */ |
| if (jwt->b64 && __verify_claims(jwt)) { |
| /* TODO Pass back the ORd list of claims failed. */ |
| jwt_write_error(jwt, "Failed one or more claims"); |
| return 1; |
| } |
| |
| if (!sig_len) { |
| if (config->key || config->alg != JWT_ALG_NONE || |
| jwt->alg != JWT_ALG_NONE) { |
| jwt_write_error(jwt, |
| "Expected a signature, but JWT has none"); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| /* Signature is known to be present from this point */ |
| if (jwt->alg == JWT_ALG_NONE) { |
| jwt_write_error(jwt, "JWT has signature block, but no alg set"); |
| return 1; |
| } |
| |
| if (config->key == NULL) { |
| jwt_write_error(jwt, |
| "JWT has signature, but no key was given"); |
| return 1; |
| } |
| |
| /* Key is known to be given at this point */ |
| if (config->alg == JWT_ALG_NONE) { |
| if (config->key->alg != jwt->alg) { |
| jwt_write_error(jwt, "Key alg does not match JWT"); |
| return 1; |
| } |
| } else if (config->key->alg == JWT_ALG_NONE) { |
| if (config->alg != jwt->alg) { |
| jwt_write_error(jwt, "Config alg does not match JWT"); |
| return 1; |
| } |
| } else if (config->alg != config->key->alg) { |
| /* It's not really possible to get here due to checks in setkey */ |
| // LCOV_EXCL_START |
| jwt_write_error(jwt, "Config and key alg does not match"); |
| return 1; |
| // LCOV_EXCL_STOP |
| } |
| |
| /* Algorithm is now bound (jwt->alg). Defensively confirm that the |
| * JWK's actual key type can carry it. This blocks algorithm |
| * confusion (GHSA-q843-6q5f-w55g) even if a malformed JWK has an |
| * "alg" hint that disagrees with its "kty". */ |
| if (jwt_alg_required_kty(jwt->alg) != config->key->kty) { |
| jwt_write_error(jwt, "Key type does not match JWT alg"); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| jwt_t *jwt_verify_complete(jwt_t *jwt, const jwt_config_t *config, |
| const char *token, unsigned int payload_len) |
| { |
| const char *sig; |
| unsigned int sig_len; |
| |
| sig = token + (payload_len + 1); |
| sig_len = strlen(sig); |
| |
| /* Check for conflicts in user request and JWT, and run the read-only |
| * claim checks (exp/nbf/iss/sub/aud). */ |
| if (__verify_config_post(jwt, config, sig_len)) |
| return jwt; |
| |
| /* After all the checks, if we don't have a sig, we can move on. */ |
| if (sig_len) { |
| /* At this point, config is never NULL */ |
| jwt->key = config->key; |
| |
| jwt = jwt_verify_sig(jwt, token, payload_len, sig); |
| if (jwt->error) |
| return jwt; |
| } |
| |
| /* Signature has now verified (or there is none and "none" was |
| * permitted). Only now run the jti replay callback, which may mutate |
| * external state and must not fire on an unauthenticated token. */ |
| if (__verify_jti(jwt)) |
| jwt_write_error(jwt, "Failed one or more claims"); |
| |
| return jwt; |
| } |
| |
| /* =================================================================== |
| * @rfc{7515,7.2} JWS JSON Serialization (multi-signature) verification |
| * =================================================================== */ |
| |
| static char *jwt_str_dup(const char *s) |
| { |
| size_t n; |
| char *d; |
| |
| if (s == NULL) |
| return NULL; // LCOV_EXCL_LINE |
| |
| n = strlen(s) + 1; |
| d = jwt_malloc(n); |
| if (d != NULL) |
| memcpy(d, s, n); |
| |
| return d; |
| } |
| |
| static const char *json_str(const jwt_json_t *obj, const char *key) |
| { |
| jwt_json_t *v = jwt_json_obj_get(obj, key); |
| |
| return (v && jwt_json_is_string(v)) ? jwt_json_str_val(v) : NULL; |
| } |
| |
| /* Parse one signature entry (a General-form array element, or the Flattened |
| * top-level object) into a new jwt_signature appended to the checker's list: |
| * its verbatim base64url "protected" and "signature", the decoded protected |
| * header, its algorithm, and the optional unprotected "header". */ |
| static int build_sig_entry(jwt_checker_t *checker, jwt_json_t *entry) |
| { |
| jwt_json_t *prot_obj, *hdr_j, *hdr_clone = NULL; |
| const char *prot_b64, *sig_b64, *alg_str; |
| struct jwt_signature *s; |
| jwt_alg_t alg; |
| |
| prot_b64 = json_str(entry, "protected"); |
| sig_b64 = json_str(entry, "signature"); |
| if (prot_b64 == NULL || sig_b64 == NULL) { |
| jwt_write_error(checker, |
| "JWS signature entry missing \"protected\"/\"signature\""); |
| return 1; |
| } |
| |
| prot_obj = jwt_base64uri_decode_to_json((char *)prot_b64); |
| if (prot_obj == NULL || !jwt_json_is_object(prot_obj)) { |
| jwt_json_release(prot_obj); |
| jwt_write_error(checker, "JWS protected header is not valid JSON"); |
| return 1; |
| } |
| |
| /* @rfc{8725,3.1} Bind the algorithm to this entry's protected header. |
| * "none" is never accepted in a JSON-serialized JWS. */ |
| alg_str = json_str(prot_obj, "alg"); |
| alg = alg_str ? jwt_str_alg(alg_str) : JWT_ALG_INVAL; |
| if (alg == JWT_ALG_NONE || alg >= JWT_ALG_INVAL) { |
| jwt_json_release(prot_obj); |
| jwt_write_error(checker, "JWS signature has an invalid \"alg\""); |
| return 1; |
| } |
| |
| hdr_j = jwt_json_obj_get(entry, "header"); |
| if (hdr_j != NULL) { |
| if (!jwt_json_is_object(hdr_j) || |
| jwt_header_params_overlap(prot_obj, hdr_j)) { |
| jwt_json_release(prot_obj); |
| jwt_write_error(checker, |
| "JWS unprotected header is invalid or overlaps"); |
| return 1; |
| } |
| hdr_clone = jwt_json_clone(hdr_j); |
| if (hdr_clone == NULL) { |
| // LCOV_EXCL_START |
| jwt_json_release(prot_obj); |
| jwt_write_error(checker, "Error allocating memory"); |
| return 1; |
| // LCOV_EXCL_STOP |
| } |
| } |
| |
| s = jwt_signature_append(&checker->c); |
| if (s == NULL) { |
| // LCOV_EXCL_START |
| jwt_json_release(prot_obj); |
| jwt_json_release(hdr_clone); |
| jwt_write_error(checker, "Error allocating memory"); |
| return 1; |
| // LCOV_EXCL_STOP |
| } |
| |
| s->alg = alg; |
| s->protected = prot_obj; |
| s->header = hdr_clone; |
| s->protected_b64 = jwt_str_dup(prot_b64); |
| s->sig_b64 = jwt_str_dup(sig_b64); |
| if (s->protected_b64 == NULL || s->sig_b64 == NULL) { |
| jwt_write_error(checker, "Error allocating memory"); // LCOV_EXCL_LINE |
| return 1; // LCOV_EXCL_LINE |
| } |
| |
| return 0; |
| } |
| |
| /* Try one candidate key against signature @s over @input. Applies the |
| * algorithm/key-type anti-confusion gate (GHSA-q843-6q5f-w55g) before any |
| * verify, and routes the crypto to the key's origin backend via jwt_item_ops() |
| * inside jwt_verify_sig(). Sets s->verified/s->key on success. */ |
| static void try_candidate(jwt_t *jwt, struct jwt_signature *s, |
| const jwk_item_t *key, const char *input, |
| unsigned int input_len) |
| { |
| if (key == NULL || s->verified) |
| return; |
| |
| if (jwt_alg_required_kty(s->alg) != jwks_item_kty(key)) |
| return; |
| |
| jwt->alg = s->alg; |
| jwt->key = key; |
| jwt->error = 0; |
| jwt->error_msg[0] = '\0'; |
| |
| jwt_verify_sig(jwt, input, input_len, s->sig_b64); |
| |
| if (!jwt->error) { |
| s->verified = 1; |
| s->key = key; |
| } |
| |
| /* Don't let a failed attempt's error leak into the checker result. */ |
| jwt->error = 0; |
| jwt->error_msg[0] = '\0'; |
| } |
| |
| /* Verify one signature entry: run the optional per-signature callback, select |
| * the key (the checker's single key, a "kid"-named keyring key, or every |
| * compatible keyring key), and verify. */ |
| static int verify_entry(jwt_checker_t *checker, jwt_t *jwt, |
| struct jwt_signature *s, const char *payload_b64, |
| int payload_len) |
| { |
| JWT_CONFIG_DECLARE(config); |
| const jwk_set_t *ring = checker->c.keyring; |
| char_auto *input = NULL; |
| const char *kid; |
| int prot_len, scan; |
| |
| /* @rfc{7515,5.1} Signing input is the VERBATIM protected_b64 "." payload_b64. */ |
| prot_len = (int)strlen(s->protected_b64); |
| input = jwt_malloc((size_t)prot_len + payload_len + 2); |
| if (input == NULL) |
| return 1; // LCOV_EXCL_LINE |
| sprintf(input, "%s.%s", s->protected_b64, payload_b64); |
| |
| /* This signature's header drives crit/callback. */ |
| jwt->headers = s->protected; |
| jwt->alg = s->alg; |
| |
| if (jwt_check_crit(jwt, checker->c.understood)) { |
| jwt_copy_error(checker, jwt); |
| jwt->headers = NULL; |
| return 1; |
| } |
| |
| /* @rfc{7797,6} Enforce "b64" in "crit" if this signature is unencoded. */ |
| { |
| int eb64; |
| |
| if (jwt_payload_b64(jwt, &eb64)) { |
| jwt_copy_error(checker, jwt); |
| jwt->headers = NULL; |
| return 1; |
| } |
| } |
| |
| /* Seed the candidate: a kid-named keyring key (a binding assertion, no |
| * fallback), or the checker's single key. A keyless keyring entry scans. */ |
| kid = json_str(s->protected, "kid"); |
| if (ring != NULL) { |
| config.key = (kid != NULL) |
| ? jwks_find_bykid((jwk_set_t *)ring, kid) : NULL; |
| scan = (kid == NULL); |
| } else { |
| config.key = checker->c.key; |
| scan = 0; |
| } |
| config.alg = s->alg; |
| config.ctx = checker->c.cb_ctx; |
| |
| if (checker->c.cb && checker->c.cb(jwt, &config)) { |
| /* Callback rejected this signature; not a hard error under ANY. */ |
| jwt->headers = NULL; |
| return 0; |
| } |
| |
| if (config.key != NULL) { |
| /* Explicit key (kid match, single key, or callback override). */ |
| try_candidate(jwt, s, config.key, input, (unsigned int)strlen(input)); |
| } else if (scan) { |
| size_t i, n = jwks_item_count(ring); |
| |
| for (i = 0; i < n && !s->verified; i++) { |
| const jwk_item_t *k = jwks_item_get(ring, i); |
| jwt_alg_t kalg = jwks_item_alg(k); |
| |
| if (kalg != JWT_ALG_NONE && kalg != s->alg) |
| continue; |
| try_candidate(jwt, s, k, input, (unsigned int)strlen(input)); |
| } |
| } |
| |
| jwt->headers = NULL; |
| |
| return 0; |
| } |
| |
| int jwt_verify_json(jwt_checker_t *checker, const char *token) |
| { |
| jwt_json_auto_t *root = NULL; |
| jwt_json_t *payload_j, *sigs; |
| jwt_auto_t *jwt = NULL; |
| const char *payload_b64; |
| struct jwt_signature *s; |
| int n_verified = 0, sig_ok, payload_len, pb64 = 1; |
| size_t n_entries; |
| |
| /* Reset for a reused checker. */ |
| checker->error = 0; |
| checker->error_msg[0] = '\0'; |
| checker->c.last_sig_count = 0; |
| |
| root = jwt_json_parse(token, JWT_JSON_REJECT_DUPLICATES, NULL); |
| if (root == NULL || !jwt_json_is_object(root)) { |
| jwt_write_error(checker, "Invalid JWS JSON Serialization"); |
| return 1; |
| } |
| |
| payload_j = jwt_json_obj_get(root, "payload"); |
| if (payload_j == NULL || !jwt_json_is_string(payload_j)) { |
| jwt_write_error(checker, |
| "JWS JSON Serialization missing a \"payload\""); |
| return 1; |
| } |
| payload_b64 = jwt_json_str_val(payload_j); |
| payload_len = (int)strlen(payload_b64); |
| |
| /* @rfc{7515,7.2} General has a "signatures" array; Flattened hoists a |
| * single signature's members to the top level. They are exclusive. */ |
| sigs = jwt_json_obj_get(root, "signatures"); |
| if (sigs != NULL) { |
| size_t i, cnt; |
| |
| if (jwt_json_obj_get(root, "protected") || |
| jwt_json_obj_get(root, "signature") || |
| jwt_json_obj_get(root, "header")) { |
| jwt_write_error(checker, |
| "JWS JSON mixes General and Flattened members"); |
| return 1; |
| } |
| if (!jwt_json_is_array(sigs)) { |
| jwt_write_error(checker, |
| "\"signatures\" must be an array"); |
| return 1; |
| } |
| cnt = jwt_json_arr_size(sigs); |
| if (cnt == 0) { |
| jwt_write_error(checker, |
| "\"signatures\" must not be empty"); |
| return 1; |
| } |
| for (i = 0; i < cnt; i++) { |
| if (build_sig_entry(checker, jwt_json_arr_get(sigs, i))) |
| return 1; |
| } |
| } else if (build_sig_entry(checker, root)) { |
| return 1; |
| } |
| |
| n_entries = checker->c.n_signatures; |
| checker->c.last_sig_count = (unsigned int)n_entries; |
| |
| if (checker->c.key == NULL && checker->c.keyring == NULL) { |
| jwt_write_error(checker, "No key or keyring set"); |
| return 1; |
| } |
| |
| /* @rfc{7797} The shared payload's encoding comes from the (shared) |
| * signatures; read it from the first one. An unencoded payload is opaque |
| * (not JSON claims), so it is neither base64-decoded nor claim-checked. */ |
| { |
| struct jwt_signature *first = jwt_signature_first(&checker->c); |
| jwt_json_t *jb = first ? |
| jwt_json_obj_get(first->protected, "b64") : NULL; |
| |
| if (jb != NULL && jwt_json_is_bool(jb) && !jwt_json_is_true(jb)) |
| pb64 = 0; |
| } |
| |
| /* A transient JWT carrying the shared payload for the per-signature |
| * verify and the read-only claim checks. */ |
| jwt = jwt_new(); |
| if (jwt == NULL) { |
| jwt_write_error(checker, "Error allocating memory"); // LCOV_EXCL_LINE |
| return 1; // LCOV_EXCL_LINE |
| } |
| jwt->checker = checker; |
| jwt->b64 = pb64; |
| jwt_json_releasep(&jwt->claims); |
| /* Release the empty header object jwt_new() allocated: from here on |
| * jwt->headers only ever borrows each signature's protected header. */ |
| jwt_json_releasep(&jwt->headers); |
| if (pb64) { |
| jwt->claims = jwt_base64uri_decode_to_json((char *)payload_b64); |
| if (jwt->claims == NULL) { |
| jwt_write_error(checker, "Error parsing payload"); |
| return 1; |
| } |
| } else { |
| jwt->claims = jwt_json_create(); |
| if (jwt->claims == NULL) |
| return 1; // LCOV_EXCL_LINE |
| } |
| |
| list_for_each_entry(s, &checker->c.signatures, node) { |
| if (verify_entry(checker, jwt, s, payload_b64, payload_len)) { |
| jwt->headers = NULL; |
| return 1; |
| } |
| if (s->verified) |
| n_verified++; |
| } |
| jwt->headers = NULL; |
| |
| /* @rfc{7515,7.2} Policy: ANY accepts on the first verified signature; |
| * ALL requires every signature in the token to verify. */ |
| if (checker->c.policy == JWT_VERIFY_POLICY_ALL) |
| sig_ok = (n_verified == (int)n_entries); |
| else |
| sig_ok = (n_verified >= 1); |
| |
| if (!sig_ok) { |
| jwt_write_error(checker, |
| "Signature policy not met (%d of %zu verified)", |
| n_verified, n_entries); |
| } else if (jwt->b64 && __verify_claims(jwt)) { |
| jwt_write_error(checker, "Failed one or more claims"); |
| } else if (__verify_jti(jwt)) { |
| /* jti runs only after signature + claims succeed. */ |
| jwt_write_error(checker, "Failed one or more claims"); |
| } |
| |
| return checker->error; |
| } |
| |
| /* Read the "b64" flag from a base64url-encoded protected header. Default 1. */ |
| static int detached_b64(const char *prot_b64) |
| { |
| jwt_json_auto_t *hdr = NULL; |
| char_auto *copy = NULL; |
| jwt_json_t *jb; |
| |
| if (prot_b64 == NULL) |
| return 1; |
| copy = jwt_str_dup(prot_b64); |
| if (copy == NULL) |
| return 1; // LCOV_EXCL_LINE |
| hdr = jwt_base64uri_decode_to_json(copy); |
| if (hdr == NULL) |
| return 1; |
| jb = jwt_json_obj_get(hdr, "b64"); |
| if (jb != NULL && jwt_json_is_bool(jb) && !jwt_json_is_true(jb)) |
| return 0; |
| |
| return 1; |
| } |
| |
| /* @rfc{7797,3} The payload as it appears in the signing input: base64url(@payload) |
| * when b64, else the raw bytes. Returns a malloc'd NUL-terminated string. */ |
| static char *detached_payload_part(int b64, const unsigned char *payload, |
| size_t len) |
| { |
| char *pp = NULL; |
| |
| if (b64) { |
| if (len == 0) |
| return jwt_str_dup(""); |
| if (len > INT_MAX || |
| jwt_base64uri_encode(&pp, (const char *)payload, (int)len) <= 0) |
| return NULL; // LCOV_EXCL_LINE |
| return pp; |
| } |
| |
| pp = jwt_malloc(len + 1); |
| if (pp == NULL) |
| return NULL; // LCOV_EXCL_LINE |
| if (len) |
| memcpy(pp, payload, len); |
| pp[len] = '\0'; |
| |
| return pp; |
| } |
| |
| int jwt_checker_verify_detached(jwt_checker_t *checker, const char *token, |
| const unsigned char *payload, size_t len) |
| { |
| char_auto *pp = NULL, *recon = NULL; |
| const char *p; |
| int b64, ret; |
| |
| if (checker == NULL) |
| return 1; |
| |
| if (token == NULL || !strlen(token)) { |
| jwt_write_error(checker, "Must pass a token"); |
| return 1; |
| } |
| if (payload == NULL && len > 0) { |
| jwt_write_error(checker, "Must pass a payload"); |
| return 1; |
| } |
| |
| p = token; |
| while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') |
| p++; |
| |
| if (*p == '{') { |
| /* @rfc{7515,7.2} JSON: insert the out-of-band "payload" and verify |
| * the now-attached token. */ |
| jwt_json_auto_t *root = NULL; |
| jwt_json_t *sigs, *prot_j, *v; |
| const char *prot_b64; |
| |
| root = jwt_json_parse(token, JWT_JSON_REJECT_DUPLICATES, NULL); |
| if (root == NULL || !jwt_json_is_object(root)) { |
| jwt_write_error(checker, "Invalid JWS JSON Serialization"); |
| return 1; |
| } |
| |
| sigs = jwt_json_obj_get(root, "signatures"); |
| if (sigs != NULL && jwt_json_is_array(sigs) && |
| jwt_json_arr_size(sigs) > 0) |
| prot_j = jwt_json_obj_get(jwt_json_arr_get(sigs, 0), |
| "protected"); |
| else |
| prot_j = jwt_json_obj_get(root, "protected"); |
| prot_b64 = (prot_j && jwt_json_is_string(prot_j)) |
| ? jwt_json_str_val(prot_j) : NULL; |
| |
| b64 = detached_b64(prot_b64); |
| pp = detached_payload_part(b64, payload, len); |
| if (pp == NULL) { |
| jwt_write_error(checker, "Error allocating memory"); // LCOV_EXCL_LINE |
| return 1; // LCOV_EXCL_LINE |
| } |
| |
| v = jwt_json_create_str(pp); |
| if (v == NULL || jwt_json_obj_set(root, "payload", v)) |
| return 1; // LCOV_EXCL_LINE |
| |
| recon = jwt_json_serialize(root, JWT_JSON_COMPACT); |
| if (recon == NULL) |
| return 1; // LCOV_EXCL_LINE |
| } else { |
| /* @rfc{7515,7.1} Compact: rebuild header "." payload "." signature. */ |
| const char *firstdot = strchr(token, '.'); |
| const char *lastdot = strrchr(token, '.'); |
| char_auto *head_b64 = NULL; |
| size_t head_len, sig_len, recon_len; |
| const char *sig; |
| |
| if (firstdot == NULL || lastdot == firstdot) { |
| jwt_write_error(checker, |
| "Not a detached Compact Serialization"); |
| return 1; |
| } |
| head_len = (size_t)(firstdot - token); |
| sig = lastdot + 1; |
| sig_len = strlen(sig); |
| |
| head_b64 = jwt_malloc(head_len + 1); |
| if (head_b64 == NULL) |
| return 1; // LCOV_EXCL_LINE |
| memcpy(head_b64, token, head_len); |
| head_b64[head_len] = '\0'; |
| |
| b64 = detached_b64(head_b64); |
| pp = detached_payload_part(b64, payload, len); |
| if (pp == NULL) |
| return 1; // LCOV_EXCL_LINE |
| |
| recon_len = head_len + 1 + strlen(pp) + 1 + sig_len; |
| recon = jwt_malloc(recon_len + 1); |
| if (recon == NULL) |
| return 1; // LCOV_EXCL_LINE |
| sprintf(recon, "%s.%s.%s", head_b64, pp, sig); |
| } |
| |
| ret = jwt_checker_verify(checker, recon); |
| |
| return ret; |
| } |