Add NIP-42 implementation and local updates

- Added NIP-42 authentication implementation (nip042.c, nip042.h)
- Added NIP-42 test suite (nip42_test.c, nip42_test)
- Updated common core files for NIP-42 support
- Updated build script
- Rebuilt test binaries
This commit is contained in:
2025-09-07 13:04:53 -04:00
parent 55e2a9c68e
commit eb7a9e6098
18 changed files with 1637 additions and 5 deletions

628
nostr_core/nip042.c Normal file
View File

@@ -0,0 +1,628 @@
/*
* NOSTR Core Library - NIP-042: Authentication of clients to relays
*
* Implements client authentication through signed ephemeral events
*/
#include "nip042.h"
#include "nip001.h"
#include "utils.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// =============================================================================
// CLIENT-SIDE FUNCTIONS
// =============================================================================
/**
* Create NIP-42 authentication event (kind 22242)
*/
cJSON* nostr_nip42_create_auth_event(const char* challenge,
const char* relay_url,
const unsigned char* private_key,
time_t timestamp) {
if (!challenge || !relay_url || !private_key) {
return NULL;
}
// Validate challenge format
size_t challenge_len = strlen(challenge);
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
return NULL;
}
// Create tags array with relay and challenge
cJSON* tags = cJSON_CreateArray();
if (!tags) {
return NULL;
}
// Add relay tag
cJSON* relay_tag = cJSON_CreateArray();
if (!relay_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_url));
cJSON_AddItemToArray(tags, relay_tag);
// Add challenge tag
cJSON* challenge_tag = cJSON_CreateArray();
if (!challenge_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString("challenge"));
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString(challenge));
cJSON_AddItemToArray(tags, challenge_tag);
// Create authentication event using existing function
// Note: Empty content as per NIP-42 specification
cJSON* auth_event = nostr_create_and_sign_event(
NOSTR_NIP42_AUTH_EVENT_KIND,
"", // Empty content
tags,
private_key,
timestamp
);
cJSON_Delete(tags);
return auth_event;
}
/**
* Create AUTH message JSON for relay communication
*/
char* nostr_nip42_create_auth_message(cJSON* auth_event) {
if (!auth_event) {
return NULL;
}
// Create AUTH message array: ["AUTH", <event-json>]
cJSON* message_array = cJSON_CreateArray();
if (!message_array) {
return NULL;
}
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
cJSON_AddItemToArray(message_array, cJSON_Duplicate(auth_event, 1));
char* message_string = cJSON_PrintUnformatted(message_array);
cJSON_Delete(message_array);
return message_string;
}
/**
* Validate challenge string format and freshness
*/
int nostr_nip42_validate_challenge(const char* challenge,
time_t received_at,
int time_tolerance) {
if (!challenge) {
return NOSTR_ERROR_INVALID_INPUT;
}
size_t challenge_len = strlen(challenge);
// Check challenge length
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH) {
return NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT;
}
if (challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
return NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG;
}
// Check time validity if provided
if (received_at > 0) {
time_t now = time(NULL);
int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
if (now - received_at > tolerance) {
return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED;
}
}
return NOSTR_SUCCESS;
}
/**
* Parse AUTH challenge message from relay
*/
int nostr_nip42_parse_auth_challenge(const char* message,
char* challenge_out,
size_t challenge_size) {
if (!message || !challenge_out || challenge_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* json = cJSON_Parse(message);
if (!json || !cJSON_IsArray(json)) {
if (json) cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check array has exactly 2 elements
if (cJSON_GetArraySize(json) != 2) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check first element is "AUTH"
cJSON* message_type = cJSON_GetArrayItem(json, 0);
if (!message_type || !cJSON_IsString(message_type) ||
strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Get challenge string
cJSON* challenge_item = cJSON_GetArrayItem(json, 1);
if (!challenge_item || !cJSON_IsString(challenge_item)) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
const char* challenge_str = cJSON_GetStringValue(challenge_item);
if (!challenge_str || strlen(challenge_str) >= challenge_size) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
}
strcpy(challenge_out, challenge_str);
cJSON_Delete(json);
return NOSTR_SUCCESS;
}
// =============================================================================
// SERVER-SIDE FUNCTIONS
// =============================================================================
/**
* Generate cryptographically secure challenge string
*/
int nostr_nip42_generate_challenge(char* challenge_out, size_t length) {
if (!challenge_out || length < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
length > NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Generate random bytes
unsigned char random_bytes[NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2];
if (nostr_secp256k1_get_random_bytes(random_bytes, length) != 1) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Convert to hex string (reusing existing function)
nostr_bytes_to_hex(random_bytes, length, challenge_out);
return NOSTR_SUCCESS;
}
/**
* Verify NIP-42 authentication event
*/
int nostr_nip42_verify_auth_event(cJSON* auth_event,
const char* expected_challenge,
const char* relay_url,
int time_tolerance) {
if (!auth_event || !expected_challenge || !relay_url) {
return NOSTR_ERROR_INVALID_INPUT;
}
// First validate basic event structure using existing function
int structure_result = nostr_validate_event_structure(auth_event);
if (structure_result != NOSTR_SUCCESS) {
return structure_result;
}
// Validate NIP-42 specific structure
int nip42_structure_result = nostr_nip42_validate_auth_event_structure(
auth_event, relay_url, expected_challenge, time_tolerance);
if (nip42_structure_result != NOSTR_SUCCESS) {
return nip42_structure_result;
}
// Finally verify cryptographic signature using existing function
return nostr_verify_event_signature(auth_event);
}
/**
* Parse AUTH message from client
*/
int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out) {
if (!message || !auth_event_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* json = cJSON_Parse(message);
if (!json || !cJSON_IsArray(json)) {
if (json) cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check array has exactly 2 elements
if (cJSON_GetArraySize(json) != 2) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Check first element is "AUTH"
cJSON* message_type = cJSON_GetArrayItem(json, 0);
if (!message_type || !cJSON_IsString(message_type) ||
strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Get event object
cJSON* event_item = cJSON_GetArrayItem(json, 1);
if (!event_item || !cJSON_IsObject(event_item)) {
cJSON_Delete(json);
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
}
// Duplicate the event for the caller
*auth_event_out = cJSON_Duplicate(event_item, 1);
cJSON_Delete(json);
if (!*auth_event_out) {
return NOSTR_ERROR_MEMORY_FAILED;
}
return NOSTR_SUCCESS;
}
/**
* Create "auth-required" error response
*/
char* nostr_nip42_create_auth_required_message(const char* subscription_id,
const char* event_id,
const char* reason) {
const char* default_reason = "authentication required";
const char* message_reason = reason ? reason : default_reason;
cJSON* response = cJSON_CreateArray();
if (!response) {
return NULL;
}
if (subscription_id) {
// CLOSED message for subscriptions
cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id));
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else if (event_id) {
// OK message for events
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else {
cJSON_Delete(response);
return NULL;
}
char* message_string = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
return message_string;
}
/**
* Create "restricted" error response
*/
char* nostr_nip42_create_restricted_message(const char* subscription_id,
const char* event_id,
const char* reason) {
const char* default_reason = "access restricted";
const char* message_reason = reason ? reason : default_reason;
cJSON* response = cJSON_CreateArray();
if (!response) {
return NULL;
}
if (subscription_id) {
// CLOSED message for subscriptions
cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id));
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else if (event_id) {
// OK message for events
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false
char prefix_message[512];
snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason);
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
} else {
cJSON_Delete(response);
return NULL;
}
char* message_string = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
return message_string;
}
// =============================================================================
// URL NORMALIZATION FUNCTIONS
// =============================================================================
/**
* Normalize relay URL for comparison
*/
char* nostr_nip42_normalize_url(const char* url) {
if (!url) {
return NULL;
}
size_t url_len = strlen(url);
char* normalized = malloc(url_len + 1);
if (!normalized) {
return NULL;
}
strcpy(normalized, url);
// Remove trailing slash
if (url_len > 1 && normalized[url_len - 1] == '/') {
normalized[url_len - 1] = '\0';
}
// Convert to lowercase for domain comparison
for (size_t i = 0; normalized[i]; i++) {
if (normalized[i] >= 'A' && normalized[i] <= 'Z') {
normalized[i] = normalized[i] + ('a' - 'A');
}
}
return normalized;
}
/**
* Check if two relay URLs match after normalization
*/
int nostr_nip42_urls_match(const char* url1, const char* url2) {
if (!url1 || !url2) {
return -1;
}
char* norm1 = nostr_nip42_normalize_url(url1);
char* norm2 = nostr_nip42_normalize_url(url2);
if (!norm1 || !norm2) {
free(norm1);
free(norm2);
return -1;
}
int result = (strcmp(norm1, norm2) == 0) ? 1 : 0;
free(norm1);
free(norm2);
return result;
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Get string description of authentication state
*/
const char* nostr_nip42_auth_state_str(nostr_auth_state_t state) {
switch (state) {
case NOSTR_AUTH_STATE_NONE:
return "none";
case NOSTR_AUTH_STATE_CHALLENGE_RECEIVED:
return "challenge_received";
case NOSTR_AUTH_STATE_AUTHENTICATING:
return "authenticating";
case NOSTR_AUTH_STATE_AUTHENTICATED:
return "authenticated";
case NOSTR_AUTH_STATE_REJECTED:
return "rejected";
default:
return "unknown";
}
}
/**
* Initialize authentication context structure
*/
int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx,
const char* relay_url,
const char* challenge,
int time_tolerance) {
if (!ctx || !relay_url || !challenge) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(ctx, 0, sizeof(nostr_auth_context_t));
ctx->relay_url = malloc(strlen(relay_url) + 1);
if (!ctx->relay_url) {
return NOSTR_ERROR_MEMORY_FAILED;
}
strcpy(ctx->relay_url, relay_url);
ctx->challenge = malloc(strlen(challenge) + 1);
if (!ctx->challenge) {
free(ctx->relay_url);
ctx->relay_url = NULL;
return NOSTR_ERROR_MEMORY_FAILED;
}
strcpy(ctx->challenge, challenge);
ctx->timestamp = time(NULL);
ctx->time_tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
return NOSTR_SUCCESS;
}
/**
* Free authentication context structure
*/
void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx) {
if (!ctx) {
return;
}
free(ctx->relay_url);
free(ctx->challenge);
free(ctx->pubkey_hex);
memset(ctx, 0, sizeof(nostr_auth_context_t));
}
/**
* Validate authentication event structure (without signature verification)
*/
int nostr_nip42_validate_auth_event_structure(cJSON* auth_event,
const char* relay_url,
const char* challenge,
int time_tolerance) {
if (!auth_event || !relay_url || !challenge) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Check event kind is 22242
cJSON* kind_item = cJSON_GetObjectItem(auth_event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) ||
(int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP42_AUTH_EVENT_KIND) {
return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID;
}
// Check timestamp is within tolerance
cJSON* created_at_item = cJSON_GetObjectItem(auth_event, "created_at");
if (!created_at_item || !cJSON_IsNumber(created_at_item)) {
return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
}
time_t event_time = (time_t)cJSON_GetNumberValue(created_at_item);
time_t now = time(NULL);
int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
if (abs((int)(now - event_time)) > tolerance) {
return NOSTR_ERROR_NIP42_TIME_TOLERANCE;
}
// Check tags contain required relay and challenge
cJSON* tags_item = cJSON_GetObjectItem(auth_event, "tags");
if (!tags_item || !cJSON_IsArray(tags_item)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
int found_relay = 0, found_challenge = 0;
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) {
continue;
}
cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag_item, 1);
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
continue;
}
const char* name = cJSON_GetStringValue(tag_name);
const char* value = cJSON_GetStringValue(tag_value);
if (strcmp(name, "relay") == 0) {
if (nostr_nip42_urls_match(value, relay_url) == 1) {
found_relay = 1;
}
} else if (strcmp(name, "challenge") == 0) {
if (strcmp(value, challenge) == 0) {
found_challenge = 1;
}
}
}
if (!found_relay) {
return NOSTR_ERROR_NIP42_URL_MISMATCH;
}
if (!found_challenge) {
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
}
return NOSTR_SUCCESS;
}
// =============================================================================
// WEBSOCKET CLIENT INTEGRATION STUB FUNCTIONS
// =============================================================================
// Note: These will need to be implemented when WebSocket client structure is available
int nostr_ws_authenticate(struct nostr_ws_client* client,
const unsigned char* private_key,
int time_tolerance) {
// TODO: Implement when WebSocket client structure is available
(void)client;
(void)private_key;
(void)time_tolerance;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client) {
// TODO: Implement when WebSocket client structure is available
(void)client;
return NOSTR_AUTH_STATE_NONE; // Placeholder
}
int nostr_ws_has_valid_challenge(struct nostr_ws_client* client) {
// TODO: Implement when WebSocket client structure is available
(void)client;
return 0; // Placeholder
}
int nostr_ws_get_challenge(struct nostr_ws_client* client,
char* challenge_out,
size_t challenge_size) {
// TODO: Implement when WebSocket client structure is available
(void)client;
(void)challenge_out;
(void)challenge_size;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
int nostr_ws_store_challenge(struct nostr_ws_client* client,
const char* challenge) {
// TODO: Implement when WebSocket client structure is available
(void)client;
(void)challenge;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}
int nostr_ws_clear_auth_state(struct nostr_ws_client* client) {
// TODO: Implement when WebSocket client structure is available
(void)client;
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
}

281
nostr_core/nip042.h Normal file
View File

@@ -0,0 +1,281 @@
/*
* NOSTR Core Library - NIP-042: Authentication of clients to relays
*
* Implements client authentication through signed ephemeral events
*/
#ifndef NIP042_H
#define NIP042_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "../cjson/cJSON.h"
#include "nostr_common.h"
#ifdef __cplusplus
extern "C" {
#endif
// =============================================================================
// NIP-42 CONSTANTS AND DEFINITIONS
// =============================================================================
#define NOSTR_NIP42_AUTH_EVENT_KIND 22242
#define NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH 32
#define NOSTR_NIP42_DEFAULT_TIME_TOLERANCE 600 // 10 minutes in seconds
#define NOSTR_NIP42_MAX_CHALLENGE_LENGTH 256
#define NOSTR_NIP42_MIN_CHALLENGE_LENGTH 16
// Authentication states for WebSocket client integration
typedef enum {
NOSTR_AUTH_STATE_NONE = 0, // No authentication attempted
NOSTR_AUTH_STATE_CHALLENGE_RECEIVED = 1, // Challenge received from relay
NOSTR_AUTH_STATE_AUTHENTICATING = 2, // AUTH event sent, waiting for OK
NOSTR_AUTH_STATE_AUTHENTICATED = 3, // Successfully authenticated
NOSTR_AUTH_STATE_REJECTED = 4 // Authentication rejected
} nostr_auth_state_t;
// Challenge storage structure
typedef struct {
char challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
time_t received_at;
int is_valid;
} nostr_auth_challenge_t;
// Authentication context for relay verification
typedef struct {
char* relay_url;
char* challenge;
time_t timestamp;
int time_tolerance;
char* pubkey_hex;
} nostr_auth_context_t;
// =============================================================================
// CLIENT-SIDE FUNCTIONS (for nostr clients)
// =============================================================================
/**
* Create NIP-42 authentication event (kind 22242)
* @param challenge Challenge string received from relay
* @param relay_url Relay URL (normalized)
* @param private_key 32-byte private key for signing
* @param timestamp Event timestamp (0 for current time)
* @return cJSON event object or NULL on error
*/
cJSON* nostr_nip42_create_auth_event(const char* challenge,
const char* relay_url,
const unsigned char* private_key,
time_t timestamp);
/**
* Create AUTH message JSON for relay communication
* @param auth_event Authentication event (kind 22242)
* @return JSON string for AUTH message or NULL on error (caller must free)
*/
char* nostr_nip42_create_auth_message(cJSON* auth_event);
/**
* Validate challenge string format and freshness
* @param challenge Challenge string to validate
* @param received_at Time when challenge was received (0 for no time check)
* @param time_tolerance Maximum age in seconds (0 for default)
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_validate_challenge(const char* challenge,
time_t received_at,
int time_tolerance);
/**
* Parse AUTH challenge message from relay
* @param message Raw message from relay
* @param challenge_out Output buffer for challenge string
* @param challenge_size Size of challenge buffer
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_parse_auth_challenge(const char* message,
char* challenge_out,
size_t challenge_size);
// =============================================================================
// SERVER-SIDE FUNCTIONS (for relay implementations)
// =============================================================================
/**
* Generate cryptographically secure challenge string
* @param challenge_out Output buffer for challenge (must be at least length*2+1)
* @param length Desired challenge length in bytes (16-128)
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_generate_challenge(char* challenge_out, size_t length);
/**
* Verify NIP-42 authentication event
* @param auth_event Authentication event to verify
* @param expected_challenge Challenge that was sent to client
* @param relay_url Expected relay URL
* @param time_tolerance Maximum timestamp deviation in seconds
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_verify_auth_event(cJSON* auth_event,
const char* expected_challenge,
const char* relay_url,
int time_tolerance);
/**
* Parse AUTH message from client
* @param message Raw AUTH message from client
* @param auth_event_out Output pointer to parsed event (caller must free)
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out);
/**
* Create "auth-required" error response
* @param subscription_id Subscription ID (for CLOSED) or NULL (for OK)
* @param event_id Event ID (for OK) or NULL (for CLOSED)
* @param reason Human-readable reason
* @return JSON string for response or NULL on error (caller must free)
*/
char* nostr_nip42_create_auth_required_message(const char* subscription_id,
const char* event_id,
const char* reason);
/**
* Create "restricted" error response
* @param subscription_id Subscription ID (for CLOSED) or NULL (for OK)
* @param event_id Event ID (for OK) or NULL (for CLOSED)
* @param reason Human-readable reason
* @return JSON string for response or NULL on error (caller must free)
*/
char* nostr_nip42_create_restricted_message(const char* subscription_id,
const char* event_id,
const char* reason);
// =============================================================================
// URL NORMALIZATION FUNCTIONS
// =============================================================================
/**
* Normalize relay URL for comparison (removes trailing slashes, etc.)
* @param url Original URL
* @return Normalized URL string or NULL on error (caller must free)
*/
char* nostr_nip42_normalize_url(const char* url);
/**
* Check if two relay URLs match after normalization
* @param url1 First URL
* @param url2 Second URL
* @return 1 if URLs match, 0 if they don't, -1 on error
*/
int nostr_nip42_urls_match(const char* url1, const char* url2);
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Get string description of authentication state
* @param state Authentication state
* @return Human-readable string
*/
const char* nostr_nip42_auth_state_str(nostr_auth_state_t state);
/**
* Initialize authentication context structure
* @param ctx Context to initialize
* @param relay_url Relay URL
* @param challenge Challenge string
* @param time_tolerance Time tolerance in seconds
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx,
const char* relay_url,
const char* challenge,
int time_tolerance);
/**
* Free authentication context structure
* @param ctx Context to free
*/
void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx);
/**
* Validate authentication event structure (without signature verification)
* @param auth_event Event to validate
* @param relay_url Expected relay URL
* @param challenge Expected challenge
* @param time_tolerance Maximum timestamp deviation in seconds
* @return NOSTR_SUCCESS or error code
*/
int nostr_nip42_validate_auth_event_structure(cJSON* auth_event,
const char* relay_url,
const char* challenge,
int time_tolerance);
// =============================================================================
// WEBSOCKET CLIENT INTEGRATION
// =============================================================================
// Forward declaration for WebSocket client
struct nostr_ws_client;
/**
* Authenticate WebSocket client with relay
* @param client WebSocket client handle
* @param private_key 32-byte private key for authentication
* @param time_tolerance Maximum timestamp deviation in seconds (0 for default)
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_authenticate(struct nostr_ws_client* client,
const unsigned char* private_key,
int time_tolerance);
/**
* Get current authentication state of WebSocket client
* @param client WebSocket client handle
* @return Current authentication state
*/
nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client);
/**
* Check if WebSocket client has stored valid challenge
* @param client WebSocket client handle
* @return 1 if valid challenge exists, 0 otherwise
*/
int nostr_ws_has_valid_challenge(struct nostr_ws_client* client);
/**
* Get stored challenge from WebSocket client
* @param client WebSocket client handle
* @param challenge_out Output buffer for challenge
* @param challenge_size Size of output buffer
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_get_challenge(struct nostr_ws_client* client,
char* challenge_out,
size_t challenge_size);
/**
* Store challenge in WebSocket client (internal function)
* @param client WebSocket client handle
* @param challenge Challenge string to store
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_store_challenge(struct nostr_ws_client* client,
const char* challenge);
/**
* Clear authentication state in WebSocket client
* @param client WebSocket client handle
* @return NOSTR_SUCCESS or error code
*/
int nostr_ws_clear_auth_state(struct nostr_ws_client* client);
#ifdef __cplusplus
}
#endif
#endif // NIP042_H

