Files
ginxsom/src/request_validator.c

1927 lines
72 KiB
C

/*
* Ginxsom Request Validator - Integrated Authentication System
*
* Provides complete request validation including:
* - Protocol validation via nostr_core_lib (signatures, pubkey extraction,
* NIP-42)
* - Database-driven authorization rules (whitelist, blacklist, size limits)
* - Memory caching for performance
* - SQLite integration for ginxsom-specific needs
*/
#define _GNU_SOURCE
#include "../nostr_core_lib/cjson/cJSON.h"
#include "../nostr_core_lib/nostr_core/nip001.h"
#include "../nostr_core_lib/nostr_core/nip042.h"
#include "../nostr_core_lib/nostr_core/nostr_common.h"
#include "../nostr_core_lib/nostr_core/utils.h"
#include "ginxsom.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
// Additional error codes for ginxsom-specific functionality
#define NOSTR_ERROR_CRYPTO_INIT -100
#define NOSTR_ERROR_AUTH_REQUIRED -101
#define NOSTR_ERROR_NIP42_DISABLED -102
#define NOSTR_ERROR_EVENT_EXPIRED -103
// Note: NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND and
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
// nostr_core_lib/nostr_core/nostr_common.h
// Use global database path from main.c
extern char g_db_path[];
// NIP-42 challenge management constants
#define MAX_CHALLENGES 1000
#define CHALLENGE_CLEANUP_INTERVAL 300 // 5 minutes
//=============================================================================
// DATA STRUCTURES
//=============================================================================
// NIP-42 challenge storage
typedef struct {
char challenge_id[65];
char client_ip[64];
time_t created_at;
time_t expires_at;
int active;
} nip42_challenge_entry_t;
// NIP-42 challenge management
typedef struct {
nip42_challenge_entry_t challenges[MAX_CHALLENGES];
int challenge_count;
time_t last_cleanup;
int timeout_seconds;
int time_tolerance_seconds;
} nip42_challenge_manager_t;
// Cached configuration structure
typedef struct {
int auth_required; // Whether authentication is required
long max_file_size; // Maximum file size in bytes
int admin_enabled; // Whether admin interface is enabled
char admin_pubkey[65]; // Admin public key
int nip42_mode; // NIP-42 authentication mode
int nip42_challenge_timeout; // NIP-42 challenge timeout in seconds
int nip42_time_tolerance; // NIP-42 time tolerance in seconds
time_t cache_expires; // When cache expires
int cache_valid; // Whether cache is valid
} auth_config_cache_t;
//=============================================================================
// GLOBAL STATE
//=============================================================================
static auth_config_cache_t g_auth_cache = {0};
static nip42_challenge_manager_t g_challenge_manager = {0};
static int g_validator_initialized = 0;
// Last rule violation details for status code mapping
struct {
char violation_type[100]; // "pubkey_blacklist", "hash_blacklist",
// "whitelist_violation", etc.
char reason[500]; // specific reason string
} g_last_rule_violation = {0};
/**
* Helper function for consistent debug logging to our debug.log file
*/
static void validator_debug_log(const char *message) {
FILE *debug_log = fopen("logs/app/debug.log", "a");
if (debug_log) {
fprintf(debug_log, "%ld %s", (long)time(NULL), message);
fclose(debug_log);
}
}
//=============================================================================
// FORWARD DECLARATIONS
//=============================================================================
static int reload_auth_config(void);
static int parse_authorization_header(const char *auth_header, char *event_json,
size_t json_size);
static int extract_pubkey_from_event(cJSON *event, char *pubkey_buffer,
size_t buffer_size);
static int validate_blossom_event(cJSON *event, const char *expected_hash,
const char *method);
static int validate_nip42_event(cJSON *event, const char *relay_url,
const char *challenge_id);
static int validate_admin_event(cJSON *event, const char *method, const char *endpoint);
static int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash, const char *mime_type);
void nostr_request_validator_clear_violation(void);
// NIP-42 challenge management functions
static void cleanup_expired_challenges(void);
static int store_challenge(const char *challenge_id, const char *client_ip);
static int validate_challenge(const char *challenge_id);
static int generate_challenge_id(char *challenge_buffer, size_t buffer_size);
//=============================================================================
// MAIN API FUNCTIONS
//=============================================================================
/**
* Initialize the ginxsom request validator system
*/
int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
// Mark db_path as unused to suppress warning - it's for future use
(void)db_path;
(void)app_name;
if (g_validator_initialized) {
return NOSTR_SUCCESS; // Already initialized
}
// Initialize nostr_core_lib if not already done
if (nostr_crypto_init() != NOSTR_SUCCESS) {
validator_debug_log(
"VALIDATOR: Failed to initialize nostr crypto system\n");
return NOSTR_ERROR_CRYPTO_INIT;
}
// Load initial configuration from database
int result = reload_auth_config();
if (result != NOSTR_SUCCESS) {
validator_debug_log(
"VALIDATOR: Failed to load configuration from database\n");
return result;
}
// Initialize NIP-42 challenge manager
memset(&g_challenge_manager, 0, sizeof(g_challenge_manager));
g_challenge_manager.timeout_seconds =
g_auth_cache.nip42_challenge_timeout > 0
? g_auth_cache.nip42_challenge_timeout
: 600;
g_challenge_manager.time_tolerance_seconds =
g_auth_cache.nip42_time_tolerance > 0 ? g_auth_cache.nip42_time_tolerance
: 300;
g_challenge_manager.last_cleanup = time(NULL);
g_validator_initialized = 1;
validator_debug_log(
"VALIDATOR: Request validator initialized successfully\n");
return NOSTR_SUCCESS;
}
/**
* Check if authentication rules are enabled
*/
int nostr_auth_rules_enabled(void) {
// Reload config if cache expired
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
reload_auth_config();
}
return g_auth_cache.auth_required;
}
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
// MAIN VALIDATION OF REQUEST
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
int nostr_validate_unified_request(const nostr_unified_request_t *request,
nostr_request_result_t *result) {
// Clear previous violation details
nostr_request_validator_clear_violation();
/////////////////////////////////////////////////////////////////////
// PHASE 1: INPUT VALIDATION (Immediate Rejection ~1μs)
/////////////////////////////////////////////////////////////////////
// 1. Null Pointer Checks - Reject malformed requests instantly
if (!request || !result) {
return NOSTR_ERROR_INVALID_INPUT;
}
// 2. Initialization Check - Verify system is properly initialized
if (!g_validator_initialized) {
return NOSTR_ERROR_INVALID_INPUT;
}
// 3. Basic Structure Validation - Ensure required fields are present
// Initialize result structure
memset(result, 0, sizeof(nostr_request_result_t));
result->valid = 1; // Default allow
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "No validation required");
result->file_data = NULL;
result->file_size = 0;
result->owns_file_data = 0;
memset(result->expected_hash, 0, sizeof(result->expected_hash));
// Reload config if needed
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
reload_auth_config();
}
char config_msg[256];
sprintf(config_msg,
"VALIDATOR_DEBUG: STEP 3 PASSED - Configuration loaded "
"(auth_required=%d, nip42_enabled=%d)\n",
g_auth_cache.auth_required, request->nip42_enabled);
// Handle challenge generation operation (no authentication required)
if (request->operation && strcmp(request->operation, "challenge") == 0) {
// Check if NIP-42 is enabled
if (!request->nip42_enabled || g_auth_cache.nip42_mode == 0) {
result->valid = 0;
result->error_code = NOSTR_ERROR_NIP42_DISABLED;
strcpy(result->reason, "NIP-42 authentication is disabled");
return NOSTR_SUCCESS;
}
// Generate and store challenge
char challenge_id[65];
int challenge_result =
generate_challenge_id(challenge_id, sizeof(challenge_id));
if (challenge_result != NOSTR_SUCCESS) {
result->valid = 0;
result->error_code = challenge_result;
strcpy(result->reason, "Failed to generate challenge ID");
return NOSTR_SUCCESS;
}
// Store challenge in manager
int store_result = store_challenge(challenge_id, request->client_ip);
if (store_result != NOSTR_SUCCESS) {
result->valid = 0;
result->error_code = store_result;
strcpy(result->reason, "Failed to store challenge");
return NOSTR_SUCCESS;
}
// Return challenge in result (we'll use the reason field for the challenge
// ID)
snprintf(result->reason, sizeof(result->reason), "CHALLENGE:%s",
challenge_id);
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
char challenge_msg[256];
sprintf(challenge_msg,
"VALIDATOR_DEBUG: STEP 4 PASSED - Challenge generated: %.16s...\n",
challenge_id);
return NOSTR_SUCCESS;
}
/////////////////////////////////////////////////////////////////////
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
/////////////////////////////////////////////////////////////////////
// Check if authentication is disabled first (regardless of header presence)
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
"disabled, allowing request\n");
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication disabled");
return NOSTR_SUCCESS;
}
// Check if this is a BUD-09 report request - allow anonymous reporting
if (request->operation && strcmp(request->operation, "report") == 0) {
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "BUD-09 report request - bypassing auth for anonymous reporting");
validator_debug_log("VALIDATOR_DEBUG: BUD-09 report detected, bypassing authentication\n");
return NOSTR_SUCCESS;
}
// Check if authentication header is provided (required for non-report operations)
if (!request->auth_header) {
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Authentication required but not provided");
return NOSTR_SUCCESS;
}
char header_msg[110];
sprintf(header_msg,
"VALIDATOR_DEBUG: STEP 4 PASSED - Auth header provided: %.50s...\n",
request->auth_header);
// 4. Authorization Header Parsing - Extract base64-encoded Nostr event
// Format: "Authorization: Nostr <base64>"
// Early exit: Invalid base64 or malformed header rejected immediately
char event_json[4096];
int parse_result = parse_authorization_header(request->auth_header,
event_json, sizeof(event_json));
if (parse_result != NOSTR_SUCCESS) {
char parse_msg[256];
sprintf(parse_msg,
"VALIDATOR_DEBUG: STEP 5 FAILED - Failed to parse authorization "
"header (error=%d)\n",
parse_result);
result->valid = 0;
result->error_code = parse_result;
strcpy(result->reason, "Invalid authorization header format. Must be 'Nostr <base64-encoded-event>'");
return NOSTR_SUCCESS;
}
char parse_success_msg[512];
sprintf(parse_success_msg,
"VALIDATOR_DEBUG: STEP 5 PASSED - Authorization header parsed, JSON: "
"%.100s...\n",
event_json);
// 5. JSON Parsing - Parse Nostr event JSON using cJSON
// Early exit: Invalid JSON rejected before signature verification
cJSON *event = cJSON_Parse(event_json);
if (!event) {
result->valid = 0;
result->error_code = NOSTR_ERROR_EVENT_INVALID_CONTENT;
strcpy(result->reason, "Invalid JSON in authorization header. Ensure event is properly formatted JSON.");
return NOSTR_SUCCESS;
}
// 6. Nostr Event Structure Validation - Validate required fields
// Early exit: Invalid structure rejected before expensive crypto
// operations
int validation_result = nostr_validate_event(event);
if (validation_result != NOSTR_SUCCESS) {
char validation_msg[256];
sprintf(validation_msg,
"VALIDATOR_DEBUG: STEP 7 FAILED - NOSTR event validation failed "
"(error=%d)\n",
validation_result);
result->valid = 0;
result->error_code = validation_result;
// Map event validation errors to detailed messages
switch (validation_result) {
case NOSTR_ERROR_EVENT_INVALID_STRUCTURE:
strcpy(result->reason, "Event structure invalid. Missing required fields: id, pubkey, created_at, kind, tags, content, sig");
break;
case NOSTR_ERROR_EVENT_INVALID_ID:
strcpy(result->reason, "Event ID verification failed. Check event serialization and hash calculation.");
break;
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
strcpy(result->reason, "Invalid pubkey format. Must be 64-character hex string.");
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strcpy(result->reason, "Event signature verification failed. Check private key and signing process.");
break;
case NOSTR_ERROR_EVENT_INVALID_CREATED_AT:
strcpy(result->reason, "Invalid created_at timestamp. Must be valid Unix timestamp.");
break;
case NOSTR_ERROR_EVENT_INVALID_KIND:
strcpy(result->reason, "Invalid event kind. Must be valid integer.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Invalid tags format. Tags must be array of string arrays.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Invalid content format. Content must be valid string.");
break;
default:
snprintf(result->reason, sizeof(result->reason),
"NOSTR event validation failed (error code: %d). Check event structure and format.",
validation_result);
break;
}
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
// 11. Public Key Extraction (Both Paths)
// Extract validated public key for rule evaluation
char extracted_pubkey[65] = {0};
int extract_result = extract_pubkey_from_event(event, extracted_pubkey,
sizeof(extracted_pubkey));
if (extract_result != NOSTR_SUCCESS) {
char extract_msg[256];
sprintf(extract_msg,
"VALIDATOR_DEBUG: STEP 8 FAILED - Failed to extract pubkey from "
"event (error=%d)\n",
extract_result);
result->valid = 0;
result->error_code = extract_result;
strcpy(result->reason, "Failed to extract public key from event. Pubkey must be 64-character hex string.");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
char extract_success_msg[256];
sprintf(extract_success_msg,
"VALIDATOR_DEBUG: STEP 8 PASSED - Extracted pubkey: %s\n",
extracted_pubkey);
/////////////////////////////////////////////////////////////////////
// EVENT KIND ROUTING - Dual Authentication Support
/////////////////////////////////////////////////////////////////////
// Kind 22242 (NIP-42): Route to NIP-42 challenge validation
// Kind 24242 (Blossom): Route to Blossom operation validation
// Other Kinds: Skip Nostr validation, proceed to rules
// Invalid Kind: Reject immediately
// Get event kind to determine authentication method
cJSON *kind_json = cJSON_GetObjectItem(event, "kind");
int event_kind = 0;
if (kind_json && cJSON_IsNumber(kind_json)) {
event_kind = cJSON_GetNumberValue(kind_json);
}
char kind_msg[256];
sprintf(kind_msg, "VALIDATOR_DEBUG: STEP 9 PASSED - Event kind: %d\n",
event_kind);
// Variable to store expected hash from Blossom events for Phase 4 validation
char expected_hash_from_event[65] = {0};
/////////////////////////////////////////////////////////////////////
// NIP42
/////////////////////////////////////////////////////////////////////
if (event_kind == NOSTR_NIP42_AUTH_EVENT_KIND) {
// 8. NIP-42 Challenge Validation (Kind 22242 Only ~500μs)
// Validate relay tag, verify challenge tag, check expiration
char nip42_msg[256];
sprintf(nip42_msg,
"VALIDATOR_DEBUG: STEP 10 - Processing NIP-42 authentication (kind "
"%d)\n",
NOSTR_NIP42_AUTH_EVENT_KIND);
validator_debug_log(nip42_msg);
// NIP-42 authentication (kind 22242)
if (!request->nip42_enabled || g_auth_cache.nip42_mode == 0) {
validator_debug_log("VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 "
"authentication is disabled\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_NIP42_DISABLED;
strcpy(result->reason, "NIP-42 authentication is disabled");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
// Extract challenge from event according to NIP-42 spec (tags only)
const char *challenge_for_validation = request->challenge_id;
if (!challenge_for_validation) {
// Look for challenge in tags (NIP-42 spec compliant)
cJSON *tags_json = cJSON_GetObjectItem(event, "tags");
if (tags_json && cJSON_IsArray(tags_json)) {
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags_json) {
if (!cJSON_IsArray(tag))
continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name))
continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "challenge") == 0) {
cJSON *challenge_value = cJSON_GetArrayItem(tag, 1);
if (challenge_value && cJSON_IsString(challenge_value)) {
const char *challenge_from_tag = cJSON_GetStringValue(challenge_value);
if (challenge_from_tag && strlen(challenge_from_tag) > 0) {
// NIP-42 doesn't specify a fixed length, so accept any reasonable length
size_t challenge_len = strlen(challenge_from_tag);
if (challenge_len >= 8 && challenge_len <= 128) { // Reasonable bounds
// Basic validation - should be hex-like
int valid_chars = 1;
for (size_t i = 0; i < challenge_len; i++) {
char c = challenge_from_tag[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) {
valid_chars = 0;
break;
}
}
if (valid_chars) {
challenge_for_validation = challenge_from_tag;
char extract_msg[256];
sprintf(extract_msg,
"VALIDATOR_DEBUG: STEP 10 - Extracted challenge from event "
"tag: %.16s...\n",
challenge_for_validation);
validator_debug_log(extract_msg);
break; // Found it, stop looking
}
}
}
}
}
}
}
}
if (!request->request_url || !challenge_for_validation) {
validator_debug_log(
"VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 requires request_url and "
"challenge (from event tags)\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
strcpy(result->reason, "NIP-42 authentication requires request_url and challenge in event tags");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
int nip42_result = validate_nip42_event(event, request->request_url,
challenge_for_validation);
if (nip42_result != NOSTR_SUCCESS) {
char nip42_fail_msg[256];
sprintf(nip42_fail_msg,
"VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 validation failed "
"(error=%d)\n",
nip42_result);
validator_debug_log(nip42_fail_msg);
result->valid = 0;
result->error_code = nip42_result;
// Map specific NIP-42 error codes to detailed error messages
switch (nip42_result) {
case NOSTR_ERROR_NIP42_INVALID_CHALLENGE:
strcpy(result->reason, "Challenge not found or invalid. Request a new challenge from /auth endpoint.");
break;
case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED:
strcpy(result->reason, "Challenge has expired. Request a new challenge from /auth endpoint.");
break;
case NOSTR_ERROR_NIP42_URL_MISMATCH:
strcpy(result->reason, "Relay URL in auth event does not match server. Use 'ginxsom' as relay value.");
break;
case NOSTR_ERROR_NIP42_TIME_TOLERANCE:
strcpy(result->reason, "Auth event timestamp is outside acceptable time window. Check system clock.");
break;
case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID:
strcpy(result->reason, "NIP-42 auth event structure is invalid. Verify event format and required tags.");
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strcpy(result->reason, "Event signature verification failed. Check private key and event serialization.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Event content is invalid. Challenge must be in event tags according to NIP-42.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Required tags missing. Auth event must include 'relay' and 'expiration' tags.");
break;
default:
snprintf(result->reason, sizeof(result->reason),
"NIP-42 authentication failed (error code: %d). Check event structure and signature.",
nip42_result);
break;
}
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
validator_debug_log(
"VALIDATOR_DEBUG: STEP 10 PASSED - NIP-42 authentication succeeded\n");
strcpy(result->reason, "NIP-42 authentication passed");
} else if (event_kind == 24242) {
// 10. Operation-Specific Validation (Kind 24242 Only)
// Verify operation authorization, check required tags, validate
// expiration Early exit: Expired or mismatched events rejected
validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Blossom "
"authentication (kind 24242)\n");
// Blossom protocol authentication (kind 24242) - ALWAYS validate kind 24242 events
char blossom_valid_msg[512];
sprintf(blossom_valid_msg,
"VALIDATOR_DEBUG: Validating Blossom event for operation='%s', "
"hash='%s'\n",
request->operation ? request->operation : "NULL",
request->resource_hash ? request->resource_hash : "NULL");
validator_debug_log(blossom_valid_msg);
int blossom_result = validate_blossom_event(event, request->resource_hash,
request->operation);
if (blossom_result != NOSTR_SUCCESS) {
char blossom_fail_msg[256];
sprintf(blossom_fail_msg,
"VALIDATOR_DEBUG: STEP 10 FAILED - Blossom validation failed "
"(error=%d)\n",
blossom_result);
validator_debug_log(blossom_fail_msg);
result->valid = 0;
result->error_code = blossom_result;
// Map specific Blossom error codes to detailed error messages
switch (blossom_result) {
case NOSTR_ERROR_EVENT_EXPIRED:
strcpy(result->reason, "Authorization event has expired. Create a new signed event with future expiration.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Event missing required tags. Blossom events need 't' (method) and 'x' tags.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Invalid or missing Blossom tags. Check 't' tag matches operation and 'x' tag matches file hash.");
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strcpy(result->reason, "Event signature verification failed. Check private key and event serialization.");
break;
case NOSTR_ERROR_EVENT_INVALID_KIND:
strcpy(result->reason, "Invalid event kind. Blossom authorization events must use kind 24242.");
break;
default:
snprintf(result->reason, sizeof(result->reason),
"Blossom event does not authorize this operation (error code: %d). Check tags and expiration.",
blossom_result);
break;
}
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
// Extract expected hash from event 'x' tag for Phase 4 use
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name)) continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "x") == 0) {
cJSON *hash_value = cJSON_GetArrayItem(tag, 1);
if (hash_value && cJSON_IsString(hash_value)) {
const char *event_hash = cJSON_GetStringValue(hash_value);
if (event_hash && strlen(event_hash) == 64) {
strncpy(expected_hash_from_event, event_hash, 64);
expected_hash_from_event[64] = '\0';
break;
}
}
}
}
}
validator_debug_log(
"VALIDATOR_DEBUG: STEP 10 PASSED - Blossom authentication succeeded\n");
strcpy(result->reason, "Blossom authentication passed");
} else if (event_kind == 33335) {
// 10. Admin/Configuration Event Validation (Kind 33335)
// Verify admin authorization, check required tags, validate expiration
validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Admin/Configuration "
"authentication (kind 33335)\n");
char admin_valid_msg[512];
sprintf(admin_valid_msg,
"VALIDATOR_DEBUG: Validating Admin event for operation='%s', "
"endpoint='%s'\n",
request->operation ? request->operation : "NULL",
request->resource_hash ? request->resource_hash : "admin_api");
validator_debug_log(admin_valid_msg);
// For admin operations, we pass the HTTP method and API endpoint
const char *admin_method = NULL;
const char *admin_endpoint = NULL;
// Extract method and endpoint from request context
// For admin API, we need to get the actual HTTP method and full endpoint
if (request->operation && strcmp(request->operation, "admin") == 0) {
// Admin events should contain method and endpoint tags that match the actual request
// We don't enforce specific values here - let the event tags drive the validation
admin_method = NULL; // Let validation check event tags without enforcing specific method
admin_endpoint = NULL; // Let validation check event tags without enforcing specific endpoint
}
int admin_result = validate_admin_event(event, admin_method, admin_endpoint);
if (admin_result != NOSTR_SUCCESS) {
char admin_fail_msg[256];
sprintf(admin_fail_msg,
"VALIDATOR_DEBUG: STEP 10 FAILED - Admin validation failed "
"(error=%d)\n",
admin_result);
validator_debug_log(admin_fail_msg);
result->valid = 0;
result->error_code = admin_result;
// Map specific Admin error codes to detailed error messages
switch (admin_result) {
case NOSTR_ERROR_EVENT_EXPIRED:
strcpy(result->reason, "Admin authorization event has expired. Create a new signed event with future expiration.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Admin event missing required tags. Admin events need 'method' and 'endpoint' tags.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Invalid or missing admin tags. Check 'method' tag matches HTTP method and 'endpoint' tag is valid.");
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strcpy(result->reason, "Admin event signature verification failed. Check private key and event serialization.");
break;
case NOSTR_ERROR_EVENT_INVALID_KIND:
strcpy(result->reason, "Invalid event kind. Admin authorization events must use kind 33335.");
break;
case NOSTR_ERROR_AUTH_REQUIRED:
strcpy(result->reason, "Admin access denied. Pubkey is not authorized for admin operations.");
break;
default:
snprintf(result->reason, sizeof(result->reason),
"Admin event does not authorize this operation (error code: %d). Check tags and admin permissions.",
admin_result);
break;
}
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
validator_debug_log(
"VALIDATOR_DEBUG: STEP 10 PASSED - Admin authentication succeeded\n");
strcpy(result->reason, "Admin authentication passed");
} else {
char unsupported_msg[256];
sprintf(unsupported_msg,
"VALIDATOR_DEBUG: STEP 10 FAILED - Unsupported event kind: %d\n",
event_kind);
validator_debug_log(unsupported_msg);
result->valid = 0;
result->error_code = NOSTR_ERROR_EVENT_INVALID_KIND;
snprintf(result->reason, sizeof(result->reason),
"Unsupported event kind %d for authentication. Use kind 22242 for NIP-42, kind 24242 for Blossom, or kind 33335 for Admin.",
event_kind);
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
// Copy validated pubkey to result
if (strlen(extracted_pubkey) == 64) {
strncpy(result->pubkey, extracted_pubkey, 64);
result->pubkey[64] = '\0';
validator_debug_log(
"VALIDATOR_DEBUG: STEP 11 PASSED - Pubkey copied to result\n");
} else {
char pubkey_warning_msg[256];
sprintf(pubkey_warning_msg,
"VALIDATOR_DEBUG: STEP 11 WARNING - Invalid pubkey length: %zu\n",
strlen(extracted_pubkey));
validator_debug_log(pubkey_warning_msg);
}
cJSON_Delete(event);
// STEP 12 PASSED: Protocol validation complete - continue to database rule
// evaluation
validator_debug_log("VALIDATOR_DEBUG: STEP 12 PASSED - Protocol validation "
"complete, proceeding to rule evaluation\n");
/////////////////////////////////////////////////////////////////////
// PHASE 3: AUTHENTICATION RULES (Database Queries ~500μs)
/////////////////////////////////////////////////////////////////////
// 12. Rules System Check - Quick config check if auth rules enabled
// Early exit: If disabled, allow request immediately
// Check if authentication is disabled first (regardless of header presence)
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
"disabled, allowing request\n");
strcpy(result->reason, "Authentication disabled");
return NOSTR_SUCCESS;
}
// 13. Cache Lookup - Check SQLite cache for previous decision
// Early exit: Cache hit returns cached decision (5-minute TTL ~100μs)
/////////////////////////////////////////////////////////////////////
// RULE EVALUATION ENGINE (Priority Order)
/////////////////////////////////////////////////////////////////////
// a. Pubkey Blacklist (highest priority) - Immediate denial if matched
// b. Hash Blacklist - Block specific content hashes
// c. MIME Type Blacklist - Block dangerous file types
// d. File Size Limits - Enforce upload size restrictions
// e. Pubkey Whitelist - Allow specific users (only if not denied above)
// f. MIME Type Whitelist - Allow specific file types
validator_debug_log("VALIDATOR_DEBUG: STEP 13 PASSED - Auth rules enabled, "
"checking database rules\n");
// Check database rules for authorization
// For Blossom uploads, use hash from event 'x' tag instead of URI
const char *hash_for_rules = request->resource_hash;
if (event_kind == 24242 && strlen(expected_hash_from_event) == 64) {
hash_for_rules = expected_hash_from_event;
char hash_msg[256];
sprintf(hash_msg, "VALIDATOR_DEBUG: Using hash from Blossom event for rules: %.16s...\n", hash_for_rules);
validator_debug_log(hash_msg);
}
int rules_result = check_database_auth_rules(
extracted_pubkey, request->operation, hash_for_rules, request->mime_type);
if (rules_result != NOSTR_SUCCESS) {
validator_debug_log(
"VALIDATOR_DEBUG: STEP 14 FAILED - Database rules denied request\n");
result->valid = 0;
result->error_code = rules_result;
// Determine specific failure reason based on rules evaluation
if (rules_result == NOSTR_ERROR_AUTH_REQUIRED) {
// This can be pubkey blacklist or whitelist violation - set generic
// message The specific reason will be detailed in the database check
// function
strcpy(result->reason, "Request denied by authorization rules");
} else {
strcpy(result->reason, "Authorization error");
}
return NOSTR_SUCCESS;
}
validator_debug_log(
"VALIDATOR_DEBUG: STEP 14 PASSED - Database rules allow request\n");
// 15. Whitelist Default Denial - If whitelist rules exist but none matched,
// deny
// Prevents whitelist bypass attacks
// 16. Cache Storage - Store decision for future requests (5-minute TTL)
/////////////////////////////////////////////////////////////////////
// PHASE 4: FILE CONTENT VALIDATION (Upload Operations Only ~100ms)
/////////////////////////////////////////////////////////////////////
// Only run expensive file processing AFTER all auth checks pass
// This ensures blacklisted users never trigger file I/O
if (event_kind == 24242 && request->operation &&
strcmp(request->operation, "upload") == 0 &&
strlen(expected_hash_from_event) == 64) {
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 - Starting file content validation for Blossom upload\n");
char phase4_msg[256];
sprintf(phase4_msg, "VALIDATOR_DEBUG: PHASE 4 - Expected hash: %.16s...\n", expected_hash_from_event);
validator_debug_log(phase4_msg);
// Get content length from request
long content_length = request->file_size;
if (content_length <= 0 || content_length > 100 * 1024 * 1024) { // 100MB limit
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Invalid content length\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Invalid file size for upload validation");
return NOSTR_SUCCESS;
}
// Allocate buffer for file data
unsigned char *file_data = malloc(content_length);
if (!file_data) {
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Memory allocation failed\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Memory allocation failed for file validation");
return NOSTR_SUCCESS;
}
// Read file data from stdin
size_t bytes_read = fread(file_data, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(file_data);
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Failed to read complete file data\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Failed to read complete file data for validation");
return NOSTR_SUCCESS;
}
char read_msg[256];
sprintf(read_msg, "VALIDATOR_DEBUG: PHASE 4 - Read %zu bytes from stdin\n", bytes_read);
validator_debug_log(read_msg);
// Calculate SHA-256 hash of file content
unsigned char hash_bytes[32];
if (nostr_sha256(file_data, bytes_read, hash_bytes) != NOSTR_SUCCESS) {
free(file_data);
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Hash calculation failed\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Failed to calculate file hash");
return NOSTR_SUCCESS;
}
// Convert hash to hex string
char calculated_hash[65];
nostr_bytes_to_hex(hash_bytes, 32, calculated_hash);
char hash_msg[256];
sprintf(hash_msg, "VALIDATOR_DEBUG: PHASE 4 - Calculated hash: %.16s...\n", calculated_hash);
validator_debug_log(hash_msg);
// Compare hashes
if (strcmp(calculated_hash, expected_hash_from_event) != 0) {
free(file_data);
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Hash mismatch detected\n");
char mismatch_msg[512];
sprintf(mismatch_msg, "VALIDATOR_DEBUG: PHASE 4 - Expected: %.16s..., Got: %.16s...\n",
expected_hash_from_event, calculated_hash);
validator_debug_log(mismatch_msg);
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "File hash mismatch. Uploaded file does not match hash in authorization event.");
return NOSTR_SUCCESS;
}
// Hash matches - store file data in result for main handler to use
result->file_data = file_data;
result->file_size = bytes_read;
result->owns_file_data = 1;
strncpy(result->expected_hash, expected_hash_from_event, 64);
result->expected_hash[64] = '\0';
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 PASSED - File hash validation successful, file data stored in result\n");
}
// All validations passed
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
validator_debug_log("VALIDATOR_DEBUG: STEP 15 PASSED - All validations "
"complete, request ALLOWED\n");
return NOSTR_SUCCESS;
}
/**
* Generate NIP-42 challenge for clients
*/
int nostr_request_validator_generate_nip42_challenge(void *challenge_struct,
const char *client_ip) {
// Mark client_ip as unused to suppress warning - it's for future enhancement
(void)client_ip;
// Use nostr_core_lib NIP-42 functionality
char challenge_id[65];
int result = nostr_nip42_generate_challenge(challenge_id, 32);
if (result != NOSTR_SUCCESS) {
return result;
}
// Fill challenge structure (assuming it's a compatible structure)
// This is a simplified implementation - adjust based on actual structure
// needs
if (challenge_struct) {
// Cast to appropriate structure and fill fields
// For now, just return success
}
return NOSTR_SUCCESS;
}
/**
* Get the last rule violation type for status code mapping
*/
const char *nostr_request_validator_get_last_violation_type(void) {
return g_last_rule_violation.violation_type;
}
/**
* Clear the last rule violation details
*/
void nostr_request_validator_clear_violation(void) {
memset(&g_last_rule_violation, 0, sizeof(g_last_rule_violation));
}
/**
* Cleanup request validator resources
*/
void ginxsom_request_validator_cleanup(void) {
g_validator_initialized = 0;
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
nostr_request_validator_clear_violation();
}
/**
* Free file data allocated by validator
*/
void nostr_request_result_free_file_data(nostr_request_result_t *result) {
if (result && result->file_data && result->owns_file_data) {
free(result->file_data);
result->file_data = NULL;
result->file_size = 0;
result->owns_file_data = 0;
}
}
//=============================================================================
// HELPER FUNCTIONS
//=============================================================================
/**
* Get cache timeout from environment variable or default
*/
static int get_cache_timeout(void) {
char *no_cache = getenv("GINX_NO_CACHE");
char *cache_timeout = getenv("GINX_CACHE_TIMEOUT");
if (no_cache && strcmp(no_cache, "1") == 0) {
return 0; // No caching
}
if (cache_timeout) {
int timeout = atoi(cache_timeout);
return (timeout >= 0) ? timeout : 300; // Use provided value or default
}
return 300; // Default 5 minutes
}
/**
* Force cache refresh - invalidates current cache
*/
void nostr_request_validator_force_cache_refresh(void) {
g_auth_cache.cache_valid = 0;
g_auth_cache.cache_expires = 0;
validator_debug_log("VALIDATOR: Cache forcibly invalidated\n");
}
/**
* Reload authentication configuration from unified config table
*/
static int reload_auth_config(void) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
// Clear cache
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
// Open database
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log("VALIDATOR: Could not open database\n");
// Use defaults
g_auth_cache.auth_required = 0;
g_auth_cache.max_file_size = 104857600; // 100MB
g_auth_cache.admin_enabled = 0;
g_auth_cache.nip42_mode = 1; // Optional
int cache_timeout = get_cache_timeout();
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
g_auth_cache.cache_valid = 1;
return NOSTR_SUCCESS;
}
// Load configuration values from unified config table
const char *config_sql =
"SELECT key, value FROM config WHERE key IN ('require_auth', "
"'auth_rules_enabled', 'max_file_size', 'admin_enabled', 'admin_pubkey', "
"'nip42_require_auth', 'nip42_challenge_timeout', "
"'nip42_time_tolerance')";
rc = sqlite3_prepare_v2(db, config_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *key = (const char *)sqlite3_column_text(stmt, 0);
const char *value = (const char *)sqlite3_column_text(stmt, 1);
if (!key || !value)
continue;
if (strcmp(key, "require_auth") == 0) {
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "auth_rules_enabled") == 0) {
// Override auth_required with auth_rules_enabled if present (higher
// priority)
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "max_file_size") == 0) {
g_auth_cache.max_file_size = atol(value);
} else if (strcmp(key, "admin_enabled") == 0) {
g_auth_cache.admin_enabled = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "admin_pubkey") == 0) {
strncpy(g_auth_cache.admin_pubkey, value,
sizeof(g_auth_cache.admin_pubkey) - 1);
} else if (strcmp(key, "nip42_require_auth") == 0) {
if (strcmp(value, "false") == 0) {
g_auth_cache.nip42_mode = 0; // Disabled
} else if (strcmp(value, "required") == 0) {
g_auth_cache.nip42_mode = 2; // Required
} else if (strcmp(value, "true") == 0) {
g_auth_cache.nip42_mode = 1; // Optional/Enabled
} else {
g_auth_cache.nip42_mode = 1; // Default to Optional/Enabled
}
} else if (strcmp(key, "nip42_challenge_timeout") == 0) {
g_auth_cache.nip42_challenge_timeout = atoi(value);
} else if (strcmp(key, "nip42_time_tolerance") == 0) {
g_auth_cache.nip42_time_tolerance = atoi(value);
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
// Set cache expiration with environment variable support
int cache_timeout = get_cache_timeout();
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
g_auth_cache.cache_valid = 1;
// Set defaults for missing values
if (g_auth_cache.max_file_size == 0) {
g_auth_cache.max_file_size = 104857600; // 100MB
}
// Debug logging
fprintf(stderr,
"VALIDATOR: Configuration loaded from unified config table - "
"auth_required: %d, max_file_size: %ld, nip42_mode: %d, "
"cache_timeout: %d\n",
g_auth_cache.auth_required, g_auth_cache.max_file_size,
g_auth_cache.nip42_mode, cache_timeout);
fprintf(stderr,
"VALIDATOR: NIP-42 mode details - nip42_mode=%d (0=disabled, "
"1=optional/enabled, 2=required)\n",
g_auth_cache.nip42_mode);
return NOSTR_SUCCESS;
}
/**
* Parse NOSTR authorization header (base64 decode)
*/
static int parse_authorization_header(const char *auth_header, char *event_json,
size_t json_size) {
if (!auth_header || !event_json) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check for "Nostr " prefix (case-insensitive)
const char *prefix = "nostr ";
size_t prefix_len = strlen(prefix);
if (strncasecmp(auth_header, prefix, prefix_len) != 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Extract base64 encoded event after "Nostr "
const char *base64_event = auth_header + prefix_len;
// Decode base64 to JSON using nostr_core_lib base64 decode
unsigned char decoded_buffer[4096];
size_t decoded_len = base64_decode(base64_event, decoded_buffer);
if (decoded_len == 0 || decoded_len >= json_size) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Copy decoded JSON to output buffer
memcpy(event_json, decoded_buffer, decoded_len);
event_json[decoded_len] = '\0';
return NOSTR_SUCCESS;
}
/**
* Extract pubkey from validated NOSTR event
*/
static int extract_pubkey_from_event(cJSON *event, char *pubkey_buffer,
size_t buffer_size) {
if (!event || !pubkey_buffer || buffer_size < 65) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize buffer to prevent corruption
memset(pubkey_buffer, 0, buffer_size);
cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_json || !cJSON_IsString(pubkey_json)) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
const char *pubkey = cJSON_GetStringValue(pubkey_json);
if (!pubkey) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
// Check the raw pubkey string before validation
size_t pubkey_len = strlen(pubkey);
if (pubkey_len != 64) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
// Validate that pubkey contains only hex characters before copying
for (int i = 0; i < 64; i++) {
char c = pubkey[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
}
// Safe copy with explicit length and null termination
memcpy(pubkey_buffer, pubkey, 64);
pubkey_buffer[64] = '\0';
return NOSTR_SUCCESS;
}
/**
* Validate Blossom protocol event (kind 24242)
*/
static int validate_blossom_event(cJSON *event, const char *expected_hash,
const char *method) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check event kind (must be 24242 for Blossom operations)
cJSON *kind_json = cJSON_GetObjectItem(event, "kind");
if (!kind_json || !cJSON_IsNumber(kind_json)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
int kind = cJSON_GetNumberValue(kind_json);
if (kind != 24242) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// ALL Blossom events (kind 24242) must have proper tag structure
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Track what we find in the event tags
int has_t_tag = 0;
int has_x_tag = 0;
int method_matches = (method == NULL); // If no expected method, consider it matched
int hash_matches = (expected_hash == NULL); // If no expected hash, consider it matched
time_t expiration = 0;
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag))
continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name))
continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "t") == 0) {
has_t_tag = 1;
cJSON *method_value = cJSON_GetArrayItem(tag, 1);
if (method_value && cJSON_IsString(method_value)) {
const char *event_method = cJSON_GetStringValue(method_value);
if (method && strcmp(event_method, method) == 0) {
method_matches = 1;
}
}
} else if (strcmp(tag_name_str, "x") == 0) {
has_x_tag = 1;
cJSON *hash_value = cJSON_GetArrayItem(tag, 1);
if (hash_value && cJSON_IsString(hash_value)) {
const char *event_hash = cJSON_GetStringValue(hash_value);
if (expected_hash && strcmp(event_hash, expected_hash) == 0) {
hash_matches = 1;
}
}
} else if (strcmp(tag_name_str, "expiration") == 0) {
cJSON *exp_value = cJSON_GetArrayItem(tag, 1);
if (exp_value && cJSON_IsString(exp_value)) {
expiration = (time_t)atol(cJSON_GetStringValue(exp_value));
}
}
}
// Blossom events MUST have both 't' and 'x' tags
if (!has_t_tag || !has_x_tag) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// If we have expected values, they must match
if (!method_matches || !hash_matches) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Check expiration
time_t now = time(NULL);
if (expiration > 0 && now > expiration) {
return NOSTR_ERROR_EVENT_EXPIRED;
}
return NOSTR_SUCCESS;
}
/**
* Check database authentication rules for the request
* Implements the 6-step rule evaluation engine from AUTH_API.md
*/
static int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash, const char *mime_type) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
if (!pubkey) {
validator_debug_log(
"VALIDATOR_DEBUG: RULES ENGINE - Missing pubkey for rule evaluation\n");
return NOSTR_ERROR_INVALID_INPUT;
}
char rules_msg[256];
sprintf(rules_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Checking rules for pubkey=%.32s..., "
"operation=%s, mime_type=%s\n",
pubkey, operation ? operation : "NULL", mime_type ? mime_type : "NULL");
validator_debug_log(rules_msg);
// Open database
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log(
"VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n");
return NOSTR_SUCCESS; // Default allow on DB error
}
// Step 1: Check pubkey blacklist (highest priority)
// Match both exact operation and wildcard '*'
const char *blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_blacklist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - "
"Pubkey blacklisted\n");
char blacklist_msg[256];
sprintf(blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(blacklist_msg);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
sprintf(g_last_rule_violation.reason, "%s: Public key blacklisted",
description ? description : "TEST_PUBKEY_BLACKLIST");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 PASSED - Pubkey "
"not blacklisted\n");
// Step 2: Check hash blacklist
if (resource_hash) {
// Match both exact operation and wildcard '*'
const char *hash_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'hash_blacklist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - "
"Hash blacklisted\n");
char hash_blacklist_msg[256];
sprintf(
hash_blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(hash_blacklist_msg);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
sprintf(g_last_rule_violation.reason, "%s: File hash blacklisted",
description ? description : "TEST_HASH_BLACKLIST");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 PASSED - Hash "
"not blacklisted\n");
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 SKIPPED - No "
"resource hash provided\n");
}
// Step 3: Check MIME type blacklist
if (mime_type) {
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
const char *mime_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'mime_blacklist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - "
"MIME type blacklisted\n");
char mime_blacklist_msg[256];
sprintf(
mime_blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - MIME blacklist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(mime_blacklist_msg);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "mime_blacklist");
sprintf(g_last_rule_violation.reason, "%s: MIME type blacklisted",
description ? description : "TEST_MIME_BLACKLIST");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - MIME "
"type not blacklisted\n");
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 SKIPPED - No "
"MIME type provided\n");
}
// Step 4: Check pubkey whitelist
// Match both exact operation and wildcard '*'
const char *whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_whitelist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - "
"Pubkey whitelisted\n");
char whitelist_msg[256];
sprintf(whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(whitelist_msg);
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_SUCCESS; // Allow whitelisted pubkey
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - Pubkey "
"not whitelisted\n");
// Step 5: Check MIME type whitelist (only if not already denied)
if (mime_type) {
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
const char *mime_whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'mime_whitelist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - "
"MIME type whitelisted\n");
char mime_whitelist_msg[256];
sprintf(mime_whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(mime_whitelist_msg);
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_SUCCESS; // Allow whitelisted MIME type
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 FAILED - MIME "
"type not whitelisted\n");
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 SKIPPED - No "
"MIME type provided\n");
}
// Step 6: Check if any MIME whitelist rules exist - if yes, deny by default
// Match both exact operation and wildcard '*'
const char *mime_whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'mime_whitelist' "
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int mime_whitelist_count = sqlite3_column_int(stmt, 0);
if (mime_whitelist_count > 0) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 FAILED - "
"MIME whitelist exists but type not in it\n");
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "mime_whitelist_violation");
strcpy(g_last_rule_violation.reason,
"MIME type not whitelisted for this operation");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 PASSED - No "
"MIME whitelist restrictions apply\n");
// Step 7: Check if any whitelist rules exist - if yes, deny by default
// Match both exact operation and wildcard '*'
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int whitelist_count = sqlite3_column_int(stmt, 0);
if (whitelist_count > 0) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 FAILED - "
"Whitelist exists but pubkey not in it\n");
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "whitelist_violation");
strcpy(g_last_rule_violation.reason,
"Public key not whitelisted for this operation");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 PASSED - No "
"whitelist restrictions apply\n");
sqlite3_close(db);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 7 PASSED - All "
"rule checks completed, default ALLOW\n");
return NOSTR_SUCCESS; // Default allow if no restrictive rules matched
}
/**
* Validate NIP-42 authentication event (kind 22242)
*/
static int validate_nip42_event(cJSON *event, const char *relay_url,
const char *challenge_id) {
if (!event || !relay_url || !challenge_id) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check event kind (must be 22242 for NIP-42)
cJSON *kind_json = cJSON_GetObjectItem(event, "kind");
if (!kind_json || !cJSON_IsNumber(kind_json)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
int kind = cJSON_GetNumberValue(kind_json);
if (kind != NOSTR_NIP42_AUTH_EVENT_KIND) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Validate that the challenge exists and is not expired
int challenge_result = validate_challenge(challenge_id);
if (challenge_result != NOSTR_SUCCESS) {
return challenge_result;
}
// Use the existing NIP-42 verification from nostr_core_lib
int verification_result =
nostr_nip42_verify_auth_event(event, challenge_id, relay_url,
g_challenge_manager.time_tolerance_seconds);
if (verification_result != NOSTR_SUCCESS) {
return verification_result;
}
return NOSTR_SUCCESS;
}
/**
* Validate Admin/Configuration event (kind 33335)
*/
static int validate_admin_event(cJSON *event, const char *method, const char *endpoint) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check event kind (must be 33335 for Admin operations)
cJSON *kind_json = cJSON_GetObjectItem(event, "kind");
if (!kind_json || !cJSON_IsNumber(kind_json)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
int kind = cJSON_GetNumberValue(kind_json);
if (kind != 33335) {
return NOSTR_ERROR_EVENT_INVALID_KIND;
}
// Get pubkey for admin authorization check
cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_json || !cJSON_IsString(pubkey_json)) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
const char *event_pubkey = cJSON_GetStringValue(pubkey_json);
if (!event_pubkey || strlen(event_pubkey) != 64) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
// Check if pubkey is authorized for admin operations
if (strlen(g_auth_cache.admin_pubkey) == 64) {
if (strcmp(event_pubkey, g_auth_cache.admin_pubkey) != 0) {
validator_debug_log("VALIDATOR_DEBUG: Admin pubkey mismatch - access denied\n");
return NOSTR_ERROR_AUTH_REQUIRED;
}
} else {
validator_debug_log("VALIDATOR_DEBUG: No admin pubkey configured - access denied\n");
return NOSTR_ERROR_AUTH_REQUIRED;
}
// Validate admin event tag structure
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Track what we find in the event tags
int has_method_tag = 0;
int has_endpoint_tag = 0;
int method_matches = (method == NULL); // If no expected method, consider it matched
int endpoint_matches = (endpoint == NULL); // If no expected endpoint, consider it matched
time_t expiration = 0;
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag))
continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name))
continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "method") == 0) {
has_method_tag = 1;
cJSON *method_value = cJSON_GetArrayItem(tag, 1);
if (method_value && cJSON_IsString(method_value)) {
const char *event_method = cJSON_GetStringValue(method_value);
if (method && strcmp(event_method, method) == 0) {
method_matches = 1;
}
}
} else if (strcmp(tag_name_str, "endpoint") == 0) {
has_endpoint_tag = 1;
cJSON *endpoint_value = cJSON_GetArrayItem(tag, 1);
if (endpoint_value && cJSON_IsString(endpoint_value)) {
const char *event_endpoint = cJSON_GetStringValue(endpoint_value);
// For endpoint matching, allow prefix matching for API endpoints
if (endpoint && strncmp(event_endpoint, endpoint, strlen(endpoint)) == 0) {
endpoint_matches = 1;
}
}
} else if (strcmp(tag_name_str, "expiration") == 0) {
cJSON *exp_value = cJSON_GetArrayItem(tag, 1);
if (exp_value && cJSON_IsString(exp_value)) {
expiration = (time_t)atol(cJSON_GetStringValue(exp_value));
}
}
}
// Admin events should have method and endpoint tags
if (!has_method_tag || !has_endpoint_tag) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// If we have expected values, they must match
if (!method_matches || !endpoint_matches) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
// Check expiration
time_t now = time(NULL);
if (expiration > 0 && now > expiration) {
return NOSTR_ERROR_EVENT_EXPIRED;
}
validator_debug_log("VALIDATOR_DEBUG: Admin event validation passed\n");
return NOSTR_SUCCESS;
}
//=============================================================================
// NIP-42 CHALLENGE MANAGEMENT FUNCTIONS
//=============================================================================
/**
* Generate a challenge ID using nostr_core_lib
*/
static int generate_challenge_id(char *challenge_buffer, size_t buffer_size) {
if (!challenge_buffer || buffer_size < 65) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Use nostr_core_lib to generate a random challenge
return nostr_nip42_generate_challenge(challenge_buffer, 32);
}
/**
* Clean up expired challenges from memory
*/
static void cleanup_expired_challenges(void) {
time_t now = time(NULL);
// Only cleanup if enough time has passed
if (now - g_challenge_manager.last_cleanup < CHALLENGE_CLEANUP_INTERVAL) {
return;
}
int active_count = 0;
for (int i = 0; i < g_challenge_manager.challenge_count; i++) {
if (g_challenge_manager.challenges[i].active) {
if (now > g_challenge_manager.challenges[i].expires_at) {
// Mark expired challenge as inactive
g_challenge_manager.challenges[i].active = 0;
memset(g_challenge_manager.challenges[i].challenge_id, 0,
sizeof(g_challenge_manager.challenges[i].challenge_id));
} else {
active_count++;
}
}
}
// Compact the array if we have many inactive entries
if (active_count < g_challenge_manager.challenge_count / 2 &&
active_count < MAX_CHALLENGES - 100) {
int write_idx = 0;
for (int read_idx = 0; read_idx < g_challenge_manager.challenge_count;
read_idx++) {
if (g_challenge_manager.challenges[read_idx].active) {
if (write_idx != read_idx) {
memcpy(&g_challenge_manager.challenges[write_idx],
&g_challenge_manager.challenges[read_idx],
sizeof(nip42_challenge_entry_t));
}
write_idx++;
}
}
g_challenge_manager.challenge_count = write_idx;
}
g_challenge_manager.last_cleanup = now;
char cleanup_msg[256];
sprintf(cleanup_msg, "NIP-42: Cleaned up challenges, %d active remaining\n",
active_count);
validator_debug_log(cleanup_msg);
}
/**
* Store a new challenge in memory
*/
static int store_challenge(const char *challenge_id, const char *client_ip) {
if (!challenge_id || strlen(challenge_id) == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cleanup_expired_challenges();
// Find an available slot
int slot_idx = -1;
// First, try to find an inactive slot
for (int i = 0; i < g_challenge_manager.challenge_count; i++) {
if (!g_challenge_manager.challenges[i].active) {
slot_idx = i;
break;
}
}
// If no inactive slot found, use next available if we haven't hit max
if (slot_idx == -1 && g_challenge_manager.challenge_count < MAX_CHALLENGES) {
slot_idx = g_challenge_manager.challenge_count++;
}
// If still no slot, we're full - remove oldest entry
if (slot_idx == -1) {
slot_idx = 0; // Overwrite first entry (oldest)
}
// Store the new challenge
nip42_challenge_entry_t *entry = &g_challenge_manager.challenges[slot_idx];
memset(entry, 0, sizeof(nip42_challenge_entry_t));
// Store challenge with proper length handling (up to buffer size - 1)
strncpy(entry->challenge_id, challenge_id, sizeof(entry->challenge_id) - 1);
entry->challenge_id[sizeof(entry->challenge_id) - 1] = '\0';
if (client_ip) {
strncpy(entry->client_ip, client_ip, sizeof(entry->client_ip) - 1);
entry->client_ip[sizeof(entry->client_ip) - 1] = '\0';
}
time_t now = time(NULL);
entry->created_at = now;
entry->expires_at = now + g_challenge_manager.timeout_seconds;
entry->active = 1;
char store_msg[256];
sprintf(store_msg,
"NIP-42: Stored challenge %.16s... (expires in %d seconds)\n",
challenge_id, g_challenge_manager.timeout_seconds);
validator_debug_log(store_msg);
return NOSTR_SUCCESS;
}
/**
* Validate that a challenge exists and is not expired
*/
static int validate_challenge(const char *challenge_id) {
if (!challenge_id || strlen(challenge_id) == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cleanup_expired_challenges();
time_t now = time(NULL);
for (int i = 0; i < g_challenge_manager.challenge_count; i++) {
nip42_challenge_entry_t *entry = &g_challenge_manager.challenges[i];
if (entry->active && strcmp(entry->challenge_id, challenge_id) == 0) {
if (now <= entry->expires_at) {
char validate_msg[256];
sprintf(validate_msg,
"NIP-42: Challenge %.16s... validated successfully\n",
challenge_id);
validator_debug_log(validate_msg);
return NOSTR_SUCCESS;
} else {
// Mark as expired
entry->active = 0;
validator_debug_log("NIP-42: Challenge found but expired\n");
return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED;
}
}
}
validator_debug_log("NIP-42: Challenge not found\n");
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
}
/**
* Generate and store a new NIP-42 challenge for /auth endpoint
*/
int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size,
const char *client_ip) {
if (!challenge_out || challenge_size < 65) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate challenge ID
int result = generate_challenge_id(challenge_out, challenge_size);
if (result != NOSTR_SUCCESS) {
return result;
}
// Store in challenge manager
result = store_challenge(challenge_out, client_ip);
if (result != NOSTR_SUCCESS) {
return result;
}
return NOSTR_SUCCESS;
}