/* * NOSTR Core Library - NIP-001: Basic Protocol Flow * * Event creation, signing, serialization and core protocol functions */ #include "nip001.h" #include "utils.h" #include "../cjson/cJSON.h" #include #include #include #include #include "../nostr_core/nostr_common.h" // Forward declarations for crypto functions (private API) // These functions are implemented in crypto/ but not exposed through public headers typedef struct { unsigned char data[64]; } nostr_secp256k1_xonly_pubkey; int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey* pubkey, const unsigned char* input32); int nostr_secp256k1_schnorrsig_verify(const unsigned char* sig64, const unsigned char* msg32, const nostr_secp256k1_xonly_pubkey* pubkey); // Declare utility functions void nostr_bytes_to_hex(const unsigned char* bytes, size_t len, char* hex); int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t len); int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash); int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key); int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature); /** * Create and sign a NOSTR event */ cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) { if (!private_key) { return NULL; } if (!content) { content = ""; // Default to empty content } // Convert private key to public key unsigned char public_key[32]; if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) { return NULL; } // Convert public key to hex char pubkey_hex[65]; nostr_bytes_to_hex(public_key, 32, pubkey_hex); // Create event structure cJSON* event = cJSON_CreateObject(); if (!event) { return NULL; } // Use provided timestamp or current time if timestamp is 0 time_t event_time = (timestamp == 0) ? time(NULL) : timestamp; cJSON_AddStringToObject(event, "pubkey", pubkey_hex); cJSON_AddNumberToObject(event, "created_at", (double)event_time); cJSON_AddNumberToObject(event, "kind", kind); // Add tags (copy provided tags or create empty array) if (tags) { cJSON_AddItemToObject(event, "tags", cJSON_Duplicate(tags, 1)); } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } cJSON_AddStringToObject(event, "content", content); // ============================================================================ // INLINE SERIALIZATION AND SIGNING LOGIC // ============================================================================ // Get event fields for serialization 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"); if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) { cJSON_Delete(event); return NULL; } // Create serialization array: [0, pubkey, created_at, kind, tags, content] cJSON* serialize_array = cJSON_CreateArray(); if (!serialize_array) { cJSON_Delete(event); return NULL; } 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) { cJSON_Delete(event); return NULL; } // 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); cJSON_Delete(event); return NULL; } // Convert hash to hex for event ID char event_id[65]; nostr_bytes_to_hex(event_hash, 32, event_id); // Sign the hash using ECDSA unsigned char signature[64]; if (nostr_ec_sign(private_key, event_hash, signature) != 0) { free(serialize_string); cJSON_Delete(event); return NULL; } // Convert signature to hex char sig_hex[129]; nostr_bytes_to_hex(signature, 64, sig_hex); // Add ID and signature to the event cJSON_AddStringToObject(event, "id", event_id); cJSON_AddStringToObject(event, "sig", sig_hex); free(serialize_string); 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); }