View File

@@ -43,6 +43,15 @@ const char* nostr_strerror(int error_code) {
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: return "NIP-13: Invalid nonce tag format";
case NOSTR_ERROR_NIP13_TARGET_MISMATCH: return "NIP-13: Target difficulty mismatch";
case NOSTR_ERROR_NIP13_CALCULATION: return "NIP-13: PoW calculation error";
case NOSTR_ERROR_NIP42_INVALID_CHALLENGE: return "NIP-42: Invalid challenge";
case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: return "NIP-42: Challenge expired";
case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID: return "NIP-42: Authentication event invalid";
case NOSTR_ERROR_NIP42_URL_MISMATCH: return "NIP-42: Relay URL mismatch";
case NOSTR_ERROR_NIP42_TIME_TOLERANCE: return "NIP-42: Timestamp outside tolerance";
case NOSTR_ERROR_NIP42_NOT_AUTHENTICATED: return "NIP-42: Client not authenticated";
case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format";
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT: return "NIP-42: Challenge too short";
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long";
default: return "Unknown error";
}
}

View File

@@ -43,6 +43,16 @@
#define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103
#define NOSTR_ERROR_NIP13_CALCULATION -104
// NIP-42 Authentication-specific error codes
#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -200
#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201
#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -202
#define NOSTR_ERROR_NIP42_URL_MISMATCH -203
#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204
#define NOSTR_ERROR_NIP42_NOT_AUTHENTICATED -205
#define NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT -206
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
// Constants
#define NOSTR_PRIVATE_KEY_SIZE 32

