Nostr note validation added to nip01

This commit is contained in:
Laan Tungir 2025-08-19 06:59:04 -04:00
parent 1da4f6751e
commit 77d92dbcf9
17 changed files with 811 additions and 36 deletions

View File

@ -1,6 +1,6 @@
This library is fully staticly linked. There should be no external dependencies. This library is fully staticly linked. There should be no external dependencies.
When building, use build.sh, not make. When building, use build.sh, not make. When building for tests use build.sh -t
When making TUI menus, try to use the first leter of the command and the key to press to execute that command. For example, if the command is "Open file" try to use a keypress of "o" upper or lower case to signal to open the file. Use this instead of number keyed menus when possible. In the command, the letter should be underlined that signifies the command. When making TUI menus, try to use the first leter of the command and the key to press to execute that command. For example, if the command is "Open file" try to use a keypress of "o" upper or lower case to signal to open the file. Use this instead of number keyed menus when possible. In the command, the letter should be underlined that signifies the command.

View File

@ -6,6 +6,7 @@
#include "nip001.h" #include "nip001.h"
#include "utils.h" #include "utils.h"
#include "crypto/nostr_secp256k1.h"
#include "../cjson/cJSON.h" #include "../cjson/cJSON.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -131,3 +132,203 @@ cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, c
return event; return event;
} }
/**
* Validate the structure of a NOSTR event
* Checks required fields, types, and basic format validation
*/
int nostr_validate_event_structure(cJSON* event) {
if (!event || !cJSON_IsObject(event)) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Check required fields exist
cJSON* id_item = cJSON_GetObjectItem(event, "id");
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
!tags_item || !content_item || !sig_item) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Validate field types
if (!cJSON_IsString(id_item)) return NOSTR_ERROR_EVENT_INVALID_ID;
if (!cJSON_IsString(pubkey_item)) return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
if (!cJSON_IsNumber(created_at_item)) return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
if (!cJSON_IsNumber(kind_item)) return NOSTR_ERROR_EVENT_INVALID_KIND;
if (!cJSON_IsArray(tags_item)) return NOSTR_ERROR_EVENT_INVALID_TAGS;
if (!cJSON_IsString(content_item)) return NOSTR_ERROR_EVENT_INVALID_CONTENT;
if (!cJSON_IsString(sig_item)) return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
// Validate hex string lengths
const char* id_str = cJSON_GetStringValue(id_item);
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
const char* sig_str = cJSON_GetStringValue(sig_item);
if (!id_str || strlen(id_str) != 64) return NOSTR_ERROR_EVENT_INVALID_ID;
if (!pubkey_str || strlen(pubkey_str) != 64) return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
if (!sig_str || strlen(sig_str) != 128) return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
// Validate hex characters (lowercase)
for (int i = 0; i < 64; i++) {
char c = id_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_ID;
}
c = pubkey_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
}
// Validate signature hex characters (lowercase) - 128 characters
for (int i = 0; i < 128; i++) {
char c = sig_str[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
}
}
// Validate created_at is a valid timestamp (positive number)
double created_at = cJSON_GetNumberValue(created_at_item);
if (created_at < 0) return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
// Validate kind is valid (0-65535)
double kind = cJSON_GetNumberValue(kind_item);
if (kind < 0 || kind > 65535 || kind != (int)kind) {
return NOSTR_ERROR_EVENT_INVALID_KIND;
}
// Validate tags array structure (array of arrays of strings)
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
cJSON* tag_element;
cJSON_ArrayForEach(tag_element, tag_item) {
if (!cJSON_IsString(tag_element)) {
return NOSTR_ERROR_EVENT_INVALID_TAGS;
}
}
}
return NOSTR_SUCCESS;
}
/**
* Verify the cryptographic signature of a NOSTR event
* Validates event ID and signature according to NIP-01
*/
int nostr_verify_event_signature(cJSON* event) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Get event fields
cJSON* id_item = cJSON_GetObjectItem(event, "id");
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
!tags_item || !content_item || !sig_item) {
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
cJSON_Delete(serialize_array);
if (!serialize_string) {
return NOSTR_ERROR_MEMORY_FAILED;
}
// Hash the serialized event
unsigned char event_hash[32];
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
free(serialize_string);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Convert hash to hex for event ID verification
char calculated_id[65];
nostr_bytes_to_hex(event_hash, 32, calculated_id);
// Compare with provided event ID
const char* provided_id = cJSON_GetStringValue(id_item);
if (!provided_id || strcmp(calculated_id, provided_id) != 0) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_ID;
}
// Verify signature
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
const char* sig_str = cJSON_GetStringValue(sig_item);
if (!pubkey_str || !sig_str) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
}
// Convert hex strings to bytes
unsigned char pubkey_bytes[32];
unsigned char sig_bytes[64];
if (nostr_hex_to_bytes(pubkey_str, pubkey_bytes, 32) != 0 ||
nostr_hex_to_bytes(sig_str, sig_bytes, 64) != 0) {
free(serialize_string);
return NOSTR_ERROR_CRYPTO_FAILED;
}
// Parse the public key into secp256k1 format
nostr_secp256k1_xonly_pubkey xonly_pubkey;
if (!nostr_secp256k1_xonly_pubkey_parse(&xonly_pubkey, pubkey_bytes)) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
}
// Verify Schnorr signature
if (!nostr_secp256k1_schnorrsig_verify(sig_bytes, event_hash, &xonly_pubkey)) {
free(serialize_string);
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
}
free(serialize_string);
return NOSTR_SUCCESS;
}
/**
* Complete validation of a NOSTR event
* Performs both structure and cryptographic validation
*/
int nostr_validate_event(cJSON* event) {
// First validate structure (fast check)
int structure_result = nostr_validate_event_structure(event);
if (structure_result != NOSTR_SUCCESS) {
return structure_result;
}
// Then verify signature (expensive check)
return nostr_verify_event_signature(event);
}

