mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
- Copy pg-knoe-auth/ wholesale from upstream/knoe-db/20260523 (Task 1 of docs/plans/junie/upstream-knoe-db-20260523-integration.md). - Extension: PG18 OAUTHBEARER JWT validator using libcurl + OpenSSL RS256. - knoe-db/Dockerfile: add libcurl4-openssl-dev to dev deps; COPY src/ and build with make USE_PGXS=1 install after tds_fdw. - NOT enabled in the default database build (absent from 20_create_extensions.sh). To enable: CREATE EXTENSION pg_knoe_auth; (requires pg_hba.conf oauth_issuer). Closes Task 1 of upstream-knoe-db-20260523-integration.md.
168 lines
6.1 KiB
C
168 lines
6.1 KiB
C
/*
|
|
* test_jwks_cache.c — unit tests for H2: JWKS process-local cache.
|
|
*
|
|
* Tests cache hit returns same pkey ptr, miss triggers fetch path,
|
|
* stale fallback logic, and eviction at JWK_CACHE_MAX entries.
|
|
* See docs/plans/junie/pg_knoe_auth-rename-harden-modularize.md §2.2.2
|
|
*/
|
|
#include "test_helpers.h"
|
|
/* Suppress OpenSSL 3.x deprecation warnings for RSA legacy API used in test stubs only */
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
#include <openssl/evp.h>
|
|
#include <openssl/rsa.h>
|
|
#include <openssl/bn.h>
|
|
#pragma GCC diagnostic pop
|
|
#include <time.h>
|
|
|
|
/* ── Inline cache implementation (same as pg_knoe_auth.c) ─────────────────── */
|
|
|
|
#define JWK_CACHE_MAX 32
|
|
#define JWK_CACHE_TTL_SECS 600
|
|
#define JWK_CACHE_STALE_SECS 86400
|
|
|
|
typedef struct {
|
|
char *issuer;
|
|
char *kid;
|
|
EVP_PKEY *pkey;
|
|
time_t fetched_at;
|
|
} JwkCacheEntry;
|
|
|
|
static JwkCacheEntry jwk_cache[JWK_CACHE_MAX];
|
|
static int jwk_cache_n = 0;
|
|
|
|
static int
|
|
jwk_cache_find(const char *issuer, const char *kid)
|
|
{
|
|
for (int i = 0; i < jwk_cache_n; i++) {
|
|
if (strcmp(jwk_cache[i].issuer, issuer) != 0) continue;
|
|
if (kid == NULL && jwk_cache[i].kid == NULL) return i;
|
|
if (kid != NULL && jwk_cache[i].kid != NULL &&
|
|
strcmp(jwk_cache[i].kid, kid) == 0) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
jwk_cache_store(const char *issuer, const char *kid, EVP_PKEY *pkey)
|
|
{
|
|
int idx = jwk_cache_find(issuer, kid);
|
|
|
|
if (idx < 0) {
|
|
if (jwk_cache_n < JWK_CACHE_MAX) {
|
|
idx = jwk_cache_n++;
|
|
} else {
|
|
idx = 0;
|
|
free(jwk_cache[0].issuer);
|
|
free(jwk_cache[0].kid);
|
|
EVP_PKEY_free(jwk_cache[0].pkey);
|
|
memmove(&jwk_cache[0], &jwk_cache[1],
|
|
(JWK_CACHE_MAX - 1) * sizeof(JwkCacheEntry));
|
|
jwk_cache_n = JWK_CACHE_MAX - 1;
|
|
idx = jwk_cache_n++;
|
|
}
|
|
} else {
|
|
free(jwk_cache[idx].issuer);
|
|
free(jwk_cache[idx].kid);
|
|
EVP_PKEY_free(jwk_cache[idx].pkey);
|
|
}
|
|
|
|
jwk_cache[idx].issuer = strdup(issuer);
|
|
jwk_cache[idx].kid = kid ? strdup(kid) : NULL;
|
|
jwk_cache[idx].pkey = pkey;
|
|
jwk_cache[idx].fetched_at = time(NULL);
|
|
}
|
|
|
|
/* Reset cache between tests */
|
|
static void cache_reset(void) {
|
|
for (int i = 0; i < jwk_cache_n; i++) {
|
|
free(jwk_cache[i].issuer);
|
|
free(jwk_cache[i].kid);
|
|
EVP_PKEY_free(jwk_cache[i].pkey);
|
|
}
|
|
jwk_cache_n = 0;
|
|
}
|
|
|
|
/* Make a throwaway RSA EVP_PKEY for testing (uses legacy RSA API — suppress deprecation) */
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
static EVP_PKEY *make_test_pkey(void) {
|
|
RSA *rsa = RSA_new();
|
|
BIGNUM *bn_n = BN_new();
|
|
BIGNUM *bn_e = BN_new();
|
|
EVP_PKEY *pkey = EVP_PKEY_new();
|
|
BN_set_word(bn_n, 0xDEADBEEF);
|
|
BN_set_word(bn_e, 65537);
|
|
RSA_set0_key(rsa, bn_n, bn_e, NULL);
|
|
EVP_PKEY_assign_RSA(pkey, rsa);
|
|
return pkey;
|
|
}
|
|
#pragma GCC diagnostic pop
|
|
|
|
int main(void)
|
|
{
|
|
const char *issuer = "https://api.0.knoe.dev/auth";
|
|
const char *kid1 = "key-001";
|
|
const char *kid2 = "key-002";
|
|
|
|
/* ── Test 1: cache miss returns -1 ─────────────────────────────────── */
|
|
cache_reset();
|
|
ASSERT(jwk_cache_find(issuer, kid1) == -1,
|
|
"cache miss returns -1 on empty cache");
|
|
|
|
/* ── Test 2: store + hit returns same index ─────────────────────────── */
|
|
EVP_PKEY *pk1 = make_test_pkey();
|
|
jwk_cache_store(issuer, kid1, pk1);
|
|
int idx = jwk_cache_find(issuer, kid1);
|
|
ASSERT(idx >= 0, "cache hit after store returns valid index");
|
|
ASSERT(jwk_cache[idx].pkey == pk1,
|
|
"cache hit returns the same pkey pointer");
|
|
|
|
/* ── Test 3: different kid is a miss ────────────────────────────────── */
|
|
ASSERT(jwk_cache_find(issuer, kid2) == -1,
|
|
"different kid is a cache miss");
|
|
|
|
/* ── Test 4: overwrite same (issuer, kid) updates fetched_at ────────── */
|
|
time_t before = time(NULL);
|
|
EVP_PKEY *pk1b = make_test_pkey();
|
|
jwk_cache_store(issuer, kid1, pk1b);
|
|
idx = jwk_cache_find(issuer, kid1);
|
|
ASSERT(idx >= 0 && jwk_cache[idx].pkey == pk1b,
|
|
"overwrite same key updates pkey pointer");
|
|
ASSERT(jwk_cache[idx].fetched_at >= before,
|
|
"overwrite updates fetched_at");
|
|
|
|
/* ── Test 5: stale detection — age >= TTL ───────────────────────────── */
|
|
jwk_cache[idx].fetched_at = time(NULL) - JWK_CACHE_TTL_SECS - 1;
|
|
time_t age = time(NULL) - jwk_cache[idx].fetched_at;
|
|
ASSERT(age >= JWK_CACHE_TTL_SECS,
|
|
"entry with age >= TTL is detected as stale");
|
|
|
|
/* ── Test 6: stale but within STALE_SECS — fallback window open ─────── */
|
|
jwk_cache[idx].fetched_at = time(NULL) - JWK_CACHE_TTL_SECS - 1;
|
|
age = time(NULL) - jwk_cache[idx].fetched_at;
|
|
ASSERT(age < JWK_CACHE_STALE_SECS,
|
|
"stale entry within 24h stale window is usable as fallback");
|
|
|
|
/* ── Test 7: eviction at JWK_CACHE_MAX ──────────────────────────────── */
|
|
cache_reset();
|
|
for (int i = 0; i < JWK_CACHE_MAX; i++) {
|
|
char kid_buf[32];
|
|
snprintf(kid_buf, sizeof(kid_buf), "kid-%03d", i);
|
|
jwk_cache_store(issuer, kid_buf, make_test_pkey());
|
|
}
|
|
ASSERT(jwk_cache_n == JWK_CACHE_MAX,
|
|
"cache fills to JWK_CACHE_MAX");
|
|
/* Adding one more should evict the oldest (kid-000) */
|
|
jwk_cache_store(issuer, "kid-new", make_test_pkey());
|
|
ASSERT(jwk_cache_n == JWK_CACHE_MAX,
|
|
"cache stays at JWK_CACHE_MAX after eviction");
|
|
ASSERT(jwk_cache_find(issuer, "kid-000") == -1,
|
|
"oldest entry (kid-000) was evicted");
|
|
ASSERT(jwk_cache_find(issuer, "kid-new") >= 0,
|
|
"new entry is present after eviction");
|
|
|
|
cache_reset();
|
|
TEST_SUMMARY();
|
|
}
|