11
2+ #include < cctype>
23#include < cstdint>
4+ #include < cstring>
5+ #include < fstream>
6+ #include < iomanip>
37#include < memory>
8+ #include < sstream>
49#include < string>
10+ #include < vector>
511
612#include < pwd.h>
713#include < stdlib.h>
1420#include < picojson/picojson.h>
1521#include < sqlite3.h>
1622
23+ #include < cmath>
24+ #include < openssl/sha.h>
25+
1726#include " scitokens_internal.h"
1827
1928namespace {
@@ -25,6 +34,290 @@ constexpr int SQLITE_BUSY_TIMEOUT_MS = 5000;
2534// Default time before expiry when next_update should occur (4 hours)
2635constexpr int64_t DEFAULT_NEXT_UPDATE_OFFSET_S = 4 * 3600 ;
2736
37+ enum class CacheLookupResult { Found, NotFound, ParseError, Expired };
38+
39+ bool json_to_int64 (const picojson::value &val, int64_t &out) {
40+ if (val.is <int64_t >()) {
41+ out = val.get <int64_t >();
42+ return true ;
43+ }
44+ if (val.is <double >()) {
45+ out = static_cast <int64_t >(std::floor (val.get <double >()));
46+ return true ;
47+ }
48+ return false ;
49+ }
50+
51+ std::string issuer_hash_prefix (const std::string &issuer) {
52+ unsigned char hash[SHA256_DIGEST_LENGTH];
53+ SHA256 (reinterpret_cast <const unsigned char *>(issuer.data ()),
54+ issuer.size (), hash);
55+
56+ std::ostringstream oss;
57+ oss << std::hex << std::setfill (' 0' );
58+ for (int i = 0 ; i < 4 ; i++) {
59+ oss << std::setw (2 ) << static_cast <int >(hash[i]);
60+ }
61+ return oss.str ();
62+ }
63+
64+ bool extract_json_objects (const std::string &content,
65+ std::vector<std::string> &objects) {
66+ size_t idx = 0 ;
67+ const size_t len = content.size ();
68+
69+ while (idx < len) {
70+ while (idx < len && isspace (static_cast <unsigned char >(content[idx]))) {
71+ idx++;
72+ }
73+ if (idx >= len) {
74+ break ;
75+ }
76+ if (content[idx] != ' {' ) {
77+ break ; // Non-whitespace outside JSON terminates parsing
78+ }
79+
80+ size_t start = idx;
81+ int depth = 0 ;
82+ bool in_string = false ;
83+ bool escape = false ;
84+ for (; idx < len; idx++) {
85+ char c = content[idx];
86+ if (in_string) {
87+ if (escape) {
88+ escape = false ;
89+ continue ;
90+ }
91+ if (c == ' \\ ' ) {
92+ escape = true ;
93+ } else if (c == ' "' ) {
94+ in_string = false ;
95+ }
96+ } else {
97+ if (c == ' "' ) {
98+ in_string = true ;
99+ } else if (c == ' {' ) {
100+ depth++;
101+ } else if (c == ' }' ) {
102+ depth--;
103+ if (depth == 0 ) {
104+ objects.emplace_back (
105+ content.substr (start, idx - start + 1 ));
106+ idx++;
107+ break ;
108+ }
109+ }
110+ }
111+ }
112+
113+ if (depth != 0 ) {
114+ return false ; // Unbalanced braces
115+ }
116+ }
117+ return true ;
118+ }
119+
120+ CacheLookupResult parse_cache_object_for_issuer (const picojson::value &val,
121+ const std::string &issuer,
122+ int64_t now,
123+ picojson::value &keys,
124+ int64_t &next_update) {
125+ if (!val.is <picojson::object>()) {
126+ return CacheLookupResult::ParseError;
127+ }
128+
129+ const auto &root = val.get <picojson::object>();
130+ auto issuer_it = root.find (issuer);
131+ if (issuer_it == root.end ()) {
132+ return CacheLookupResult::NotFound;
133+ }
134+
135+ if (!issuer_it->second .is <picojson::object>()) {
136+ return CacheLookupResult::ParseError;
137+ }
138+
139+ const auto &entry_obj = issuer_it->second .get <picojson::object>();
140+
141+ auto exp_it = entry_obj.find (" expiration" );
142+ if (exp_it == entry_obj.end ()) {
143+ return CacheLookupResult::ParseError;
144+ }
145+
146+ int64_t expiration;
147+ if (!json_to_int64 (exp_it->second , expiration)) {
148+ return CacheLookupResult::ParseError;
149+ }
150+
151+ auto jwks_it = entry_obj.find (" jwks" );
152+ if (jwks_it == entry_obj.end () || !jwks_it->second .is <picojson::object>()) {
153+ return CacheLookupResult::ParseError;
154+ }
155+
156+ if (now > expiration) {
157+ return CacheLookupResult::Expired;
158+ }
159+
160+ int64_t nu_value = expiration - DEFAULT_NEXT_UPDATE_OFFSET_S;
161+ auto nu_it = entry_obj.find (" next_update" );
162+ if (nu_it != entry_obj.end ()) {
163+ int64_t tmp;
164+ if (!json_to_int64 (nu_it->second , tmp)) {
165+ return CacheLookupResult::ParseError;
166+ }
167+ nu_value = tmp;
168+ }
169+
170+ keys = jwks_it->second ;
171+ next_update = nu_value;
172+ return CacheLookupResult::Found;
173+ }
174+
175+ CacheLookupResult parse_cache_file_for_issuer (const std::string &path,
176+ const std::string &issuer,
177+ int64_t now,
178+ picojson::value &keys,
179+ int64_t &next_update) {
180+ std::ifstream infile (path);
181+ if (!infile) {
182+ return CacheLookupResult::NotFound;
183+ }
184+
185+ std::stringstream buffer;
186+ buffer << infile.rdbuf ();
187+ std::string content = buffer.str ();
188+
189+ std::vector<std::string> objects;
190+ if (!extract_json_objects (content, objects)) {
191+ return CacheLookupResult::ParseError;
192+ }
193+
194+ CacheLookupResult status = CacheLookupResult::NotFound;
195+ for (const auto &obj_str : objects) {
196+ picojson::value val;
197+ std::string err = picojson::parse (val, obj_str);
198+ if (!err.empty ()) {
199+ return CacheLookupResult::ParseError;
200+ }
201+
202+ auto res =
203+ parse_cache_object_for_issuer (val, issuer, now, keys, next_update);
204+ if (res == CacheLookupResult::ParseError) {
205+ return res;
206+ }
207+ if (res == CacheLookupResult::Found) {
208+ status = CacheLookupResult::Found;
209+ } else if (res == CacheLookupResult::Expired &&
210+ status == CacheLookupResult::NotFound) {
211+ status = CacheLookupResult::Expired;
212+ }
213+ }
214+
215+ return status;
216+ }
217+
218+ CacheLookupResult parse_cache_dir_for_issuer (const std::string &dir,
219+ const std::string &issuer,
220+ int64_t now, picojson::value &keys,
221+ int64_t &next_update) {
222+ bool expired_seen = false ;
223+ auto prefix = issuer_hash_prefix (issuer);
224+ for (int idx = 0 ;; idx++) {
225+ std::ostringstream fname;
226+ fname << dir << " /" << prefix << " ." << idx;
227+
228+ struct stat st;
229+ if (stat (fname.str ().c_str (), &st) != 0 ) {
230+ break ; // First missing file terminates search
231+ }
232+ if (!S_ISREG (st.st_mode )) {
233+ continue ;
234+ }
235+
236+ auto res = parse_cache_file_for_issuer (fname.str (), issuer, now, keys,
237+ next_update);
238+ if (res == CacheLookupResult::Found ||
239+ res == CacheLookupResult::ParseError) {
240+ return res;
241+ }
242+ if (res == CacheLookupResult::Expired) {
243+ expired_seen = true ;
244+ }
245+ }
246+
247+ if (expired_seen) {
248+ return CacheLookupResult::Expired;
249+ }
250+ return CacheLookupResult::NotFound;
251+ }
252+
253+ bool get_public_keys_from_system_cache (const std::string &issuer, int64_t now,
254+ picojson::value &keys,
255+ int64_t &next_update,
256+ bool &expired_hit) {
257+
258+ expired_hit = false ;
259+
260+ struct Location {
261+ enum class Type { Dir, File } type;
262+ std::string path;
263+ };
264+
265+ std::vector<Location> search_order;
266+ const char *env_dir = getenv (" JWKS_CACHE_DIR" );
267+ const char *env_file = getenv (" JWKS_CACHE_FILE" );
268+
269+ if (env_dir && strlen (env_dir) > 0 ) {
270+ search_order.push_back ({Location::Type::Dir, env_dir});
271+ }
272+ if (env_file && strlen (env_file) > 0 ) {
273+ search_order.push_back ({Location::Type::File, env_file});
274+ }
275+
276+ search_order.push_back ({Location::Type::Dir, " /etc/jwks" });
277+ search_order.push_back ({Location::Type::File, " /etc/jwks/cache.json" });
278+ search_order.push_back ({Location::Type::Dir, " /var/cache/jwks" });
279+ search_order.push_back (
280+ {Location::Type::File, " /var/cache/jwks/cache.json" });
281+
282+ for (const auto &loc : search_order) {
283+ struct stat st;
284+ if (stat (loc.path .c_str (), &st) != 0 ) {
285+ continue ;
286+ }
287+
288+ CacheLookupResult res = CacheLookupResult::NotFound;
289+ if (loc.type == Location::Type::Dir) {
290+ if (!S_ISDIR (st.st_mode )) {
291+ continue ;
292+ }
293+ res = parse_cache_dir_for_issuer (loc.path , issuer, now, keys,
294+ next_update);
295+ } else {
296+ if (!S_ISREG (st.st_mode )) {
297+ continue ;
298+ }
299+ res = parse_cache_file_for_issuer (loc.path , issuer, now, keys,
300+ next_update);
301+ }
302+
303+ if (res == CacheLookupResult::Found) {
304+ auto &stats = scitokens::internal::MonitoringStats::instance ()
305+ .get_issuer_stats (issuer);
306+ stats.inc_system_cache_hit ();
307+ return true ;
308+ }
309+ if (res == CacheLookupResult::ParseError) {
310+ throw scitokens::JsonException (" Failed to parse JWKS cache at " +
311+ loc.path );
312+ }
313+ if (res == CacheLookupResult::Expired) {
314+ expired_hit = true ;
315+ }
316+ }
317+
318+ return false ;
319+ }
320+
28321void initialize_cachedb (const std::string &keycache_file) {
29322
30323 sqlite3 *db;
@@ -159,6 +452,23 @@ bool scitokens::Validator::get_public_keys_from_db(const std::string issuer,
159452 int64_t now,
160453 picojson::value &keys,
161454 int64_t &next_update) {
455+ bool expired_hit = false ;
456+ try {
457+ if (get_public_keys_from_system_cache (issuer, now, keys, next_update,
458+ expired_hit)) {
459+ return true ;
460+ }
461+ } catch (const JsonException &) {
462+ throw ;
463+ }
464+
465+ if (expired_hit) {
466+ auto &stats =
467+ scitokens::internal::MonitoringStats::instance ().get_issuer_stats (
468+ issuer);
469+ stats.inc_system_cache_expired ();
470+ }
471+
162472 auto cache_fname = get_cache_file ();
163473 if (cache_fname.size () == 0 ) {
164474 return false ;
0 commit comments