View File

@@ -42,7 +42,14 @@
* - nostr_nip44_encrypt() -> Encrypt with ChaCha20 + HMAC
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
* - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages
*
*
* NIP-42 AUTHENTICATION:
* - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242)
* - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side)
* - nostr_nip42_generate_challenge() -> Generate challenge string (relay-side)
* - nostr_ws_authenticate() -> Authenticate WebSocket client
* - nostr_ws_get_auth_state() -> Get client authentication state
*
* BIP39 MNEMONICS:
* - nostr_bip39_mnemonic_from_bytes() -> Generate mnemonic from entropy
* - nostr_bip39_mnemonic_validate() -> Validate mnemonic phrase
@@ -96,7 +103,11 @@
* nostr_bip32_key_from_seed(seed, 64, &master_key);
* uint32_t path[] = {44, 1237, 0, 0, 0}; // m/44'/1237'/0'/0/0
* nostr_bip32_derive_path(&master_key, path, 5, &derived_key);
*
*
* Client Authentication (NIP-42):
* cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay_url, private_key, 0);
* nostr_ws_authenticate(client, private_key, 600); // Auto-authenticate WebSocket
*
* ============================================================================
*/
@@ -116,6 +127,7 @@ extern "C" {
#include "nip011.h" // Relay information document
#include "nip013.h" // Proof of Work
#include "nip019.h" // Bech32 encoding (nsec/npub)
#include "nip042.h" // Authentication of clients to relays
#include "nip044.h" // Encryption (modern)
// Relay communication functions are defined in nostr_common.h