Moved auth system from nostr_core_lib back into ginxsom. Still debugging but so many changes I wanted to commit.

This commit is contained in:
Your Name
2025-09-09 07:26:00 -04:00
parent 20792871f8
commit dd0d8a8b65
65 changed files with 2851 additions and 19358 deletions

840
src/request_validator.c Normal file
View File

@@ -0,0 +1,840 @@
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <sqlite3.h>
#include "../nostr_core_lib/nostr_core/nostr_common.h"
#include "../nostr_core_lib/nostr_core/nip001.h"
#include "../nostr_core_lib/nostr_core/nip042.h"
#include "../nostr_core_lib/nostr_core/utils.h"
#include "../nostr_core_lib/cjson/cJSON.h"
#include "ginxsom.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
// Database path (consistent with main.c)
#define DB_PATH "db/ginxsom.db"
//=============================================================================
// DATA STRUCTURES
//=============================================================================
// 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
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 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 check_database_auth_rules(const char* pubkey, const char* operation, const char* resource_hash);
void nostr_request_validator_clear_violation(void);
//=============================================================================
// MAIN API FUNCTIONS
//=============================================================================
/**
* Initialize the ginxsom request validator system
*/
int nostr_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;
}
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 request validation function - this is the primary entry point
*/
int nostr_validate_request(const nostr_request_t* request, nostr_request_result_t* result) {
// Clear previous violation details
nostr_request_validator_clear_violation();
// Simple test debug log
validator_debug_log("VALIDATOR_DEBUG: nostr_validate_request() was called\n");
validator_debug_log("VALIDATOR_DEBUG: Starting request validation\n");
if (!g_validator_initialized) {
validator_debug_log("VALIDATOR_DEBUG: STEP 1 FAILED - System not initialized\n");
return NOSTR_ERROR_INVALID_INPUT;
}
validator_debug_log("VALIDATOR_DEBUG: STEP 1 PASSED - System initialized\n");
if (!request || !result) {
validator_debug_log("VALIDATOR_DEBUG: STEP 2 FAILED - Invalid input parameters\n");
return NOSTR_ERROR_INVALID_INPUT;
}
validator_debug_log("VALIDATOR_DEBUG: STEP 2 PASSED - Input parameters valid\n");
// 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");
// Reload config if needed
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
validator_debug_log("VALIDATOR_DEBUG: Reloading configuration cache\n");
reload_auth_config();
}
char config_msg[256];
sprintf(config_msg, "VALIDATOR_DEBUG: STEP 3 PASSED - Configuration loaded (auth_required=%d)\n", g_auth_cache.auth_required);
validator_debug_log(config_msg);
// If no auth header provided and auth not required, allow
if (!request->auth_header) {
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - No auth required, allowing request\n");
strcpy(result->reason, "Authentication not required");
return NOSTR_SUCCESS;
} else {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 FAILED - Auth required but no header provided\n");
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);
validator_debug_log(header_msg);
// Parse authorization header
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);
validator_debug_log(parse_msg);
result->valid = 0;
result->error_code = parse_result;
strcpy(result->reason, "Failed to parse authorization header");
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);
validator_debug_log(parse_success_msg);
// Parse JSON event
cJSON* event = cJSON_Parse(event_json);
if (!event) {
validator_debug_log("VALIDATOR_DEBUG: STEP 6 FAILED - Invalid JSON in authorization\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_EVENT_INVALID_CONTENT;
strcpy(result->reason, "Invalid JSON in authorization");
return NOSTR_SUCCESS;
}
validator_debug_log("VALIDATOR_DEBUG: STEP 6 PASSED - JSON parsed successfully\n");
// Validate NOSTR event structure and signature using nostr_core_lib
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);
validator_debug_log(validation_msg);
result->valid = 0;
result->error_code = validation_result;
strcpy(result->reason, "NOSTR event validation failed");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
validator_debug_log("VALIDATOR_DEBUG: STEP 7 PASSED - NOSTR event validation succeeded\n");
// Extract pubkey for authorization checks
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);
validator_debug_log(extract_msg);
result->valid = 0;
result->error_code = extract_result;
strcpy(result->reason, "Failed to extract pubkey from event");
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);
validator_debug_log(extract_success_msg);
// 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);
validator_debug_log(kind_msg);
// Handle different authentication methods
if (event_kind == NOSTR_NIP42_AUTH_EVENT_KIND) {
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_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;
}
if (!request->relay_url || !request->challenge_id) {
validator_debug_log("VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 requires relay_url and challenge_id\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_NIP42_NOT_CONFIGURED;
strcpy(result->reason, "NIP-42 authentication requires relay_url and challenge_id");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
int nip42_result = validate_nip42_event(event, request->relay_url, request->challenge_id);
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;
strcpy(result->reason, "NIP-42 authentication failed");
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) {
validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Blossom authentication (kind 24242)\n");
// Blossom protocol authentication (kind 24242)
if (request->operation && request->resource_hash) {
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;
strcpy(result->reason, "Blossom event does not authorize this operation");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
} else {
validator_debug_log("VALIDATOR_DEBUG: Skipping Blossom validation (no operation/hash specified)\n");
}
validator_debug_log("VALIDATOR_DEBUG: STEP 10 PASSED - Blossom authentication succeeded\n");
strcpy(result->reason, "Blossom 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;
strcpy(result->reason, "Unsupported event kind for authentication");
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");
// Check if auth rules are enabled
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 13 PASSED - Auth rules disabled, allowing request\n");
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication rules disabled");
return NOSTR_SUCCESS;
}
validator_debug_log("VALIDATOR_DEBUG: STEP 13 PASSED - Auth rules enabled, checking database rules\n");
// Check database rules for authorization
int rules_result = check_database_auth_rules(extracted_pubkey, request->operation, request->resource_hash);
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");
// 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 nostr_request_validator_cleanup(void) {
g_validator_initialized = 0;
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
nostr_request_validator_clear_violation();
}
//=============================================================================
// HELPER FUNCTIONS
//=============================================================================
/**
* Reload authentication configuration from database
*/
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(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
g_auth_cache.cache_expires = time(NULL) + 300; // 5 minutes
g_auth_cache.cache_valid = 1;
return NOSTR_SUCCESS;
}
// Load configuration values from server_config table
const char* server_sql = "SELECT key, value FROM server_config WHERE key IN ('require_auth', 'max_file_size', 'admin_enabled', 'admin_pubkey')";
rc = sqlite3_prepare_v2(db, server_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, "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);
}
}
sqlite3_finalize(stmt);
}
// Load auth-specific configuration from auth_config table
const char* auth_sql = "SELECT key, value FROM auth_config WHERE key IN ('auth_rules_enabled', 'require_nip42_auth')";
rc = sqlite3_prepare_v2(db, auth_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, "auth_rules_enabled") == 0) {
// Override auth_required with auth_rules_enabled if present
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "require_nip42_auth") == 0) {
if (strcmp(value, "false") == 0) {
g_auth_cache.nip42_mode = 0;
} else if (strcmp(value, "required") == 0) {
g_auth_cache.nip42_mode = 2;
} else {
g_auth_cache.nip42_mode = 1; // Optional
}
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
// Set cache expiration (5 minutes from now)
g_auth_cache.cache_expires = time(NULL) + 300;
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
}
// Note: This is the final debug statement, no need to log it to our debug file as it's just informational
fprintf(stderr, "VALIDATOR: Configuration loaded - auth_required: %d, max_file_size: %ld, nip42_mode: %d\n",
g_auth_cache.auth_required, g_auth_cache.max_file_size, 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;
}
// Look for required tags if method and hash are specified
if (method || expected_hash) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
int found_method = (method == NULL);
int found_hash = (expected_hash == NULL);
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 && method) {
cJSON* method_value = cJSON_GetArrayItem(tag, 1);
if (method_value && cJSON_IsString(method_value)) {
const char* event_method = cJSON_GetStringValue(method_value);
if (strcmp(event_method, method) == 0) {
found_method = 1;
}
}
} else if (strcmp(tag_name_str, "x") == 0 && expected_hash) {
cJSON* hash_value = cJSON_GetArrayItem(tag, 1);
if (hash_value && cJSON_IsString(hash_value)) {
const char* event_hash = cJSON_GetStringValue(hash_value);
if (strcmp(event_hash, expected_hash) == 0) {
found_hash = 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));
}
}
}
if (!found_method || !found_hash) {
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) {
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\n",
pubkey, operation ? operation : "NULL");
validator_debug_log(rules_msg);
// Open database
rc = sqlite3_open_v2(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)
const char* blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? AND 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) {
const char* hash_blacklist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = 'hash_blacklist' AND rule_target = ? AND 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 pubkey whitelist
const char* whitelist_sql = "SELECT rule_type, description FROM auth_rules WHERE rule_type = 'pubkey_whitelist' AND rule_target = ? AND 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 4: Check if any whitelist rules exist - if yes, deny by default
const char* whitelist_exists_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' AND 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 5 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;
}
// Use the existing NIP-42 verification from nostr_core_lib
int verification_result = nostr_nip42_verify_auth_event(event, challenge_id,
relay_url, NOSTR_NIP42_DEFAULT_TIME_TOLERANCE);
if (verification_result != NOSTR_SUCCESS) {
return verification_result;
}
return NOSTR_SUCCESS;
}