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:
parent
55e2a9c68e
commit
eb7a9e6098
8
build.sh
8
build.sh
|
@ -135,6 +135,7 @@ if [ "$HELP" = true ]; then
|
|||
echo " 011 - Relay information document"
|
||||
echo " 013 - Proof of Work"
|
||||
echo " 019 - Bech32 encoding (nsec/npub)"
|
||||
echo " 042 - Authentication of clients to relays"
|
||||
echo " 044 - Encryption (modern)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
|
@ -184,7 +185,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
|
|||
NEEDED_NIPS=""
|
||||
if [ -n "$FORCE_NIPS" ]; then
|
||||
if [ "$FORCE_NIPS" = "all" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 044"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
print_info "Forced: Building all available NIPs"
|
||||
else
|
||||
# Convert comma-separated list to space-separated with 3-digit format
|
||||
|
@ -203,7 +204,7 @@ else
|
|||
# Check for nostr_core.h (includes everything)
|
||||
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
|
||||
print_info "Found #include \"nostr_core.h\" - building all NIPs"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 044"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
elif [ -n "$DETECTED" ]; then
|
||||
NEEDED_NIPS="$DETECTED"
|
||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||
|
@ -221,7 +222,7 @@ fi
|
|||
|
||||
# If building tests, include all NIPs to ensure test compatibility
|
||||
if [ "$BUILD_TESTS" = true ] && [ -z "$FORCE_NIPS" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 044"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
print_info "Building tests - including all available NIPs for test compatibility"
|
||||
fi
|
||||
|
||||
|
@ -506,6 +507,7 @@ for nip in $NEEDED_NIPS; do
|
|||
011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;;
|
||||
013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;;
|
||||
019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;;
|
||||
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
|
||||
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
|
||||
esac
|
||||
else
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
tests/bip32_test
BIN
tests/bip32_test
Binary file not shown.
BIN
tests/nip01_test
BIN
tests/nip01_test
Binary file not shown.
BIN
tests/nip04_test
BIN
tests/nip04_test
Binary file not shown.
BIN
tests/nip05_test
BIN
tests/nip05_test
Binary file not shown.
BIN
tests/nip11_test
BIN
tests/nip11_test
Binary file not shown.
BIN
tests/nip13_test
BIN
tests/nip13_test
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,690 @@
|
|||
/*
|
||||
* NIP-42 Authentication of Clients to Relays Test Suite
|
||||
* Tests auth challenge generation, event creation, validation, and message parsing
|
||||
* Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE // For strdup on Linux
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "../nostr_core/nip042.h"
|
||||
#include "../nostr_core/nip001.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
#include "../nostr_core/utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Ensure strdup is declared
|
||||
#ifndef strdup
|
||||
extern char *strdup(const char *s);
|
||||
#endif
|
||||
|
||||
// Test counter for tracking progress
|
||||
static int test_count = 0;
|
||||
static int passed_tests = 0;
|
||||
|
||||
void print_test_header(const char* test_name) {
|
||||
test_count++;
|
||||
printf("\n=== TEST %d: %s ===\n", test_count, test_name);
|
||||
}
|
||||
|
||||
void print_test_result(int passed, const char* test_name) {
|
||||
if (passed) {
|
||||
passed_tests++;
|
||||
printf("✅ PASS: %s\n", test_name);
|
||||
} else {
|
||||
printf("❌ FAIL: %s\n", test_name);
|
||||
}
|
||||
}
|
||||
|
||||
void print_json_comparison(const char* label, cJSON* expected, cJSON* actual) {
|
||||
char* expected_str = cJSON_Print(expected);
|
||||
char* actual_str;
|
||||
|
||||
if (actual) {
|
||||
actual_str = cJSON_Print(actual);
|
||||
} else {
|
||||
actual_str = strdup("NULL");
|
||||
}
|
||||
|
||||
printf("%s Expected JSON:\n%s\n", label, expected_str ? expected_str : "NULL");
|
||||
printf("%s Actual JSON:\n%s\n", label, actual_str ? actual_str : "NULL");
|
||||
|
||||
if (expected_str) free(expected_str);
|
||||
if (actual_str) free(actual_str);
|
||||
}
|
||||
|
||||
// Test 1: Challenge generation
|
||||
int test_challenge_generation(void) {
|
||||
print_test_header("Challenge Generation");
|
||||
|
||||
nostr_auth_challenge_t challenge1, challenge2;
|
||||
|
||||
printf("Generating first challenge...\n");
|
||||
int result1 = nostr_nip42_generate_challenge(challenge1.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
printf("Result: %d (%s)\n", result1, nostr_strerror(result1));
|
||||
|
||||
if (result1 != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate first challenge\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("First challenge: %s\n", challenge1.challenge);
|
||||
printf("Challenge length: %lu\n", strlen(challenge1.challenge));
|
||||
printf("Expected length: %d\n", NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH * 2);
|
||||
|
||||
if (strlen(challenge1.challenge) != NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH * 2) {
|
||||
printf("❌ Challenge length incorrect\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Generating second challenge...\n");
|
||||
int result2 = nostr_nip42_generate_challenge(challenge2.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
printf("Result: %d (%s)\n", result2, nostr_strerror(result2));
|
||||
|
||||
if (result2 != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate second challenge\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Second challenge: %s\n", challenge2.challenge);
|
||||
|
||||
// Challenges should be different (extremely high probability)
|
||||
if (strcmp(challenge1.challenge, challenge2.challenge) == 0) {
|
||||
printf("❌ Two challenges are identical (highly unlikely)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ Challenges are different (good entropy)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 2: AUTH message creation (server-side challenge)
|
||||
int test_auth_message_creation(void) {
|
||||
print_test_header("AUTH Message Creation - Server Challenge");
|
||||
|
||||
nostr_auth_challenge_t challenge;
|
||||
int gen_result = nostr_nip42_generate_challenge(challenge.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
|
||||
if (gen_result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate challenge for message test\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Generated challenge: %s\n", challenge.challenge);
|
||||
|
||||
// Create AUTH challenge message: ["AUTH", "challenge_string"]
|
||||
cJSON* message_array = cJSON_CreateArray();
|
||||
if (!message_array) {
|
||||
printf("❌ Failed to create message array\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString(challenge.challenge));
|
||||
|
||||
char* auth_message = cJSON_PrintUnformatted(message_array);
|
||||
cJSON_Delete(message_array);
|
||||
|
||||
if (!auth_message) {
|
||||
printf("❌ Failed to create AUTH message\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int result = NOSTR_SUCCESS;
|
||||
|
||||
printf("Message creation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("AUTH message: %s\n", auth_message);
|
||||
|
||||
// Parse the message to verify format
|
||||
cJSON* parsed = cJSON_Parse(auth_message);
|
||||
if (!parsed) {
|
||||
printf("❌ AUTH message is not valid JSON\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if it's an array with 2 elements
|
||||
if (!cJSON_IsArray(parsed) || cJSON_GetArraySize(parsed) != 2) {
|
||||
printf("❌ AUTH message is not a 2-element array\n");
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check first element is "AUTH"
|
||||
cJSON* first = cJSON_GetArrayItem(parsed, 0);
|
||||
if (!cJSON_IsString(first) || strcmp(cJSON_GetStringValue(first), "AUTH") != 0) {
|
||||
printf("❌ First element is not 'AUTH'\n");
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check second element is our challenge string
|
||||
cJSON* second = cJSON_GetArrayItem(parsed, 1);
|
||||
if (!cJSON_IsString(second) || strcmp(cJSON_GetStringValue(second), challenge.challenge) != 0) {
|
||||
printf("❌ Second element is not our challenge string\n");
|
||||
printf("Expected: %s\n", challenge.challenge);
|
||||
printf("Actual: %s\n", cJSON_GetStringValue(second));
|
||||
cJSON_Delete(parsed);
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ AUTH challenge message format is correct\n");
|
||||
cJSON_Delete(parsed);
|
||||
free(auth_message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 3: Authentication event creation (client-side)
|
||||
int test_auth_event_creation(void) {
|
||||
print_test_header("Authentication Event Creation - Client Side");
|
||||
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* challenge_string = "test_challenge_12345678901234567890123456789012";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
printf("Private key (hex): %s\n", private_key_hex);
|
||||
printf("Relay URL: %s\n", relay_url);
|
||||
printf("Challenge: %s\n", challenge_string);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0);
|
||||
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create authentication event\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* event_str = cJSON_Print(auth_event);
|
||||
printf("Created Auth Event JSON:\n%s\n", event_str);
|
||||
free(event_str);
|
||||
|
||||
// Validate the event structure
|
||||
int structure_result = nostr_validate_event_structure(auth_event);
|
||||
printf("Structure validation result: %d (%s)\n", structure_result, nostr_strerror(structure_result));
|
||||
|
||||
if (structure_result != NOSTR_SUCCESS) {
|
||||
printf("❌ Auth event failed structure validation\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate the event signature
|
||||
int crypto_result = nostr_verify_event_signature(auth_event);
|
||||
printf("Signature validation result: %d (%s)\n", crypto_result, nostr_strerror(crypto_result));
|
||||
|
||||
if (crypto_result != NOSTR_SUCCESS) {
|
||||
printf("❌ Auth event failed signature validation\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check kind is 22242
|
||||
cJSON* kind = cJSON_GetObjectItem(auth_event, "kind");
|
||||
if (!cJSON_IsNumber(kind) || cJSON_GetNumberValue(kind) != 22242) {
|
||||
printf("❌ Auth event kind is not 22242\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check for relay tag
|
||||
cJSON* tags = cJSON_GetObjectItem(auth_event, "tags");
|
||||
int found_relay = 0, found_challenge = 0;
|
||||
|
||||
if (cJSON_IsArray(tags)) {
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
|
||||
if (strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) {
|
||||
found_relay = 1;
|
||||
if (strcmp(cJSON_GetStringValue(tag_value), relay_url) != 0) {
|
||||
printf("❌ Relay tag value incorrect\n");
|
||||
printf("Expected: %s\n", relay_url);
|
||||
printf("Actual: %s\n", cJSON_GetStringValue(tag_value));
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
} else if (strcmp(cJSON_GetStringValue(tag_name), "challenge") == 0) {
|
||||
found_challenge = 1;
|
||||
if (strcmp(cJSON_GetStringValue(tag_value), challenge_string) != 0) {
|
||||
printf("❌ Challenge tag value incorrect\n");
|
||||
printf("Expected: %s\n", challenge_string);
|
||||
printf("Actual: %s\n", cJSON_GetStringValue(tag_value));
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_relay) {
|
||||
printf("❌ Missing relay tag\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!found_challenge) {
|
||||
printf("❌ Missing challenge tag\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ Authentication event created successfully with correct tags\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 4: Authentication event validation (server-side)
|
||||
int test_auth_event_validation(void) {
|
||||
print_test_header("Authentication Event Validation - Server Side");
|
||||
|
||||
// Create a valid auth event first
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* challenge_string = "validation_challenge_1234567890123456789012";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0);
|
||||
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create auth event for validation test\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* event_str = cJSON_Print(auth_event);
|
||||
printf("Auth Event to validate:\n%s\n", event_str);
|
||||
free(event_str);
|
||||
|
||||
// Test successful validation
|
||||
printf("Testing successful validation...\n");
|
||||
int result = nostr_nip42_verify_auth_event(auth_event, challenge_string, relay_url, 0);
|
||||
printf("Validation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Valid auth event failed validation\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Valid auth event passed validation\n");
|
||||
|
||||
// Test wrong relay URL
|
||||
printf("\nTesting wrong relay URL...\n");
|
||||
result = nostr_nip42_verify_auth_event(auth_event, challenge_string, "wss://wrong.relay.com", 0);
|
||||
printf("Expected: NOSTR_ERROR_NIP42_URL_MISMATCH (-206)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_ERROR_NIP42_URL_MISMATCH) {
|
||||
printf("❌ Wrong relay validation didn't fail correctly\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Wrong relay URL correctly rejected\n");
|
||||
|
||||
// Test wrong challenge
|
||||
printf("\nTesting wrong challenge...\n");
|
||||
result = nostr_nip42_verify_auth_event(auth_event, "wrong_challenge_string_here", relay_url, 0);
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_CHALLENGE (-203)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_CHALLENGE) {
|
||||
printf("❌ Wrong challenge validation didn't fail correctly\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Wrong challenge correctly rejected\n");
|
||||
|
||||
cJSON_Delete(auth_event);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 5: AUTH message parsing (client-side)
|
||||
int test_auth_message_parsing(void) {
|
||||
print_test_header("AUTH Message Parsing - Client Side");
|
||||
|
||||
// Test parsing challenge message
|
||||
const char* challenge_msg = "[\"AUTH\", \"test_challenge_from_server_123456789012\"]";
|
||||
printf("Parsing AUTH challenge message: %s\n", challenge_msg);
|
||||
|
||||
char extracted_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
int result = nostr_nip42_parse_auth_challenge(challenge_msg, extracted_challenge, sizeof(extracted_challenge));
|
||||
|
||||
printf("Parse result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("Expected challenge: test_challenge_from_server_123456789012\n");
|
||||
printf("Extracted challenge: %s\n", extracted_challenge);
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to parse valid AUTH message\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(extracted_challenge, "test_challenge_from_server_123456789012") != 0) {
|
||||
printf("❌ Extracted challenge doesn't match expected\n");
|
||||
return 0;
|
||||
}
|
||||
printf("✅ AUTH challenge message parsed correctly\n");
|
||||
|
||||
// Test invalid message format
|
||||
printf("\nTesting invalid message format...\n");
|
||||
const char* invalid_msg = "[\"WRONG\", \"challenge\"]";
|
||||
result = nostr_nip42_parse_auth_challenge(invalid_msg, extracted_challenge, sizeof(extracted_challenge));
|
||||
|
||||
printf("Parse result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n");
|
||||
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) {
|
||||
printf("❌ Invalid message format should have failed\n");
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Invalid message format correctly rejected\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 6: AUTH response message creation (client-side)
|
||||
int test_auth_response_creation(void) {
|
||||
print_test_header("AUTH Response Message Creation - Client Side");
|
||||
|
||||
// Create an auth event first
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* challenge_string = "response_test_challenge_1234567890123456";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0);
|
||||
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create auth event for response test\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* auth_response = nostr_nip42_create_auth_message(auth_event);
|
||||
int result = auth_response ? NOSTR_SUCCESS : NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
printf("Response creation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("AUTH response message: %s\n", auth_response ? auth_response : "NULL");
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to create AUTH response message\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse and validate the response format
|
||||
cJSON* parsed = cJSON_Parse(auth_response);
|
||||
if (!parsed) {
|
||||
printf("❌ AUTH response is not valid JSON\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Should be ["AUTH", <event-json>]
|
||||
if (!cJSON_IsArray(parsed) || cJSON_GetArraySize(parsed) != 2) {
|
||||
printf("❌ AUTH response is not a 2-element array\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* first = cJSON_GetArrayItem(parsed, 0);
|
||||
if (!cJSON_IsString(first) || strcmp(cJSON_GetStringValue(first), "AUTH") != 0) {
|
||||
printf("❌ First element is not 'AUTH'\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* second = cJSON_GetArrayItem(parsed, 1);
|
||||
if (!cJSON_IsObject(second)) {
|
||||
printf("❌ Second element is not an object (event)\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if the event in the response matches our created event
|
||||
cJSON* response_kind = cJSON_GetObjectItem(second, "kind");
|
||||
if (!cJSON_IsNumber(response_kind) || cJSON_GetNumberValue(response_kind) != 22242) {
|
||||
printf("❌ Response event kind is not 22242\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ AUTH response message format is correct\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_response);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 7: Error conditions and edge cases
|
||||
int test_error_conditions(void) {
|
||||
print_test_header("Error Conditions and Edge Cases");
|
||||
|
||||
int all_passed = 1;
|
||||
|
||||
// Test 1: NULL parameters
|
||||
printf("\nSubtest 1: NULL parameters\n");
|
||||
int result = nostr_nip42_generate_challenge(NULL, 32);
|
||||
printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_INVALID_INPUT) all_passed = 0;
|
||||
|
||||
// Test 2: Invalid challenge length
|
||||
printf("\nSubtest 2: Invalid challenge in validation\n");
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* valid_challenge = "valid_challenge_1234567890123456789012345";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(valid_challenge, relay_url, private_key, 0);
|
||||
if (auth_event) {
|
||||
result = nostr_nip42_verify_auth_event(auth_event, "short", relay_url, 0); // Too short
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_CHALLENGE (-203)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_CHALLENGE) all_passed = 0;
|
||||
cJSON_Delete(auth_event);
|
||||
} else {
|
||||
printf("❌ Failed to create auth event for validation test\n");
|
||||
all_passed = 0;
|
||||
}
|
||||
|
||||
// Test 3: Invalid JSON parsing
|
||||
printf("\nSubtest 3: Invalid JSON in message parsing\n");
|
||||
char challenge_buffer[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
result = nostr_nip42_parse_auth_challenge("invalid json", challenge_buffer, sizeof(challenge_buffer));
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) all_passed = 0;
|
||||
|
||||
// Test 4: Wrong array size
|
||||
printf("\nSubtest 4: Wrong array size in message parsing\n");
|
||||
result = nostr_nip42_parse_auth_challenge("[\"AUTH\"]", challenge_buffer, sizeof(challenge_buffer)); // Only 1 element
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) all_passed = 0;
|
||||
|
||||
return all_passed;
|
||||
}
|
||||
|
||||
// Test 8: Full authentication flow simulation
|
||||
int test_full_auth_flow(void) {
|
||||
print_test_header("Full Authentication Flow Simulation");
|
||||
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://test-relay.nostr.com";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
printf("=== STEP 1: Server generates challenge ===\n");
|
||||
nostr_auth_challenge_t challenge;
|
||||
int result = nostr_nip42_generate_challenge(challenge.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate challenge\n");
|
||||
return 0;
|
||||
}
|
||||
printf("Generated challenge: %s\n", challenge.challenge);
|
||||
|
||||
printf("\n=== STEP 2: Server sends AUTH message ===\n");
|
||||
cJSON* message_array = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString(challenge.challenge));
|
||||
char* auth_message = cJSON_PrintUnformatted(message_array);
|
||||
cJSON_Delete(message_array);
|
||||
|
||||
if (!auth_message) {
|
||||
printf("❌ Failed to create AUTH message\n");
|
||||
return 0;
|
||||
}
|
||||
printf("Server sends: %s\n", auth_message);
|
||||
|
||||
printf("\n=== STEP 3: Client parses AUTH message ===\n");
|
||||
char parsed_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
result = nostr_nip42_parse_auth_challenge(auth_message, parsed_challenge, sizeof(parsed_challenge));
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to parse AUTH message\n");
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
printf("Client extracted challenge: %s\n", parsed_challenge);
|
||||
|
||||
if (strcmp(challenge.challenge, parsed_challenge) != 0) {
|
||||
printf("❌ Parsed challenge doesn't match original\n");
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("\n=== STEP 4: Client creates auth event ===\n");
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(parsed_challenge, relay_url, private_key, 0);
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create auth event\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* event_str = cJSON_Print(auth_event);
|
||||
printf("Client created auth event:\n%s\n", event_str);
|
||||
free(event_str);
|
||||
|
||||
printf("\n=== STEP 5: Client sends AUTH response ===\n");
|
||||
char* auth_response = nostr_nip42_create_auth_message(auth_event);
|
||||
if (!auth_response) {
|
||||
printf("❌ Failed to create AUTH response\n");
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
printf("Client sends: %s\n", auth_response);
|
||||
|
||||
printf("\n=== STEP 6: Server validates auth event ===\n");
|
||||
result = nostr_nip42_verify_auth_event(auth_event, challenge.challenge, relay_url, 0);
|
||||
printf("Server validation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Server validation failed\n");
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_message);
|
||||
free(auth_response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ FULL AUTHENTICATION FLOW COMPLETED SUCCESSFULLY!\n");
|
||||
printf("✅ Challenge generated -> Message sent -> Challenge parsed -> Event created -> Response sent -> Event validated\n");
|
||||
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_message);
|
||||
free(auth_response);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== NIP-42 Authentication of Clients to Relays Test Suite ===\n");
|
||||
printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\n");
|
||||
printf("Tests both client-side and server-side authentication functionality\n");
|
||||
|
||||
// Initialize crypto library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to initialize nostr library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int all_passed = 1;
|
||||
int test_result;
|
||||
|
||||
// Test 1: Challenge generation
|
||||
test_result = test_challenge_generation();
|
||||
print_test_result(test_result, "Challenge Generation");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 2: AUTH message creation
|
||||
test_result = test_auth_message_creation();
|
||||
print_test_result(test_result, "AUTH Message Creation - Server Challenge");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 3: Auth event creation
|
||||
test_result = test_auth_event_creation();
|
||||
print_test_result(test_result, "Authentication Event Creation - Client Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 4: Auth event validation
|
||||
test_result = test_auth_event_validation();
|
||||
print_test_result(test_result, "Authentication Event Validation - Server Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 5: AUTH message parsing
|
||||
test_result = test_auth_message_parsing();
|
||||
print_test_result(test_result, "AUTH Message Parsing - Client Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 6: AUTH response creation
|
||||
test_result = test_auth_response_creation();
|
||||
print_test_result(test_result, "AUTH Response Message Creation - Client Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 7: Error conditions
|
||||
test_result = test_error_conditions();
|
||||
print_test_result(test_result, "Error Conditions and Edge Cases");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 8: Full authentication flow
|
||||
test_result = test_full_auth_flow();
|
||||
print_test_result(test_result, "Full Authentication Flow Simulation");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Summary
|
||||
printf("\n=== TEST SUMMARY ===\n");
|
||||
printf("Total tests: %d\n", test_count);
|
||||
printf("Passed: %d\n", passed_tests);
|
||||
printf("Failed: %d\n", test_count - passed_tests);
|
||||
|
||||
if (all_passed) {
|
||||
printf("🎉 ALL TESTS PASSED! NIP-42 Authentication implementation is working correctly.\n");
|
||||
printf("✅ Challenge generation works\n");
|
||||
printf("✅ AUTH message creation/parsing works\n");
|
||||
printf("✅ Authentication event creation works\n");
|
||||
printf("✅ Authentication event validation works\n");
|
||||
printf("✅ Full authentication flow works\n");
|
||||
printf("✅ Error handling works\n");
|
||||
} else {
|
||||
printf("❌ SOME TESTS FAILED. Please review the output above.\n");
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return all_passed ? 0 : 1;
|
||||
}
|
BIN
tests/nip44_test
BIN
tests/nip44_test
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/wss_test
BIN
tests/wss_test
Binary file not shown.
Loading…
Reference in New Issue