1927 lines
72 KiB
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;
|
|
}
|