11#include " ../src/scitokens.h"
22
3+ #include < pwd.h>
34#include < memory>
45#include < gtest/gtest.h>
56
7+ #include < openssl/bio.h>
8+ #include < openssl/err.h>
9+ #include < openssl/ec.h>
10+ #include < openssl/pem.h>
11+
12+ #ifndef PICOJSON_USE_INT64
13+ #define PICOJSON_USE_INT64
14+ #endif
15+ #include < picojson/picojson.h>
16+ #include < sqlite3.h>
17+
618namespace {
719
820const char ec_private[] = " -----BEGIN EC PRIVATE KEY-----\n "
@@ -27,6 +39,216 @@ const char ec_public_2[] = "-----BEGIN PUBLIC KEY-----\n"
2739" XWCq4E/g2ME/uBOdP8RE0tqle8fxYcaPikgMcppGq2ycTiLGgEYXgsq2JA==\n "
2840" -----END PUBLIC KEY-----\n " ;
2941
42+ /* *
43+ * Duplicate of get_cache_file from scitokens_cache.cpp; used for direct
44+ * SQLite manipulation.
45+ */
46+ std::string
47+ get_cache_file () {
48+
49+ const char *xdg_cache_home = getenv (" XDG_CACHE_HOME" );
50+
51+ auto bufsize = sysconf (_SC_GETPW_R_SIZE_MAX);
52+ bufsize = (bufsize == -1 ) ? 16384 : bufsize;
53+
54+ std::unique_ptr<char []> buf (new char [bufsize]);
55+
56+ std::string home_dir;
57+ struct passwd pwd, *result = NULL ;
58+ getpwuid_r (geteuid (), &pwd, buf.get (), bufsize, &result);
59+ if (result && result->pw_dir ) {
60+ home_dir = result->pw_dir ;
61+ home_dir += " /.cache" ;
62+ }
63+
64+ std::string cache_dir (xdg_cache_home ? xdg_cache_home : home_dir.c_str ());
65+ if (cache_dir.size () == 0 ) {
66+ return " " ;
67+ }
68+
69+ int r = mkdir (cache_dir.c_str (), 0700 );
70+ if ((r < 0 ) && errno != EEXIST) {
71+ return " " ;
72+ }
73+
74+ std::string keycache_dir = cache_dir + " /scitokens" ;
75+ r = mkdir (keycache_dir.c_str (), 0700 );
76+ if ((r < 0 ) && errno != EEXIST) {
77+ return " " ;
78+ }
79+
80+ std::string keycache_file = keycache_dir + " /scitokens_cpp.sqllite" ;
81+ // Assume this isn't needed; we'll trigger it via the "real" cache routines.
82+ // initialize_cachedb(keycache_file);
83+
84+ return keycache_file;
85+ }
86+
87+ /* *
88+ * Duplicate of remove_issuer_entry from scitokens_cache.cpp; used for direct cache manipulation
89+ */
90+ void
91+ remove_issuer_entry (sqlite3 *db, const std::string &issuer, bool new_transaction) {
92+
93+ if (new_transaction) sqlite3_exec (db, " BEGIN" , 0 , 0 , 0 );
94+
95+ sqlite3_stmt *stmt;
96+ int rc = sqlite3_prepare_v2 (db, " DELETE FROM keycache WHERE issuer = ?" , -1 , &stmt, NULL );
97+ if (rc != SQLITE_OK) {
98+ sqlite3_close (db);
99+ return ;
100+ }
101+
102+ if (sqlite3_bind_text (stmt, 1 , issuer.c_str (), issuer.size (), SQLITE_STATIC) != SQLITE_OK) {
103+ sqlite3_finalize (stmt);
104+ sqlite3_close (db);
105+ return ;
106+ }
107+
108+ rc = sqlite3_step (stmt);
109+ if (rc != SQLITE_DONE) {
110+ sqlite3_finalize (stmt);
111+ sqlite3_close (db);
112+ return ;
113+ }
114+
115+ sqlite3_finalize (stmt);
116+
117+ if (new_transaction) sqlite3_exec (db, " COMMIT" , 0 , 0 , 0 );
118+ }
119+
120+ /* *
121+ * Duplicate of store_public_keys from scitokens_cache.cpp; used for direct cache manipulation.
122+ */
123+ bool
124+ store_public_keys (const std::string &issuer, const std::string &keys, int64_t next_update, int64_t expires) {
125+
126+ picojson::value json_obj;
127+ auto err = picojson::parse (json_obj, keys);
128+ if (!err.empty () || !json_obj.is <picojson::object>()) {
129+ return false ;
130+ }
131+
132+ picojson::object top_obj;
133+ top_obj[" jwks" ] = json_obj;
134+ top_obj[" next_update" ] = picojson::value (next_update);
135+ top_obj[" expires" ] = picojson::value (expires);
136+ picojson::value db_value (top_obj);
137+ std::string db_str = db_value.serialize ();
138+
139+ auto cache_fname = get_cache_file ();
140+ if (cache_fname.size () == 0 ) {return false ;}
141+
142+ sqlite3 *db;
143+ int rc = sqlite3_open (cache_fname.c_str (), &db);
144+ if (rc) {
145+ sqlite3_close (db);
146+ return false ;
147+ }
148+
149+ sqlite3_exec (db, " BEGIN" , 0 , 0 , 0 );
150+
151+ remove_issuer_entry (db, issuer, false );
152+
153+ sqlite3_stmt *stmt;
154+ rc = sqlite3_prepare_v2 (db, " INSERT INTO keycache VALUES (?, ?)" , -1 , &stmt, NULL );
155+ if (rc != SQLITE_OK) {
156+ sqlite3_close (db);
157+ return false ;
158+ }
159+
160+ if (sqlite3_bind_text (stmt, 1 , issuer.c_str (), issuer.size (), SQLITE_STATIC) != SQLITE_OK) {
161+ sqlite3_finalize (stmt);
162+ sqlite3_close (db);
163+ return false ;
164+ }
165+
166+ if (sqlite3_bind_text (stmt, 2 , db_str.c_str (), db_str.size (), SQLITE_STATIC) != SQLITE_OK) {
167+ sqlite3_finalize (stmt);
168+ sqlite3_close (db);
169+ return false ;
170+ }
171+
172+ rc = sqlite3_step (stmt);
173+ if (rc != SQLITE_DONE) {
174+ sqlite3_finalize (stmt);
175+ sqlite3_close (db);
176+ return false ;
177+ }
178+
179+ sqlite3_exec (db, " COMMIT" , 0 , 0 , 0 );
180+
181+ sqlite3_finalize (stmt);
182+ sqlite3_close (db);
183+ return true ;
184+ }
185+
186+ bool
187+ get_public_keys_from_db (const std::string issuer, int64_t &expires, int64_t &next_update) {
188+ auto cache_fname = get_cache_file ();
189+ if (cache_fname.size () == 0 ) {return false ;}
190+
191+ sqlite3 *db;
192+ int rc = sqlite3_open (cache_fname.c_str (), &db);
193+ if (rc) {
194+ sqlite3_close (db);
195+ return false ;
196+ }
197+
198+ sqlite3_stmt *stmt;
199+ rc = sqlite3_prepare_v2 (db, " SELECT keys from keycache where issuer = ?" , -1 , &stmt, NULL );
200+ if (rc != SQLITE_OK) {
201+ sqlite3_close (db);
202+ return false ;
203+ }
204+
205+ if (sqlite3_bind_text (stmt, 1 , issuer.c_str (), issuer.size (), SQLITE_STATIC) != SQLITE_OK) {
206+ sqlite3_finalize (stmt);
207+ sqlite3_close (db);
208+ return false ;
209+ }
210+
211+ rc = sqlite3_step (stmt);
212+ if (rc == SQLITE_ROW) {
213+ const unsigned char * data = sqlite3_column_text (stmt, 0 );
214+ std::string metadata (reinterpret_cast <const char *>(data));
215+ sqlite3_finalize (stmt);
216+ picojson::value json_obj;
217+ auto err = picojson::parse (json_obj, metadata);
218+ if (!err.empty () || !json_obj.is <picojson::object>()) {
219+ sqlite3_close (db);
220+ return false ;
221+ }
222+ auto top_obj = json_obj.get <picojson::object>();
223+ auto iter = top_obj.find (" jwks" );
224+ auto keys_local = iter->second ;
225+ iter = top_obj.find (" expires" );
226+ if (iter == top_obj.end () || !iter->second .is <int64_t >()) {
227+ sqlite3_close (db);
228+ return false ;
229+ }
230+ auto expiry = iter->second .get <int64_t >();
231+ sqlite3_close (db);
232+ iter = top_obj.find (" next_update" );
233+ if (iter == top_obj.end () || !iter->second .is <int64_t >()) {
234+ next_update = expiry - 4 *3600 ;
235+ } else {
236+ next_update = iter->second .get <int64_t >();
237+ }
238+ expires = expiry;
239+ return true ;
240+ } else if (rc == SQLITE_DONE) {
241+ sqlite3_finalize (stmt);
242+ sqlite3_close (db);
243+ return false ;
244+ } else {
245+ // TODO: log error?
246+ sqlite3_finalize (stmt);
247+ sqlite3_close (db);
248+ return false ;
249+ }
250+ }
251+
30252TEST (SciTokenTest, CreateToken) {
31253 SciToken token = scitoken_create (nullptr );
32254 ASSERT_TRUE (token != nullptr );
@@ -63,6 +285,7 @@ class KeycacheTest : public ::testing::Test
63285{
64286 protected:
65287 std::string demo_scitokens_url = " https://demo.scitokens.org" ;
288+ std::string demo_invalid_url = " https://demo.scitokens.org/invalid" ;
66289
67290 void SetUp () override {
68291 char *err_msg;
@@ -77,6 +300,76 @@ class KeycacheTest : public ::testing::Test
77300};
78301
79302
303+ // Emulate the case of an issuer failure. Store a public key that
304+ // is in the need of an update. Make sure, on failure, the next_update
305+ // is 5 minutes ahead of the present.
306+ TEST_F (KeycacheTest, FailureTest) {
307+ time_t now = time (NULL );
308+ const time_t expiry = now + 86400 ;
309+ // Insert a public key that requires an update on next token verification.
310+ ASSERT_TRUE (store_public_keys (demo_invalid_url, demo_scitokens2, now - 600 , expiry));
311+
312+ // Create a new token with an invalid signature.
313+ OpenSSL_add_all_algorithms ();
314+ ERR_load_BIO_strings ();
315+ ERR_load_crypto_strings ();
316+ auto outbio = BIO_new (BIO_s_mem ());
317+ ASSERT_TRUE (outbio != nullptr );
318+ auto eccgrp = OBJ_txt2nid (" secp256k1" );
319+ auto ecc = EC_KEY_new_by_curve_name (eccgrp);
320+ ASSERT_TRUE (1 == EC_KEY_generate_key (ecc));
321+
322+ auto pkey = EVP_PKEY_new ();
323+ ASSERT_TRUE (1 == EVP_PKEY_assign_EC_KEY (pkey, ecc));
324+ ASSERT_TRUE (1 == PEM_write_bio_PrivateKey (outbio, pkey, NULL , NULL , 0 , 0 , NULL ));
325+
326+ char *pem_data;
327+ long pem_len = BIO_get_mem_data (outbio, &pem_data);
328+ std::string pem_str (pem_data, pem_len);
329+
330+ // Generate a serialized token from the new key.
331+ auto key = scitoken_key_create (" test_key" , " ES256" , " " , pem_str.c_str (), nullptr );
332+ ASSERT_TRUE (key != nullptr );
333+
334+ auto token = scitoken_create (key);
335+ ASSERT_TRUE (token != nullptr );
336+
337+ auto rv = scitoken_set_claim_string (token, " iss" , demo_invalid_url.c_str (), nullptr );
338+ ASSERT_TRUE (rv == 0 );
339+
340+ rv = scitoken_set_claim_string (token, " sub" , " test_user" , nullptr );
341+ ASSERT_TRUE (rv == 0 );
342+
343+ scitoken_set_lifetime (token, 86400 );
344+
345+ char *token_encoded;
346+ rv = scitoken_serialize (token, &token_encoded, nullptr );
347+ ASSERT_TRUE (rv == 0 );
348+ std::string token_str (token_encoded);
349+ free (token_encoded);
350+
351+ // Try to deserialize the newly generated token. Should fail as the key doesn't match.
352+ auto token_read = scitoken_create (nullptr );
353+ ASSERT_TRUE (token_read != nullptr );
354+ rv = scitoken_deserialize_v2 (token_str.c_str (), token_read, nullptr , nullptr );
355+ ASSERT_FALSE (rv == 0 );
356+
357+ // Now, for the real test -- what's the value of expired and next_update?
358+ int64_t new_expiry, new_next_update;
359+ ASSERT_TRUE (get_public_keys_from_db (demo_invalid_url, new_expiry, new_next_update));
360+
361+ EXPECT_EQ (new_expiry, expiry);
362+ EXPECT_GE (new_next_update, now + 300 );
363+
364+ // Second test: if the expiration is behind us, fetching the key should trigger
365+ // a deletion of the key cache.
366+ ASSERT_TRUE (store_public_keys (demo_invalid_url, demo_scitokens2, now - 600 , now - 600 ));
367+
368+ rv = scitoken_deserialize_v2 (token_str.c_str (), token_read, nullptr , nullptr );
369+
370+ ASSERT_FALSE (get_public_keys_from_db (demo_invalid_url, new_expiry, new_next_update));
371+ }
372+
80373TEST_F (KeycacheTest, RefreshTest) {
81374 char *err_msg;
82375 auto rv = keycache_refresh_jwks (demo_scitokens_url.c_str (), &err_msg);
0 commit comments