View File

@ -15,4 +15,9 @@
// Function declarations // Function declarations
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp); cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp);
// Event validation functions
int nostr_validate_event_structure(cJSON* event);
int nostr_verify_event_signature(cJSON* event);
int nostr_validate_event(cJSON* event);
#endif // NIP001_H #endif // NIP001_H

View File

@ -30,6 +30,14 @@ const char* nostr_strerror(int error_code) {
case NOSTR_ERROR_NIP05_JSON_PARSE_FAILED: return "NIP-05: JSON parsing failed"; case NOSTR_ERROR_NIP05_JSON_PARSE_FAILED: return "NIP-05: JSON parsing failed";
case NOSTR_ERROR_NIP05_NAME_NOT_FOUND: return "NIP-05: Name not found in .well-known"; case NOSTR_ERROR_NIP05_NAME_NOT_FOUND: return "NIP-05: Name not found in .well-known";
case NOSTR_ERROR_NIP05_PUBKEY_MISMATCH: return "NIP-05: Public key mismatch"; case NOSTR_ERROR_NIP05_PUBKEY_MISMATCH: return "NIP-05: Public key mismatch";
case NOSTR_ERROR_EVENT_INVALID_STRUCTURE: return "Event has invalid structure";
case NOSTR_ERROR_EVENT_INVALID_ID: return "Event has invalid ID";
case NOSTR_ERROR_EVENT_INVALID_PUBKEY: return "Event has invalid public key";
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: return "Event has invalid signature";
case NOSTR_ERROR_EVENT_INVALID_CREATED_AT: return "Event has invalid timestamp";
case NOSTR_ERROR_EVENT_INVALID_KIND: return "Event has invalid kind";
case NOSTR_ERROR_EVENT_INVALID_TAGS: return "Event has invalid tags";
case NOSTR_ERROR_EVENT_INVALID_CONTENT: return "Event has invalid content";
default: return "Unknown error"; default: return "Unknown error";
} }
} }

View File

