1
1
#include " ../src/scitokens.h"
2
2
3
+ #include < pwd.h>
3
4
#include < memory>
4
5
#include < gtest/gtest.h>
5
6
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
+
6
18
namespace {
7
19
8
20
const char ec_private[] = " -----BEGIN EC PRIVATE KEY-----\n "
@@ -27,6 +39,216 @@ const char ec_public_2[] = "-----BEGIN PUBLIC KEY-----\n"
27
39
" XWCq4E/g2ME/uBOdP8RE0tqle8fxYcaPikgMcppGq2ycTiLGgEYXgsq2JA==\n "
28
40
" -----END PUBLIC KEY-----\n " ;
29
41
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
+
30
252
TEST (SciTokenTest, CreateToken) {
31
253
SciToken token = scitoken_create (nullptr );
32
254
ASSERT_TRUE (token != nullptr );
@@ -63,6 +285,7 @@ class KeycacheTest : public ::testing::Test
63
285
{
64
286
protected:
65
287
std::string demo_scitokens_url = " https://demo.scitokens.org" ;
288
+ std::string demo_invalid_url = " https://demo.scitokens.org/invalid" ;
66
289
67
290
void SetUp () override {
68
291
char *err_msg;
@@ -77,6 +300,76 @@ class KeycacheTest : public ::testing::Test
77
300
};
78
301
79
302
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
+
80
373
TEST_F (KeycacheTest, RefreshTest) {
81
374
char *err_msg;
82
375
auto rv = keycache_refresh_jwks (demo_scitokens_url.c_str (), &err_msg);
0 commit comments