Skip to content

Commit 2f14506

Browse files
committed
Unit test coverage for failed issuer
Test the behavior around an issuer failure -- as long as the cached pubkey isn't expired, we shouldn't try again for 5 minutes.
1 parent 7ec2670 commit 2f14506

File tree

1 file changed

+293
-0
lines changed

1 file changed

+293
-0
lines changed

test/main.cpp

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
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+
618
namespace {
719

820
const 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+
30252
TEST(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+
80373
TEST_F(KeycacheTest, RefreshTest) {
81374
char *err_msg;
82375
auto rv = keycache_refresh_jwks(demo_scitokens_url.c_str(), &err_msg);

0 commit comments

Comments
 (0)