@ -27,6 +27,14 @@
#define NOSTR_ERROR_NIP05_JSON_PARSE_FAILED -18 #define NOSTR_ERROR_NIP05_JSON_PARSE_FAILED -18
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19 #define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20 #define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
#define NOSTR_ERROR_EVENT_INVALID_ID -31
#define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32
#define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33
#define NOSTR_ERROR_EVENT_INVALID_CREATED_AT -34
#define NOSTR_ERROR_EVENT_INVALID_KIND -35
#define NOSTR_ERROR_EVENT_INVALID_TAGS -36
#define NOSTR_ERROR_EVENT_INVALID_CONTENT -37
// Constants // Constants

Binary file not shown.

BIN
tests/nip01_validation_test Executable file

Binary file not shown.

View File

@ -0,0 +1,494 @@
/*
* NIP-01 Event Validation Test Suite
* Tests event structure validation and cryptographic verification
* 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 "../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 vector 1: Valid event from nak (should pass all validation)
int test_valid_event_1(void) {
print_test_header("Valid Event from nak - Basic");
// Generated with: nak event --sec nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99 -c "Test event 1" -k 1
const char* event_json = "{"
"\"kind\":1,"
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599338,"
"\"tags\":[],"
"\"content\":\"Test event 1\","
"\"sig\":\"b9c7148e5a3e4ae1985a4b83a6317df72e2ef78dc4389063b7b83d5642aeccb5fb42f7e820ccfaf38c8077f9f6fef1fa1fd7d05c27c7fbc797cf53ee249ea640\""
"}";
printf("Input Event JSON:\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
printf("Testing Structure Validation...\n");
int structure_result = nostr_validate_event_structure(event);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", structure_result, nostr_strerror(structure_result));
if (structure_result != NOSTR_SUCCESS) {
printf("❌ Structure validation failed\n");
cJSON_Delete(event);
return 0;
}
printf("✅ Structure validation passed\n\n");
printf("Testing Cryptographic Verification...\n");
int crypto_result = nostr_verify_event_signature(event);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", crypto_result, nostr_strerror(crypto_result));
if (crypto_result != NOSTR_SUCCESS) {
printf("❌ Cryptographic verification failed\n");
cJSON_Delete(event);
return 0;
}
printf("✅ Cryptographic verification passed\n\n");
printf("Testing Complete Validation...\n");
int complete_result = nostr_validate_event(event);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", complete_result, nostr_strerror(complete_result));
cJSON_Delete(event);
return (complete_result == NOSTR_SUCCESS);
}
// Test vector 2: Valid event with tags (should pass all validation)
int test_valid_event_with_tags(void) {
print_test_header("Valid Event with Tags");
// Generated with: nak event --sec nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99 -c "Test event with tags" -k 1 -t "hello=world" -t "test=value"
const char* event_json = "{"
"\"kind\":1,"
"\"id\":\"781bf3185479315350c4039f719c3a8859a1ebb21a4b04e0e649addf0ae4665b\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599345,"
"\"tags\":[[\"hello\",\"world\"],[\"test\",\"value\"]],"
"\"content\":\"Test event with tags\","
"\"sig\":\"3a390ece9d746bd576cb8fbba6f9ed59730099781d0d414d1194f832ca072f23e7764935451a025399202e02f28a59fde2d4c25399e241f3de2ad0b3a4830d6a\""
"}";
printf("Input Event JSON:\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int result = nostr_validate_event(event);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
cJSON_Delete(event);
return (result == NOSTR_SUCCESS);
}
// Test vector 3: Valid metadata event (kind 0)
int test_valid_metadata_event(void) {
print_test_header("Valid Metadata Event (Kind 0)");
// Generated with: nak event --sec nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99 -c "" -k 0 --ts 1640995200
const char* event_json = "{"
"\"kind\":0,"
"\"id\":\"a2ed5175224f6597bc3a553e91d6fcea2e13f199e56afc90ccecca1c890b308a\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1640995200,"
"\"tags\":[],"
"\"content\":\"\","
"\"sig\":\"b9b2859ded73bbc010331145e2d297b30205d2b94d80ef0619077b687d53756b8d9eb3954ef1483b814059cd879c992153ca9d7b50dcf7053a3d530cd82fb884\""
"}";
printf("Input Event JSON:\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int result = nostr_validate_event(event);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
cJSON_Delete(event);
return (result == NOSTR_SUCCESS);
}
// Test 4: Missing required field (id)
int test_missing_id_field(void) {
print_test_header("Missing Required Field - ID");
const char* event_json = "{"
"\"kind\":1,"
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599338,"
"\"tags\":[],"
"\"content\":\"Test event 1\","
"\"sig\":\"b9c7148e5a3e4ae1985a4b83a6317df72e2ef78dc4389063b7b83d5642aeccb5fb42f7e820ccfaf38c8077f9f6fef1fa1fd7d05c27c7fbc797cf53ee249ea640\""
"}";
printf("Input Event JSON (missing 'id' field):\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int result = nostr_validate_event_structure(event);
printf("Expected: NOSTR_ERROR_EVENT_INVALID_STRUCTURE (-30)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
cJSON_Delete(event);
return (result == NOSTR_ERROR_EVENT_INVALID_STRUCTURE);
}
// Test 5: Invalid hex string length (pubkey too short)
int test_invalid_pubkey_length(void) {
print_test_header("Invalid Pubkey Length");
const char* event_json = "{"
"\"kind\":1,"
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7\"," // Too short (63 chars instead of 64)
"\"created_at\":1755599338,"
"\"tags\":[],"
"\"content\":\"Test event 1\","
"\"sig\":\"b9c7148e5a3e4ae1985a4b83a6317df72e2ef78dc4389063b7b83d5642aeccb5fb42f7e820ccfaf38c8077f9f6fef1fa1fd7d05c27c7fbc797cf53ee249ea640\""
"}";
printf("Input Event JSON (pubkey too short - 63 chars instead of 64):\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int result = nostr_validate_event_structure(event);
printf("Expected: NOSTR_ERROR_EVENT_INVALID_PUBKEY (-32)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
cJSON_Delete(event);
return (result == NOSTR_ERROR_EVENT_INVALID_PUBKEY);
}
// Test 6: Invalid kind (negative)
int test_invalid_kind_negative(void) {
print_test_header("Invalid Kind - Negative Value");
const char* event_json = "{"
"\"kind\":-1," // Invalid negative kind
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599338,"
"\"tags\":[],"
"\"content\":\"Test event 1\","
"\"sig\":\"b9c7148e5a3e4ae1985a4b83a6317df72e2ef78dc4389063b7b83d5642aeccb5fb42f7e820ccfaf38c8077f9f6fef1fa1fd7d05c27c7fbc797cf53ee249ea640\""
"}";
printf("Input Event JSON (kind = -1):\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int result = nostr_validate_event_structure(event);
printf("Expected: NOSTR_ERROR_EVENT_INVALID_KIND (-35)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
cJSON_Delete(event);
return (result == NOSTR_ERROR_EVENT_INVALID_KIND);
}
// Test 7: Invalid tags (not an array)
int test_invalid_tags_not_array(void) {
print_test_header("Invalid Tags - Not Array");
const char* event_json = "{"
"\"kind\":1,"
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599338,"
"\"tags\":\"not an array\"," // Should be array
"\"content\":\"Test event 1\","
"\"sig\":\"b9c7148e5a3e4ae1985a4b83a6317df72e2ef78dc4389063b7b83d5642aeccb5fb42f7e820ccfaf38c8077f9f6fef1fa1fd7d05c27c7fbc797cf53ee249ea640\""
"}";
printf("Input Event JSON (tags is string instead of array):\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int result = nostr_validate_event_structure(event);
printf("Expected: NOSTR_ERROR_EVENT_INVALID_TAGS (-36)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
cJSON_Delete(event);
return (result == NOSTR_ERROR_EVENT_INVALID_TAGS);
}
// Test 8: Wrong event ID (cryptographic test)
int test_wrong_event_id(void) {
print_test_header("Wrong Event ID - Cryptographic Test");
// Take valid event but change the ID to something incorrect
const char* event_json = "{"
"\"kind\":1,"
"\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\"," // Wrong ID
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599338,"
"\"tags\":[],"
"\"content\":\"Test event 1\","
"\"sig\":\"b9c7148e5a3e4ae1985a4b83a6317df72e2ef78dc4389063b7b83d5642aeccb5fb42f7e820ccfaf38c8077f9f6fef1fa1fd7d05c27c7fbc797cf53ee249ea640\""
"}";
printf("Input Event JSON (ID changed to all zeros):\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
// Structure should pass
int structure_result = nostr_validate_event_structure(event);
printf("Structure validation - Expected: NOSTR_SUCCESS (0)\n");
printf("Structure validation - Actual: %d (%s)\n", structure_result, nostr_strerror(structure_result));
if (structure_result != NOSTR_SUCCESS) {
printf("❌ Unexpected structure validation failure\n");
cJSON_Delete(event);
return 0;
}
// Crypto should fail
int crypto_result = nostr_verify_event_signature(event);
printf("Crypto verification - Expected: NOSTR_ERROR_EVENT_INVALID_ID (-31)\n");
printf("Crypto verification - Actual: %d (%s)\n", crypto_result, nostr_strerror(crypto_result));
cJSON_Delete(event);
return (crypto_result == NOSTR_ERROR_EVENT_INVALID_ID);
}
// Test 9: Invalid signature (cryptographic test)
int test_invalid_signature(void) {
print_test_header("Invalid Signature - Cryptographic Test");
// Take valid event but change the signature to a structurally valid but cryptographically wrong signature
const char* event_json = "{"
"\"kind\":1,"
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
"\"pubkey\":\"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\","
"\"created_at\":1755599338,"
"\"tags\":[],"
"\"content\":\"Test event 1\","
"\"sig\":\"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef\"" // Wrong but valid hex
"}";
printf("Input Event JSON (signature changed to invalid hex):\n%s\n\n", event_json);
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
// Structure should pass
int structure_result = nostr_validate_event_structure(event);
printf("Structure validation - Expected: NOSTR_SUCCESS (0)\n");
printf("Structure validation - Actual: %d (%s)\n", structure_result, nostr_strerror(structure_result));
if (structure_result != NOSTR_SUCCESS) {
printf("❌ Unexpected structure validation failure\n");
cJSON_Delete(event);
return 0;
}
// Crypto should fail
int crypto_result = nostr_verify_event_signature(event);
printf("Crypto verification - Expected: NOSTR_ERROR_EVENT_INVALID_SIGNATURE (-33)\n");
printf("Crypto verification - Actual: %d (%s)\n", crypto_result, nostr_strerror(crypto_result));
cJSON_Delete(event);
return (crypto_result == NOSTR_ERROR_EVENT_INVALID_SIGNATURE);
}
// Test 10: Integration test with our own event creation
int test_integration_with_event_creation(void) {
print_test_header("Integration Test - Validate Our Own Created Event");
// Create a test event using our existing function
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char private_key[32];
nostr_hex_to_bytes(private_key_hex, private_key, 32);
cJSON* tags = cJSON_CreateArray();
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("test"));
cJSON_AddItemToArray(tag, cJSON_CreateString("integration"));
cJSON_AddItemToArray(tags, tag);
printf("Creating event with our library...\n");
cJSON* created_event = nostr_create_and_sign_event(1, "Integration test message", tags, private_key, time(NULL));
if (!created_event) {
printf("❌ Event creation failed\n");
cJSON_Delete(tags);
return 0;
}
char* event_str = cJSON_Print(created_event);
printf("Created Event JSON:\n%s\n\n", event_str);
free(event_str);
printf("Validating our created event...\n");
int result = nostr_validate_event(created_event);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
if (result == NOSTR_SUCCESS) {
printf("✅ Our library creates valid events that pass validation\n");
} else {
printf("❌ Our own created event failed validation\n");
}
cJSON_Delete(created_event);
return (result == NOSTR_SUCCESS);
}
int main(void) {
printf("=== NIP-01 Event Validation Test Suite ===\n");
printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\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;
// Valid event tests
test_result = test_valid_event_1();
print_test_result(test_result, "Valid Event from nak - Basic");
if (!test_result) all_passed = 0;
test_result = test_valid_event_with_tags();
print_test_result(test_result, "Valid Event with Tags");
if (!test_result) all_passed = 0;
test_result = test_valid_metadata_event();
print_test_result(test_result, "Valid Metadata Event (Kind 0)");
if (!test_result) all_passed = 0;
// Invalid structure tests
test_result = test_missing_id_field();
print_test_result(test_result, "Missing Required Field - ID");
if (!test_result) all_passed = 0;
test_result = test_invalid_pubkey_length();
print_test_result(test_result, "Invalid Pubkey Length");
if (!test_result) all_passed = 0;
test_result = test_invalid_kind_negative();
print_test_result(test_result, "Invalid Kind - Negative Value");
if (!test_result) all_passed = 0;
test_result = test_invalid_tags_not_array();
print_test_result(test_result, "Invalid Tags - Not Array");
if (!test_result) all_passed = 0;
// Invalid cryptography tests
test_result = test_wrong_event_id();
print_test_result(test_result, "Wrong Event ID - Cryptographic Test");
if (!test_result) all_passed = 0;
test_result = test_invalid_signature();
print_test_result(test_result, "Invalid Signature - Cryptographic Test");
if (!test_result) all_passed = 0;
// Integration test
test_result = test_integration_with_event_creation();
print_test_result(test_result, "Integration Test - Validate Our Own Created Event");
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! Event validation implementation is working correctly.\n");
} else {
printf("❌ SOME TESTS FAILED. Please review the output above.\n");
}
nostr_cleanup();
return all_passed ? 0 : 1;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

129
todo.md
View File

@ -1,51 +1,110 @@
### __Tier 1: Foundational & High-Priority NIPs (The Essentials)__ # Nostr Event Validation Implementation Checklist
These are fundamental features that most Nostr applications rely on. ## Implementation Plan: NIP-001 Event Validation
| NIP | Description | `nostr_core_lib` Status | `nak` / `nostr-tools` Status | Recommendation | | :-- | :--- | :--- | :--- | :--- | | __NIP-04__ | __Encrypted Direct Messages__ | ❌ __Missing__ | ✅ __Supported__ | __High Priority.__ Essential for private communication. The `encrypt` / `decrypt` functions are a must-have. | | __NIP-05__ | __Mapping to DNS-based Names__ | ❌ __Missing__ | ✅ __Supported__ | __High Priority.__ Needed for user-friendly identifiers (e.g., `user@domain.com`). We should add a function to verify these. | | __NIP-07__ | __`window.nostr` for Web Browsers__ | ❌ __Missing__ | ✅ __Supported__ | __Medium Priority (for C).__ This is browser-specific, but we should consider adding helper functions to format requests for browser extensions that implement NIP-07. | | __NIP-11__ | __Relay Information Document__ | ❌ __Missing__ | ✅ __Supported__ | __High Priority.__ Crucial for client-side relay discovery and capability checking. A function to fetch and parse this JSON is needed. | | __NIP-21__ | __`nostr:` URI Scheme__ | ❌ __Missing__ | ✅ __Supported__ | __Medium Priority.__ Useful for linking to profiles, events, etc. from outside Nostr clients. We should add parsing and generation functions. | | __NIP-57__| __Lightning Zaps__ | ❌ __Missing__ | ✅ __Supported__ | __High Priority.__ Zaps are a cornerstone of value-for-value on Nostr. We need functions to create and verify zap request events (Kind 9734) and zap receipt events (Kind 9735). | ### 1. Create Test Suite `tests/nip01_validation_test.c` (FIRST - Test-Driven Development)
- [x] Use `nak` command line tool to generate valid test events
- [x] Create test vectors with known valid events
- [x] Test valid event validation (should pass)
- [x] Test invalid structure cases:
- Missing required fields
- Wrong field types
- Invalid hex string lengths
- Invalid timestamps
- Invalid kind values
- Invalid tag structures
- [x] Test invalid cryptographic cases:
- Wrong event ID
- Invalid signature
- Mismatched pubkey
- [x] Test edge cases and boundary conditions
- [x] Follow TESTS POLICY: Show expected vs actual values, print full JSON events
--- ### 2. Add Error Codes to `nostr_core/nostr_common.h`
- [x] Add validation-specific error codes after existing NIP error codes (line ~21):
```c
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
#define NOSTR_ERROR_EVENT_INVALID_ID -31
#define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32
#define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33
#define NOSTR_ERROR_EVENT_INVALID_CREATED_AT -34
#define NOSTR_ERROR_EVENT_INVALID_KIND -35
#define NOSTR_ERROR_EVENT_INVALID_TAGS -36
#define NOSTR_ERROR_EVENT_INVALID_CONTENT -37
```
### __Tier 2: Advanced & Ecosystem NIPs (Expanding Capabilities)__ ### 3. Update Error String Function in `nostr_core/nostr_common.c`
- [ ] Add cases for new error codes in `nostr_strerror()` function
These features enable more complex interactions and integrations. ### 4. Add Function Declarations to `nostr_core/nip001.h`
- [x] Add validation function declarations after existing function:
```c
// Event validation functions
int nostr_validate_event_structure(cJSON* event);
int nostr_verify_event_signature(cJSON* event);
int nostr_validate_event(cJSON* event);
```
| NIP | Description | `nostr_core_lib` Status | `nak` / `nostr-tools` Status | Recommendation | | :-- | :--- | :--- | :--- | :--- | | __NIP-25__ | __Reactions__ | ❌ __Missing__ | ✅ __Supported__ | __Medium Priority.__ Relatively simple to implement (Kind 7 events) and common in social clients. | | __NIP-26__ | __Delegated Event Signing__| ❌ __Missing__| ✅ __Supported__| __Medium Priority.__ Allows for one key to authorize another to sign events, useful for temporary or restricted keys. | | __NIP-44__ | __Versioned Encryption (v2)__ | ❌ __Missing__ | ✅ __Supported__ | __High Priority (along with NIP-04).__ Newer standard for encryption. We should aim to support both, but prioritize NIP-44 for new applications. | | __NIP-46__ | __Nostr Connect (Remote Signing)__| ❌ __Missing__ | ✅ __Supported__ | __Low Priority (for C library).__ More of an application-level concern, but we could provide structures and helpers to facilitate communication with signing "bunkers". | | __NIP-47__ | __Nostr Wallet Connect__ | ❌ __Missing__ | ✅ __Supported__| __Low Priority (for C library).__ Similar to NIP-46, this is primarily for app-level integration, but helpers would be valuable. | | __NIP-58__ | __Badges__ | ❌ __Missing__ | ✅ __Supported__ | __Medium Priority.__ Defines how to award and display badges (Kind 30008/30009 events). | | __NIP-94__ | __File Metadata__ | ❌ __Missing__ | ✅ __Supported__| __Medium Priority.__ For events that reference external files (images, videos), providing metadata like hashes and URLs. | | __NIP-98__ | __HTTP Auth__| ❌ __Missing__ | ✅ __Supported__| __High Priority.__ Allows services to authenticate users via a Nostr event, a powerful tool for integrating Nostr identity with web services. | ### 5. Implement Functions in `nostr_core/nip001.c`
- [ ] **`nostr_validate_event_structure()`** - Structure validation:
- Check required fields exist: id, pubkey, created_at, kind, tags, content, sig
- Validate field types (strings, numbers, arrays)
- Validate hex string formats (id: 64 chars, pubkey: 64 chars, sig: 128 chars)
- Validate created_at is valid timestamp
- Validate kind is valid integer (0-65535)
- Validate tags is array of string arrays
- Validate content is string
--- - [ ] **`nostr_verify_event_signature()`** - Cryptographic verification:
- Generate serialized event string: `[0,<pubkey>,<created_at>,<kind>,<tags>,<content>]`
- Calculate SHA-256 hash of serialized event
- Convert hash to hex string and compare with event.id
- Verify Schnorr signature using existing `nostr_schnorr_verify()` from utils.h
- Use hex conversion functions from utils.h
### __Tier 3: Specialized & Less Common NIPs (Future Considerations)__ - [ ] **`nostr_validate_event()`** - Complete validation:
- Call `nostr_validate_event_structure()` first
- If structure valid, call `nostr_verify_event_signature()`
- Return appropriate error codes
| NIP | Description | `nostr_core_lib` Status | `nak` / `nostr-tools` Status | Recommendation | | :-- | :--- | :--- | :--- | :--- | | __NIP-18__| __Reposts__ | ❌ __Missing__ | ✅ __Supported__| __Low Priority.__ Simple to implement (Kind 6 events) but less critical than other features. | | __NIP-28__| __Public Chat Channels__| ❌ __Missing__ | ✅ __Supported__| __Low Priority.__ Defines event kinds for creating and managing public chat channels. | | __NIP-39__| __External Identities__ | ❌ __Missing__ | ✅ __Supported__| __Low Priority.__ For linking a Nostr profile to identities on other platforms like GitHub or Twitter. | | __NIP-42__ | __Relay Authentication__ | ❌ __Missing__| ✅ __Supported__| __Medium Priority.__ For paid relays or those requiring login. Essential for interacting with the full relay ecosystem. | | __NIP-43__| __Musig2 (Multisig)__ | ❌ __Missing__ | ✅ __Supported__| __Low Priority.__ A very powerful but niche feature. Complex to implement correctly. | ### 6. Update Build System
- [ ] Ensure new test compiles with existing build.sh
- [ ] Test compilation of all new code
--- ### 7. Integration Testing
- [ ] Test with real Nostr events from network
- [ ] Test with events created by existing `nostr_create_and_sign_event()`
- [ ] Verify compatibility with existing relay functions
### __Proposed Roadmap__ ## Technical Implementation Details
Based on this analysis, I recommend the following roadmap, prioritized by impact and foundational need: ### Required Dependencies (Already Available):
- `nostr_sha256()` from `nostr_core/utils.h`
- `nostr_schnorr_verify()` from `nostr_core/utils.h`
- `nostr_hex_to_bytes()` from `nostr_core/utils.h`
- `nostr_bytes_to_hex()` from `nostr_core/utils.h`
- cJSON library for JSON parsing
1. __Immediate Next Steps (High-Impact Basics):__ ### Validation Logic Based on NIP-01 and nostr-tools Reference:
1. **Structure Validation**: Fast checks on JSON structure and basic format
2. **Cryptographic Validation**: Expensive signature verification only after structure passes
3. **Two-tier approach**: Allows early exit on malformed events
- __Implement NIP-04 & NIP-44:__ Encrypted messaging is a huge gap. ### Error Handling Strategy:
- __Implement NIP-11:__ Fetching and parsing relay info documents. - Return specific error codes for different validation failures
- __Implement NIP-05:__ DNS-based name verification. - Enable caller to understand exactly what failed
- __Implement NIP-57:__ Create and verify Zap events. - Maintain consistency with existing error code patterns
- __Implement NIP-98:__ HTTP Auth for web services.
2. __Phase 2 (Ecosystem & Social Features):__ ## Files to Modify:
- `nostr_core/nostr_common.h` (add error codes)
- `nostr_core/nostr_common.c` (update error strings)
- `nostr_core/nip001.h` (add function declarations)
- `nostr_core/nip001.c` (implement functions)
- `tests/nip01_validation_test.c` (create new file)
- __Implement NIP-25:__ Reactions. ## Testing Priority:
- __Implement NIP-26:__ Delegation. 1. Structure validation with malformed events
- __Implement NIP-42:__ Relay Authentication. 2. Cryptographic validation with tampered events
- __Implement NIP-21:__ `nostr:` URI handling. 3. Valid event validation end-to-end
- __Implement NIP-58:__ Badges. 4. Integration with existing event creation functions
5. Performance testing with large numbers of events
3. __Phase 3 (Advanced & Specialized Features):__
- __Implement NIP-18:__ Reposts.
- __Implement NIP-43:__ Musig2 (this is a big one).
- Add helpers for NIP-07, NIP-46, and NIP-47.
What are your thoughts on this assessment and proposed roadmap? We can adjust the priorities based on your goals for the library. Once we have a plan you're happy with, I can start implementing the first features when you're ready to switch to
Act Mode (⌘⇧A).