Compare commits

...

7 Commits

Author SHA1 Message Date
d52ac9c106 Merge branch 'master' of ssh://git.laantungir.net:222/laantungir/nostr_core_lib
Working on Ginxsom auth and NIP 42 on a different computer at the same
time. My bad.
2025-09-07 13:07:02 -04:00
e087a17eb9 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
2025-09-07 13:04:53 -04:00
Your Name
d7e43328bb Add unified request validation system with authentication rules
- Add request_validator.h/c with comprehensive authentication system
- Implement pluggable database backend interface (SQLite default)
- Add priority-based rule evaluation engine with caching
- Support pubkey whitelist/blacklist, hash blacklist, MIME restrictions
- Add new authentication error codes to nostr_common.h
- Include request_validator.h in nostr_core.h
- Update build.sh to compile request_validator.c
- Designed for shared use between ginxsom and c-relay projects
2025-09-07 09:44:38 -04:00
0af77dffab nip13 validation added 2025-09-05 13:42:03 -04:00
0f72d7433a readme.md 2025-09-03 15:16:08 -04:00
2f3140d4f6 remove exposed .h crypto headers 2025-09-02 12:36:52 -04:00
31947d12b7 Streaming sha256 2025-08-19 11:24:48 -04:00
33 changed files with 5228 additions and 888 deletions

2
.gitignore vendored
View File

@@ -7,7 +7,7 @@ nips/
node_modules/
nostr-tools/
tiny-AES-c/
blossom/
Trash/debug_tests/

133
README.md
View File

@@ -1,46 +1,103 @@
# NOSTR Core Library
A comprehensive, production-ready C library for NOSTR protocol implementation with OpenSSL-based cryptography and extensive protocol support.
A C library for NOSTR protocol implementation. Work in progress.
[![Version](https://img.shields.io/badge/version-0.1.20-blue.svg)](VERSION)
[![Version](https://img.shields.io/badge/version-0.2.1-blue.svg)](VERSION)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](#license)
[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](#building)
## 🚀 Features
### Core Protocol Support
- **NIP-01**: Basic protocol flow - event creation, signing, and validation
- **NIP-04**: Encrypted direct messages (ECDH + AES-CBC + Base64)
- **NIP-05**: DNS-based internet identifier verification
- **NIP-06**: Key derivation from mnemonic (BIP39/BIP32 compliant)
- **NIP-11**: Relay information documents
- **NIP-13**: Proof of Work for events
- **NIP-44**: Versioned encrypted direct messages (ECDH + ChaCha20 + HMAC)
## 📋 NIP Implementation Status
### Cryptographic Features
- **OpenSSL-Based**: Production-grade cryptography with OpenSSL backend
- **Secp256k1**: Complete elliptic curve implementation bundled
- **BIP39**: Mnemonic phrase generation and validation
- **BIP32**: Hierarchical deterministic key derivation
- **ChaCha20**: Stream cipher for NIP-44 encryption
- **AES-CBC**: Block cipher for NIP-04 encryption
- **Schnorr Signatures**: BIP-340 compliant signing and verification
### Core Protocol NIPs
- [x] [NIP-01](nips/01.md) - Basic protocol flow - event creation, signing, and validation
- [ ] [NIP-02](nips/02.md) - Contact List and Petnames
- [ ] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
- [x] [NIP-04](nips/04.md) - Encrypted Direct Messages (legacy)
- [x] [NIP-05](nips/05.md) - Mapping Nostr keys to DNS-based internet identifiers
- [x] [NIP-06](nips/06.md) - Basic key derivation from mnemonic seed phrase
- [ ] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
- [ ] [NIP-08](nips/08.md) - Handling Mentions
- [ ] [NIP-09](nips/09.md) - Event Deletion
- [ ] [NIP-10](nips/10.md) - Conventions for clients' use of `e` and `p` tags in text events
- [x] [NIP-11](nips/11.md) - Relay Information Document
- [ ] [NIP-12](nips/12.md) - Generic Tag Queries
- [x] [NIP-13](nips/13.md) - Proof of Work
- [ ] [NIP-14](nips/14.md) - Subject tag in text events
- [ ] [NIP-15](nips/15.md) - Nostr Marketplace (for resilient marketplaces)
- [ ] [NIP-16](nips/16.md) - Event Treatment
- [ ] [NIP-17](nips/17.md) - Private Direct Messages
- [ ] [NIP-18](nips/18.md) - Reposts
- [x] [NIP-19](nips/19.md) - bech32-encoded entities
- [ ] [NIP-20](nips/20.md) - Command Results
- [ ] [NIP-21](nips/21.md) - `nostr:` URI scheme
- [ ] [NIP-22](nips/22.md) - Event `created_at` Limits
- [ ] [NIP-23](nips/23.md) - Long-form Content
- [ ] [NIP-24](nips/24.md) - Extra metadata fields and tags
- [ ] [NIP-25](nips/25.md) - Reactions
- [ ] [NIP-26](nips/26.md) - Delegated Event Signing
- [ ] [NIP-27](nips/27.md) - Text Note References
- [ ] [NIP-28](nips/28.md) - Public Chat
- [ ] [NIP-29](nips/29.md) - Relay-based Groups
- [ ] [NIP-30](nips/30.md) - Custom Emoji
- [ ] [NIP-31](nips/31.md) - Dealing with Unknown Events
- [ ] [NIP-32](nips/32.md) - Labeling
- [ ] [NIP-33](nips/33.md) - Parameterized Replaceable Events
- [ ] [NIP-34](nips/34.md) - `git` stuff
- [ ] [NIP-35](nips/35.md) - Torrents
- [ ] [NIP-36](nips/36.md) - Sensitive Content / Content Warning
- [ ] [NIP-37](nips/37.md) - Draft Events
- [ ] [NIP-38](nips/38.md) - User Statuses
- [ ] [NIP-39](nips/39.md) - External Identities in Profiles
- [ ] [NIP-40](nips/40.md) - Expiration Timestamp
- [ ] [NIP-42](nips/42.md) - Authentication of clients to relays
- [x] [NIP-44](nips/44.md) - Versioned Encryption
- [ ] [NIP-45](nips/45.md) - Counting results
- [ ] [NIP-46](nips/46.md) - Nostr Connect
- [ ] [NIP-47](nips/47.md) - Wallet Connect
- [ ] [NIP-48](nips/48.md) - Proxy Tags
- [ ] [NIP-49](nips/49.md) - Private Key Encryption
- [ ] [NIP-50](nips/50.md) - Search Capability
- [ ] [NIP-51](nips/51.md) - Lists
- [ ] [NIP-52](nips/52.md) - Calendar Events
- [ ] [NIP-53](nips/53.md) - Live Activities
- [ ] [NIP-54](nips/54.md) - Wiki
- [ ] [NIP-55](nips/55.md) - Android Signer Application
- [ ] [NIP-56](nips/56.md) - Reporting
- [ ] [NIP-57](nips/57.md) - Lightning Zaps
- [ ] [NIP-58](nips/58.md) - Badges
- [ ] [NIP-59](nips/59.md) - Gift Wrap
- [ ] [NIP-60](nips/60.md) - Cashu Wallet
- [ ] [NIP-61](nips/61.md) - Nutzaps
- [ ] [NIP-62](nips/62.md) - Log events
- [ ] [NIP-64](nips/64.md) - Chess (PGN)
- [ ] [NIP-65](nips/65.md) - Relay List Metadata
- [ ] [NIP-66](nips/66.md) - Relay Monitor
- [ ] [NIP-68](nips/68.md) - Web badges
- [ ] [NIP-69](nips/69.md) - Peer-to-peer Order events
- [ ] [NIP-70](nips/70.md) - Protected Events
- [ ] [NIP-71](nips/71.md) - Video Events
- [ ] [NIP-72](nips/72.md) - Moderated Communities
- [ ] [NIP-73](nips/73.md) - External Content IDs
- [ ] [NIP-75](nips/75.md) - Zap Goals
- [ ] [NIP-77](nips/77.md) - Arbitrary custom app data
- [ ] [NIP-78](nips/78.md) - Application-specific data
- [ ] [NIP-84](nips/84.md) - Highlights
- [ ] [NIP-86](nips/86.md) - Relay Management API
- [ ] [NIP-87](nips/87.md) - Relay List Recommendations
- [ ] [NIP-88](nips/88.md) - Stella: A Stellar Relay
- [ ] [NIP-89](nips/89.md) - Recommended Application Handlers
- [ ] [NIP-90](nips/90.md) - Data Vending Machines
- [ ] [NIP-92](nips/92.md) - Media Attachments
- [ ] [NIP-94](nips/94.md) - File Metadata
- [ ] [NIP-96](nips/96.md) - HTTP File Storage Integration
- [ ] [NIP-98](nips/98.md) - HTTP Auth
- [ ] [NIP-99](nips/99.md) - Classified Listings
### Networking & Relay Support
- **Multi-Relay Queries**: Synchronous querying with progress callbacks
- **Relay Pools**: Asynchronous connection management with statistics
- **OpenSSL WebSocket Communication**: Full relay protocol support with TLS
- **NIP-05 Identifier Verification**: DNS-based identity resolution
- **NIP-11 Relay Information**: Automatic relay capability discovery
- **Event Deduplication**: Automatic handling of duplicate events across relays
- **Connection Management**: Automatic reconnection and error handling
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
**Implementation Summary:** 8 of 96+ NIPs fully implemented (8.3%)
### Developer Experience
- **System Dependencies**: Uses system-installed OpenSSL, curl, and secp256k1 libraries
- **Thread-Safe**: Core cryptographic functions are stateless
- **Cross-Platform**: Builds on Linux, macOS, Windows
- **Comprehensive Examples**: Ready-to-run demonstration programs
- **Automatic Versioning**: Git-tag based version management
## 📦 Quick Start
@@ -434,7 +491,7 @@ make arm64
## 📈 Version History
Current version: **0.1.20**
Current version: **0.2.1**
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
@@ -442,13 +499,15 @@ The library uses automatic semantic versioning based on Git tags. Each build inc
- **OpenSSL Migration**: Transitioned from mbedTLS to OpenSSL for improved compatibility
- **NIP-05 Support**: DNS-based internet identifier verification
- **NIP-11 Support**: Relay information document fetching and parsing
- **NIP-19 Support**: Bech32-encoded entities (nsec/npub)
- **Enhanced WebSocket**: OpenSSL-based TLS WebSocket communication
- **Production Ready**: Comprehensive test suite and error handling
- **Comprehensive Testing**: Extensive test suite and error handling
**Version Timeline:**
- `v0.2.x` - Current development releases with enhanced NIP support
- `v0.1.x` - Initial development releases
- Focus on core protocol implementation and OpenSSL-based crypto
- Full NIP-01, NIP-04, NIP-05, NIP-06, NIP-11, NIP-13, NIP-44 support
- Full NIP-01, NIP-04, NIP-05, NIP-06, NIP-11, NIP-13, NIP-19, NIP-44 support
## 🐛 Troubleshooting
@@ -496,4 +555,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
**Built with ❤️ for the decentralized web**
*OpenSSL-based • Minimal dependencies • Production ready*
*OpenSSL-based • Minimal dependencies • Work in progress*

View File

@@ -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
@@ -484,13 +485,14 @@ detect_system_curl
###########################################################################################
SOURCES="nostr_core/crypto/nostr_secp256k1.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c"
SOURCES="$SOURCES cjson/cJSON.c"
SOURCES="$SOURCES nostr_core/utils.c"
SOURCES="$SOURCES nostr_core/nostr_common.c"
SOURCES="$SOURCES nostr_core/core_relays.c"
SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c"
SOURCES="$SOURCES nostr_core/request_validator.c"
NIP_DESCRIPTIONS=""
@@ -506,6 +508,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

View File

@@ -1,15 +1,76 @@
/*
* NOSTR AES Implementation
*
*
* Based on tiny-AES-c by kokke (public domain)
* Configured specifically for NIP-04: AES-256-CBC only
*
*
* This is an implementation of the AES algorithm, specifically CBC mode.
* Configured for AES-256 as required by NIP-04.
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h> // CBC mode, for memset
#include "nostr_aes.h"
// Configure for NIP-04 requirements: AES-256-CBC only
#define CBC 1
#define ECB 0
#define CTR 0
// Configure for AES-256 (required by NIP-04)
#define AES128 0
#define AES192 0
#define AES256 1
#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only
#if defined(AES256) && (AES256 == 1)
#define AES_KEYLEN 32
#define AES_keyExpSize 240
#elif defined(AES192) && (AES192 == 1)
#define AES_KEYLEN 24
#define AES_keyExpSize 208
#else
#define AES_KEYLEN 16 // Key length in bytes
#define AES_keyExpSize 176
#endif
struct AES_ctx
{
uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
// state - array holding the intermediate results during decryption.
typedef uint8_t state_t[4][4];
// Function prototypes (internal use only)
static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key);
static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey);
static void SubBytes(state_t* state);
static void ShiftRows(state_t* state);
static uint8_t xtime(uint8_t x);
static void MixColumns(state_t* state);
static void InvMixColumns(state_t* state);
static void InvSubBytes(state_t* state);
static void InvShiftRows(state_t* state);
static void Cipher(state_t* state, const uint8_t* RoundKey);
static void InvCipher(state_t* state, const uint8_t* RoundKey);
static void XorWithIv(uint8_t* buf, const uint8_t* Iv);
// Public functions (used by NIP-04 implementation)
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif
#if defined(CBC) && (CBC == 1)
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif
// The number of columns comprising a state in AES. This is a constant in AES. Value=4
#define Nb 4

View File

@@ -1,53 +0,0 @@
#ifndef _NOSTR_AES_H_
#define _NOSTR_AES_H_
#include <stdint.h>
#include <stddef.h>
// Configure for NIP-04 requirements: AES-256-CBC only
#define CBC 1
#define ECB 0
#define CTR 0
// Configure for AES-256 (required by NIP-04)
#define AES128 0
#define AES192 0
#define AES256 1
#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only
#if defined(AES256) && (AES256 == 1)
#define AES_KEYLEN 32
#define AES_keyExpSize 240
#elif defined(AES192) && (AES192 == 1)
#define AES_KEYLEN 24
#define AES_keyExpSize 208
#else
#define AES_KEYLEN 16 // Key length in bytes
#define AES_keyExpSize 176
#endif
struct AES_ctx
{
uint8_t RoundKey[AES_keyExpSize];
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
uint8_t Iv[AES_BLOCKLEN];
#endif
};
void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
#endif
#if defined(CBC) && (CBC == 1)
// buffer size MUST be multiple of AES_BLOCKLEN;
// Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme
// NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv()
// no IV should ever be reused with the same key
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
#endif // #if defined(CBC) && (CBC == 1)
#endif // _NOSTR_AES_H_

View File

@@ -1,15 +1,47 @@
/*
* nostr_chacha20.c - ChaCha20 stream cipher implementation
*
*
* Implementation based on RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols"
*
*
* This implementation is adapted from the RFC 8439 reference specification.
* It prioritizes correctness and clarity over performance optimization.
*/
#include "nostr_chacha20.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
/*
* ============================================================================
* CONSTANTS AND DEFINITIONS
* ============================================================================
*/
#define CHACHA20_KEY_SIZE 32 /* 256 bits */
#define CHACHA20_NONCE_SIZE 12 /* 96 bits */
#define CHACHA20_BLOCK_SIZE 64 /* 512 bits */
/*
* ============================================================================
* FUNCTION PROTOTYPES (INTERNAL USE ONLY)
* ============================================================================
*/
// Internal utility functions
static uint32_t bytes_to_u32_le(const uint8_t *bytes);
static void u32_to_bytes_le(uint32_t val, uint8_t *bytes);
// Public functions (used by NIP-44 implementation)
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d);
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]);
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], const uint8_t* input,
uint8_t* output, size_t length);
void chacha20_init_state(uint32_t state[16], const uint8_t key[32],
uint32_t counter, const uint8_t nonce[12]);
void chacha20_serialize_state(const uint32_t state[16], uint8_t output[64]);
/*
* ============================================================================
* UTILITY MACROS AND FUNCTIONS

View File

@@ -1,115 +0,0 @@
/*
* nostr_chacha20.h - ChaCha20 stream cipher implementation
*
* Implementation based on RFC 8439 "ChaCha20 and Poly1305 for IETF Protocols"
*
* This is a small, portable implementation for NIP-44 support in the NOSTR library.
* The implementation prioritizes correctness and simplicity over performance.
*/
#ifndef NOSTR_CHACHA20_H
#define NOSTR_CHACHA20_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* ============================================================================
* CONSTANTS AND DEFINITIONS
* ============================================================================
*/
#define CHACHA20_KEY_SIZE 32 /* 256 bits */
#define CHACHA20_NONCE_SIZE 12 /* 96 bits */
#define CHACHA20_BLOCK_SIZE 64 /* 512 bits */
/*
* ============================================================================
* CORE CHACHA20 FUNCTIONS
* ============================================================================
*/
/**
* ChaCha20 quarter round operation
*
* Operates on four 32-bit words performing the core ChaCha20 quarter round:
* a += b; d ^= a; d <<<= 16;
* c += d; b ^= c; b <<<= 12;
* a += b; d ^= a; d <<<= 8;
* c += d; b ^= c; b <<<= 7;
*
* @param state[in,out] ChaCha state as 16 32-bit words
* @param a, b, c, d Indices into state array for quarter round
*/
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d);
/**
* ChaCha20 block function
*
* Transforms a 64-byte input block using ChaCha20 algorithm with 20 rounds.
*
* @param key[in] 32-byte key
* @param counter[in] 32-bit block counter
* @param nonce[in] 12-byte nonce
* @param output[out] 64-byte output buffer
* @return 0 on success, negative on error
*/
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]);
/**
* ChaCha20 encryption/decryption
*
* Encrypts or decrypts data using ChaCha20 stream cipher.
* Since ChaCha20 is a stream cipher, encryption and decryption are the same operation.
*
* @param key[in] 32-byte key
* @param counter[in] Initial 32-bit counter value
* @param nonce[in] 12-byte nonce
* @param input[in] Input data to encrypt/decrypt
* @param output[out] Output buffer (can be same as input)
* @param length[in] Length of input data in bytes
* @return 0 on success, negative on error
*/
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], const uint8_t* input,
uint8_t* output, size_t length);
/*
* ============================================================================
* UTILITY FUNCTIONS
* ============================================================================
*/
/**
* Initialize ChaCha20 state matrix
*
* Sets up the initial 16-word state matrix with constants, key, counter, and nonce.
*
* @param state[out] 16-word state array to initialize
* @param key[in] 32-byte key
* @param counter[in] 32-bit block counter
* @param nonce[in] 12-byte nonce
*/
void chacha20_init_state(uint32_t state[16], const uint8_t key[32],
uint32_t counter, const uint8_t nonce[12]);
/**
* Serialize ChaCha20 state to bytes
*
* Converts 16 32-bit words to 64 bytes in little-endian format.
*
* @param state[in] 16-word state array
* @param output[out] 64-byte output buffer
*/
void chacha20_serialize_state(const uint32_t state[16], uint8_t output[64]);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_CHACHA20_H */

View File

@@ -1,4 +1,3 @@
#include "nostr_secp256k1.h"
#include <secp256k1.h>
#include <secp256k1_schnorrsig.h>
#include <secp256k1_ecdh.h>
@@ -6,6 +5,33 @@
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
/*
* PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED
* These functions are for internal library use only.
*/
/** Opaque data structure that holds a parsed and valid public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_pubkey {
unsigned char data[64];
} nostr_secp256k1_pubkey;
/** Opaque data structure that holds a parsed keypair.
* Guaranteed to be 96 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_keypair {
unsigned char data[96];
} nostr_secp256k1_keypair;
/** Opaque data structure that holds a parsed x-only public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_xonly_pubkey {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
// Global context for secp256k1 operations
static secp256k1_context* g_ctx = NULL;

View File

@@ -1,141 +0,0 @@
#ifndef NOSTR_SECP256K1_H
#define NOSTR_SECP256K1_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
/** Opaque data structure that holds a parsed and valid public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_pubkey {
unsigned char data[64];
} nostr_secp256k1_pubkey;
/** Opaque data structure that holds a parsed keypair.
* Guaranteed to be 96 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_keypair {
unsigned char data[96];
} nostr_secp256k1_keypair;
/** Opaque data structure that holds a parsed x-only public key.
* Guaranteed to be 64 bytes in size, and can be safely copied/moved.
*/
typedef struct nostr_secp256k1_xonly_pubkey {
unsigned char data[64];
} nostr_secp256k1_xonly_pubkey;
/** Initialize the secp256k1 library. Must be called before any other functions.
* Returns: 1 on success, 0 on failure.
*/
int nostr_secp256k1_context_create(void);
/** Clean up the secp256k1 library resources.
*/
void nostr_secp256k1_context_destroy(void);
/** Verify an elliptic curve secret key.
* Returns: 1: secret key is valid, 0: secret key is invalid
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_ec_seckey_verify(const unsigned char *seckey);
/** Compute the public key for a secret key.
* Returns: 1: secret was valid, public key stored. 0: secret was invalid.
* Out: pubkey: pointer to the created public key.
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey);
/** Create a keypair from a secret key.
* Returns: 1: keypair created, 0: secret key invalid.
* Out: keypair: pointer to the created keypair.
* In: seckey: pointer to a 32-byte secret key.
*/
int nostr_secp256k1_keypair_create(nostr_secp256k1_keypair *keypair, const unsigned char *seckey);
/** Get the x-only public key from a keypair.
* Returns: 1 always.
* Out: pubkey: pointer to storage for the x-only public key.
* In: keypair: pointer to a keypair.
*/
int nostr_secp256k1_keypair_xonly_pub(nostr_secp256k1_xonly_pubkey *pubkey, const nostr_secp256k1_keypair *keypair);
/** Parse an x-only public key from bytes.
* Returns: 1: public key parsed, 0: invalid public key.
* Out: pubkey: pointer to the created x-only public key.
* In: input32: pointer to a 32-byte x-only public key.
*/
int nostr_secp256k1_xonly_pubkey_parse(nostr_secp256k1_xonly_pubkey *pubkey, const unsigned char *input32);
/** Serialize an x-only public key to bytes.
* Returns: 1 always.
* Out: output32: pointer to a 32-byte array to store the serialized key.
* In: pubkey: pointer to an x-only public key.
*/
int nostr_secp256k1_xonly_pubkey_serialize(unsigned char *output32, const nostr_secp256k1_xonly_pubkey *pubkey);
/** Create a Schnorr signature.
* Returns: 1: signature created, 0: nonce generation failed or secret key invalid.
* Out: sig64: pointer to a 64-byte array where the signature will be placed.
* In: msghash32: the 32-byte message hash being signed.
* keypair: pointer to an initialized keypair.
* aux_rand32: pointer to 32 bytes of auxiliary randomness (can be NULL).
*/
int nostr_secp256k1_schnorrsig_sign32(unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_keypair *keypair, const unsigned char *aux_rand32);
/** Verify a Schnorr signature.
* Returns: 1: correct signature, 0: incorrect signature
* In: sig64: pointer to the 64-byte signature being verified.
* msghash32: the 32-byte message hash being verified.
* pubkey: pointer to an x-only public key to verify with.
*/
int nostr_secp256k1_schnorrsig_verify(const unsigned char *sig64, const unsigned char *msghash32, const nostr_secp256k1_xonly_pubkey *pubkey);
/** Serialize a pubkey object into a serialized byte sequence.
* Returns: 1 always.
* Out: output: pointer to a 33-byte array to place the serialized key in.
* In: pubkey: pointer to a secp256k1_pubkey containing an initialized public key.
*
* The output will be a 33-byte compressed public key (0x02 or 0x03 prefix + 32 bytes x coordinate).
*/
int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char *output, const nostr_secp256k1_pubkey *pubkey);
/** Tweak a secret key by adding a 32-byte tweak to it.
* Returns: 1: seckey was valid, 0: seckey invalid or resulting key invalid
* In/Out: seckey: pointer to a 32-byte secret key. Will be modified in-place.
* In: tweak: pointer to a 32-byte tweak.
*/
int nostr_secp256k1_ec_seckey_tweak_add(unsigned char *seckey, const unsigned char *tweak);
/** Parse a variable-length public key into the pubkey object.
* Returns: 1: public key parsed, 0: invalid public key.
* Out: pubkey: pointer to the created public key.
* In: input: pointer to a serialized public key
* inputlen: length of the array pointed to by input
*/
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen);
/** Compute an EC Diffie-Hellman secret in constant time.
* Returns: 1: exponentiation was successful, 0: scalar was invalid (zero or overflow)
* Out: result: a 32-byte array which will be populated by an ECDH secret computed from point and scalar
* In: pubkey: a pointer to a secp256k1_pubkey containing an initialized public key
* seckey: a 32-byte scalar with which to multiply the point
*/
int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pubkey, const unsigned char *seckey, void *hashfp, void *data);
/** Generate cryptographically secure random bytes.
* Returns: 1: success, 0: failure
* Out: buf: buffer to fill with random bytes
* In: len: number of bytes to generate
*/
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_SECP256K1_H */

View File

@@ -6,7 +6,6 @@
#include "nip001.h"
#include "utils.h"
#include "crypto/nostr_secp256k1.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
@@ -14,9 +13,21 @@
#include <time.h>
#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

View File

@@ -6,13 +6,24 @@
#include "nip004.h"
#include "utils.h"
#include "nostr_common.h"
#include "crypto/nostr_secp256k1.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Include our AES implementation
#include "crypto/nostr_aes.h"
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
int ecdh_shared_secret(const unsigned char* private_key, const unsigned char* public_key, unsigned char* shared_secret);
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// AES context and functions for NIP-04 encryption
struct AES_ctx {
unsigned char RoundKey[240]; // AES-256 key expansion
unsigned char Iv[16]; // Initialization vector
};
void AES_init_ctx_iv(struct AES_ctx* ctx, const unsigned char* key, const unsigned char* iv);
void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, unsigned char* buf, size_t length);
void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, unsigned char* buf, size_t length);
// Forward declarations for internal functions
static int aes_cbc_encrypt(const unsigned char* key, const unsigned char* iv,

View File

@@ -277,3 +277,332 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
// If we reach here, we've exceeded max attempts
return NOSTR_ERROR_CRYPTO_FAILED;
}
/**
* Calculate PoW difficulty (leading zero bits) for an event ID
*
* @param event_id_hex Hexadecimal event ID string (64 characters)
* @return Number of leading zero bits, or negative error code on failure
*/
int nostr_calculate_pow_difficulty(const char* event_id_hex) {
if (!event_id_hex) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Validate hex string length (should be 64 characters for SHA-256)
size_t hex_len = strlen(event_id_hex);
if (hex_len != 64) {
return NOSTR_ERROR_NIP13_CALCULATION;
}
// Convert hex to bytes
unsigned char hash[32];
if (nostr_hex_to_bytes(event_id_hex, hash, 32) != NOSTR_SUCCESS) {
return NOSTR_ERROR_NIP13_CALCULATION;
}
// Use existing NIP-13 reference implementation
return count_leading_zero_bits(hash);
}
/**
* Extract nonce information from event tags
*
* @param event Complete event JSON object
* @param nonce_out Output pointer for nonce value (can be NULL)
* @param target_difficulty_out Output pointer for target difficulty (can be NULL)
* @return NOSTR_SUCCESS if nonce tag found, NOSTR_ERROR_NIP13_NO_NONCE_TAG if not found, other error codes on failure
*/
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize output values
if (nonce_out) *nonce_out = 0;
if (target_difficulty_out) *target_difficulty_out = -1;
// Get tags array
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
}
// Search for nonce tag
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
// Check if this is a nonce tag
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (!tag_type || !cJSON_IsString(tag_type)) {
continue;
}
const char* tag_name = cJSON_GetStringValue(tag_type);
if (!tag_name || strcmp(tag_name, "nonce") != 0) {
continue;
}
// Extract nonce value (second element)
cJSON* nonce_item = cJSON_GetArrayItem(tag, 1);
if (!nonce_item || !cJSON_IsString(nonce_item)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
const char* nonce_str = cJSON_GetStringValue(nonce_item);
if (!nonce_str) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Parse nonce value
char* endptr;
uint64_t nonce_val = strtoull(nonce_str, &endptr, 10);
if (*endptr != '\0') {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
if (nonce_out) *nonce_out = nonce_val;
// Extract target difficulty (third element, optional)
if (cJSON_GetArraySize(tag) >= 3) {
cJSON* target_item = cJSON_GetArrayItem(tag, 2);
if (target_item && cJSON_IsString(target_item)) {
const char* target_str = cJSON_GetStringValue(target_item);
if (target_str) {
char* endptr2;
long target_val = strtol(target_str, &endptr2, 10);
if (*endptr2 == '\0' && target_val >= 0) {
if (target_difficulty_out) *target_difficulty_out = (int)target_val;
}
}
}
}
return NOSTR_SUCCESS;
}
// No nonce tag found
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
}
/**
* Validate just the nonce tag format (without PoW calculation)
*
* @param nonce_tag_array JSON array representing a nonce tag
* @return NOSTR_SUCCESS if valid format, error code otherwise
*/
int nostr_validate_nonce_tag(cJSON* nonce_tag_array) {
if (!nonce_tag_array || !cJSON_IsArray(nonce_tag_array)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
int array_size = cJSON_GetArraySize(nonce_tag_array);
if (array_size < 2) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// First element should be "nonce"
cJSON* tag_type = cJSON_GetArrayItem(nonce_tag_array, 0);
if (!tag_type || !cJSON_IsString(tag_type)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
const char* tag_name = cJSON_GetStringValue(tag_type);
if (!tag_name || strcmp(tag_name, "nonce") != 0) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Second element should be a valid nonce (numeric string)
cJSON* nonce_item = cJSON_GetArrayItem(nonce_tag_array, 1);
if (!nonce_item || !cJSON_IsString(nonce_item)) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
const char* nonce_str = cJSON_GetStringValue(nonce_item);
if (!nonce_str) {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Validate nonce is a valid number
char* endptr;
strtoull(nonce_str, &endptr, 10);
if (*endptr != '\0') {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Third element (target difficulty) is optional, but if present should be valid
if (array_size >= 3) {
cJSON* target_item = cJSON_GetArrayItem(nonce_tag_array, 2);
if (target_item && cJSON_IsString(target_item)) {
const char* target_str = cJSON_GetStringValue(target_item);
if (target_str) {
char* endptr2;
strtol(target_str, &endptr2, 10);
if (*endptr2 != '\0') {
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
}
}
}
return NOSTR_SUCCESS;
}
/**
* Validate Proof of Work for an event according to NIP-13
*
* @param event Complete event JSON object to validate
* @param min_difficulty Minimum difficulty required by the relay (0 = no requirement)
* @param validation_flags Bitflags for validation options
* @param result_info Optional output struct for detailed results (can be NULL)
* @return NOSTR_SUCCESS if PoW is valid and meets requirements, error code otherwise
*/
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
nostr_pow_result_t* result_info) {
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
// Initialize result info if provided
if (result_info) {
result_info->actual_difficulty = 0;
result_info->committed_target = -1;
result_info->nonce_value = 0;
result_info->has_nonce_tag = 0;
result_info->error_detail[0] = '\0';
}
// Get event ID for PoW calculation
cJSON* id_item = cJSON_GetObjectItem(event, "id");
if (!id_item || !cJSON_IsString(id_item)) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Missing or invalid event ID");
}
return NOSTR_ERROR_EVENT_INVALID_ID;
}
const char* event_id = cJSON_GetStringValue(id_item);
if (!event_id) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Event ID is not a string");
}
return NOSTR_ERROR_EVENT_INVALID_ID;
}
// Calculate actual PoW difficulty
int actual_difficulty = nostr_calculate_pow_difficulty(event_id);
if (actual_difficulty < 0) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Failed to calculate PoW difficulty");
}
return actual_difficulty; // Return the specific error from calculation
}
if (result_info) {
result_info->actual_difficulty = actual_difficulty;
}
// Check if minimum difficulty requirement is met
if (min_difficulty > 0 && actual_difficulty < min_difficulty) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Insufficient difficulty: %d < %d", actual_difficulty, min_difficulty);
}
return NOSTR_ERROR_NIP13_INSUFFICIENT;
}
// Extract nonce information if validation flags require it
uint64_t nonce_value = 0;
int committed_target = -1;
int nonce_extract_result = nostr_extract_nonce_info(event, &nonce_value, &committed_target);
if (result_info) {
result_info->nonce_value = nonce_value;
result_info->committed_target = committed_target;
result_info->has_nonce_tag = (nonce_extract_result == NOSTR_SUCCESS) ? 1 : 0;
}
// Validate nonce tag presence if required
if (validation_flags & NOSTR_POW_VALIDATE_NONCE_TAG) {
if (nonce_extract_result == NOSTR_ERROR_NIP13_NO_NONCE_TAG) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Missing required nonce tag");
}
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
} else if (nonce_extract_result != NOSTR_SUCCESS) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Invalid nonce tag format");
}
return nonce_extract_result;
}
// If strict format validation is enabled, validate the nonce tag structure
if (validation_flags & NOSTR_POW_STRICT_FORMAT) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
if (tag_type && cJSON_IsString(tag_type)) {
const char* tag_name = cJSON_GetStringValue(tag_type);
if (tag_name && strcmp(tag_name, "nonce") == 0) {
int validation_result = nostr_validate_nonce_tag(tag);
if (validation_result != NOSTR_SUCCESS) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Nonce tag failed strict format validation");
}
return validation_result;
}
break;
}
}
}
}
}
}
}
// Validate committed target difficulty if required
if (validation_flags & NOSTR_POW_VALIDATE_TARGET_COMMIT) {
if (nonce_extract_result == NOSTR_SUCCESS && committed_target == -1) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Missing committed target difficulty in nonce tag");
}
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
}
// Check for target/difficulty mismatch if rejecting lower targets
if (validation_flags & NOSTR_POW_REJECT_LOWER_TARGET) {
// According to NIP-13: "if you require 40 bits to reply to your thread and see
// a committed target of 30, you can safely reject it even if the note has 40 bits difficulty"
// This means we reject if committed_target < min_difficulty, not actual_difficulty
if (committed_target != -1 && min_difficulty > 0 && committed_target < min_difficulty) {
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"Committed target (%d) is lower than required minimum (%d)",
committed_target, min_difficulty);
}
return NOSTR_ERROR_NIP13_TARGET_MISMATCH;
}
}
}
// All validations passed
if (result_info) {
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
"PoW validation successful");
}
return NOSTR_SUCCESS;
}

View File

@@ -8,11 +8,41 @@
#include "nip001.h"
#include <stdint.h>
// PoW validation flags
#define NOSTR_POW_VALIDATE_NONCE_TAG 0x01 // Require and validate nonce tag format
#define NOSTR_POW_VALIDATE_TARGET_COMMIT 0x02 // Validate committed target difficulty
#define NOSTR_POW_REJECT_LOWER_TARGET 0x04 // Reject if committed target < actual difficulty
#define NOSTR_POW_STRICT_FORMAT 0x08 // Strict nonce tag format validation
// Convenience combinations
#define NOSTR_POW_VALIDATE_BASIC (NOSTR_POW_VALIDATE_NONCE_TAG)
#define NOSTR_POW_VALIDATE_FULL (NOSTR_POW_VALIDATE_NONCE_TAG | NOSTR_POW_VALIDATE_TARGET_COMMIT)
#define NOSTR_POW_VALIDATE_ANTI_SPAM (NOSTR_POW_VALIDATE_FULL | NOSTR_POW_REJECT_LOWER_TARGET)
// Result information structure (optional output)
typedef struct {
int actual_difficulty; // Calculated difficulty (leading zero bits)
int committed_target; // Target difficulty from nonce tag (-1 if none)
uint64_t nonce_value; // Nonce value from tag (0 if none)
int has_nonce_tag; // 1 if nonce tag present, 0 otherwise
char error_detail[256]; // Detailed error description
} nostr_pow_result_t;
// Function declarations
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
int target_difficulty, int max_attempts,
int progress_report_interval, int timestamp_update_interval,
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
void* user_data);
// PoW validation functions
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
nostr_pow_result_t* result_info);
int nostr_calculate_pow_difficulty(const char* event_id_hex);
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out);
int nostr_validate_nonce_tag(cJSON* nonce_tag_array);
#endif // NIP013_H

628
nostr_core/nip042.c Normal file
View File

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

281
nostr_core/nip042.h Normal file
View File

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

View File

@@ -9,10 +9,29 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "./crypto/nostr_secp256k1.h"
// Include our ChaCha20 implementation
#include "crypto/nostr_chacha20.h"
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
int ecdh_shared_secret(const unsigned char* private_key, const unsigned char* public_key, unsigned char* shared_secret);
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// ChaCha20 functions for NIP-44 encryption
int chacha20_encrypt(const unsigned char key[32], unsigned int counter,
const unsigned char nonce[12], const unsigned char* input,
unsigned char* output, size_t length);
// HKDF functions for NIP-44 key derivation
int nostr_hkdf_extract(const unsigned char* salt, size_t salt_len,
const unsigned char* ikm, size_t ikm_len,
unsigned char* prk);
int nostr_hkdf_expand(const unsigned char* prk, size_t prk_len,
const unsigned char* info, size_t info_len,
unsigned char* okm, size_t okm_len);
// HMAC-SHA256 function for NIP-44 authentication
int nostr_hmac_sha256(const unsigned char* key, size_t key_len,
const unsigned char* data, size_t data_len,
unsigned char* hmac);
// Forward declarations for internal functions
static size_t calc_padded_len(size_t unpadded_len);

View File

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

View File

@@ -36,6 +36,31 @@
#define NOSTR_ERROR_EVENT_INVALID_TAGS -36
#define NOSTR_ERROR_EVENT_INVALID_CONTENT -37
// Authentication Rules System Error Codes
#define NOSTR_ERROR_AUTH_RULES_DISABLED -50
#define NOSTR_ERROR_AUTH_RULES_DENIED -51
#define NOSTR_ERROR_AUTH_RULES_DB_FAILED -52
#define NOSTR_ERROR_AUTH_RULES_INVALID_RULE -53
#define NOSTR_ERROR_AUTH_RULES_CACHE_FAILED -54
#define NOSTR_ERROR_AUTH_RULES_BACKEND_NOT_FOUND -55
// NIP-13 PoW-specific error codes
#define NOSTR_ERROR_NIP13_INSUFFICIENT -100
#define NOSTR_ERROR_NIP13_NO_NONCE_TAG -101
#define NOSTR_ERROR_NIP13_INVALID_NONCE_TAG -102
#define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103
#define NOSTR_ERROR_NIP13_CALCULATION -104
// NIP-42 Authentication-specific error codes
#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -200
#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201
#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -202
#define NOSTR_ERROR_NIP42_URL_MISMATCH -203
#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204
#define NOSTR_ERROR_NIP42_NOT_AUTHENTICATED -205
#define NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT -206
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
// Constants
#define NOSTR_PRIVATE_KEY_SIZE 32

View File

@@ -1,11 +1,123 @@
#ifndef NOSTR_CORE_H
#define NOSTR_CORE_H
/**
* NOSTR Core Library - Unified Header File
/*
* NOSTR Core Library - Complete API Reference
*
* This file provides a single include point for all NOSTR Core functionality.
* Include this file to access all available NIPs and core functions.
* This header includes ALL library functionality. For modular includes,
* use individual headers instead.
*
* ============================================================================
* QUICK FUNCTION REFERENCE - Find what you need fast!
* ============================================================================
*
* EVENT OPERATIONS (NIP-01):
* - nostr_create_and_sign_event() -> Create and sign new Nostr events
* - nostr_validate_event() -> Validate complete Nostr event
* - nostr_validate_event_structure() -> Check event structure only
* - nostr_verify_event_signature() -> Verify cryptographic signature
*
* CRYPTOGRAPHIC HASHING:
* - nostr_sha256() -> Single-call SHA-256 hash
* - nostr_sha256_init() -> Initialize streaming SHA-256 context
* - nostr_sha256_update() -> Process data chunks incrementally
* - nostr_sha256_final() -> Finalize hash and clear context
* - nostr_sha256_file_stream() -> Hash large files efficiently (NEW!)
* - nostr_sha512() -> SHA-512 hash function
* - nostr_hmac_sha256() -> HMAC with SHA-256
* - nostr_hmac_sha512() -> HMAC with SHA-512
*
* DIGITAL SIGNATURES & KEYS:
* - nostr_schnorr_sign() -> Create Schnorr signatures
* - nostr_ec_public_key_from_private_key() -> Generate public keys
* - nostr_ec_private_key_verify() -> Validate private key format
* - nostr_ec_sign() -> ECDSA signature creation
* - nostr_rfc6979_generate_k() -> Deterministic nonce generation
*
* NIP-04 ENCRYPTION (Legacy):
* - nostr_nip04_encrypt() -> Encrypt messages (AES-256-CBC)
* - nostr_nip04_decrypt() -> Decrypt messages (AES-256-CBC)
*
* NIP-44 ENCRYPTION (Modern - Recommended):
* - 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
* - nostr_bip39_mnemonic_to_seed() -> Convert mnemonic to seed
*
* BIP32 HD WALLETS:
* - nostr_bip32_key_from_seed() -> Create master key from seed
* - nostr_bip32_derive_child() -> Derive child key from parent
* - nostr_bip32_derive_path() -> Derive keys from derivation path
*
* KEY DERIVATION:
* - nostr_hkdf() -> HKDF key derivation (full)
* - nostr_hkdf_extract() -> HKDF extract step only
* - nostr_hkdf_expand() -> HKDF expand step only
* - nostr_pbkdf2_hmac_sha512() -> PBKDF2 with HMAC-SHA512
* - ecdh_shared_secret() -> ECDH shared secret computation
*
* UTILITIES & ENCODING:
* - nostr_bytes_to_hex() -> Convert bytes to hex string
* - nostr_hex_to_bytes() -> Convert hex string to bytes
* - base64_encode() -> Base64 encoding
* - base64_decode() -> Base64 decoding
*
* REQUEST VALIDATION & AUTHENTICATION:
* - nostr_validate_request() -> Unified request validation (events + auth rules)
* - nostr_request_validator_init() -> Initialize authentication system
* - nostr_auth_check_upload() -> Validate file upload requests
* - nostr_auth_check_delete() -> Validate file delete requests
* - nostr_auth_check_publish() -> Validate event publish requests
* - nostr_auth_rule_add() -> Add authentication rule
* - nostr_auth_rule_remove() -> Remove authentication rule
*
* SYSTEM FUNCTIONS:
* - nostr_crypto_init() -> Initialize crypto subsystem
* - nostr_crypto_cleanup() -> Cleanup crypto subsystem
*
* ============================================================================
* USAGE EXAMPLES:
* ============================================================================
*
* Basic Event Creation:
* cJSON* event = nostr_create_and_sign_event(1, "Hello Nostr!", NULL, private_key, time(NULL));
* if (nostr_validate_event(event) == NOSTR_SUCCESS) { ... }
*
* Streaming SHA-256 (for large files):
* nostr_sha256_ctx_t ctx;
* nostr_sha256_init(&ctx);
* // Process data in chunks...
* nostr_sha256_update(&ctx, data, data_size);
* nostr_sha256_final(&ctx, hash_output);
*
* File Hashing:
* unsigned char file_hash[32];
* nostr_sha256_file_stream("large_video.mp4", file_hash);
*
* Modern Encryption (NIP-44):
* nostr_nip44_encrypt(sender_key, recipient_pubkey, "secret message", output, sizeof(output));
*
* HD Wallet Derivation:
* 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
*
* ============================================================================
*/
#ifdef __cplusplus
@@ -24,8 +136,12 @@ 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)
// Authentication and request validation system
#include "request_validator.h" // Request validation and authentication rules
// Relay communication functions are defined in nostr_common.h
// WebSocket functions are defined in nostr_common.h

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,274 @@
/*
* NOSTR Core Library - Request Validator
*
* Unified authentication and authorization system for NOSTR applications.
* Provides rule-based validation for requests with pluggable database backends.
*
* This module combines basic NOSTR event validation with sophisticated
* authentication rules to provide a single entry point for request validation
* across different NOSTR applications (ginxsom, c-relay, etc.).
*/
#ifndef NOSTR_REQUEST_VALIDATOR_H
#define NOSTR_REQUEST_VALIDATOR_H
#include "nostr_common.h"
#include <time.h>
#include <sqlite3.h>
#ifdef __cplusplus
extern "C" {
#endif
// Forward declaration for cJSON
struct cJSON;
// Authentication rule types
typedef enum {
NOSTR_AUTH_RULE_PUBKEY_WHITELIST,
NOSTR_AUTH_RULE_PUBKEY_BLACKLIST,
NOSTR_AUTH_RULE_HASH_BLACKLIST,
NOSTR_AUTH_RULE_MIME_WHITELIST,
NOSTR_AUTH_RULE_MIME_BLACKLIST,
NOSTR_AUTH_RULE_SIZE_LIMIT,
NOSTR_AUTH_RULE_RATE_LIMIT,
NOSTR_AUTH_RULE_CUSTOM
} nostr_auth_rule_type_t;
// Authentication request context
typedef struct {
const char* operation; // Operation type ("upload", "delete", "list", "publish")
const char* auth_header; // Raw authorization header (optional)
struct cJSON* event; // NOSTR event for validation (optional)
// Resource context (for file/blob operations)
const char* resource_hash; // Resource hash (SHA-256, optional)
const char* mime_type; // MIME type (optional)
long file_size; // File size (optional)
// Client context
const char* client_ip; // Client IP for rate limiting (optional)
void* app_context; // Application-specific context (optional)
} nostr_request_t;
// Authentication result
typedef struct {
int valid; // 0 = invalid/denied, 1 = valid/allowed
int error_code; // NOSTR_SUCCESS or specific error code
char reason[256]; // Human-readable reason for denial
char pubkey[65]; // Extracted pubkey from validated event (if available)
int rule_id; // Rule ID that made the decision (0 if no rule)
int priority; // Priority of the rule that matched
time_t cached_until; // Cache expiration time
} nostr_request_result_t;
// Authentication rule definition
typedef struct {
int rule_id; // Unique rule identifier
nostr_auth_rule_type_t type; // Rule type
char operation[32]; // Target operation ("*", "upload", "delete", "publish", etc.)
char target[256]; // Rule target (pubkey, hash, mime pattern, etc.)
char value[256]; // Rule value (size limit, rate limit, custom data)
int priority; // Rule priority (lower number = higher priority)
int enabled; // 1 = enabled, 0 = disabled
time_t expires_at; // Expiration timestamp (0 = never expires)
char description[512]; // Human-readable description
time_t created_at; // Creation timestamp
} nostr_auth_rule_t;
// Database backend interface (pluggable)
typedef struct nostr_auth_db_interface {
const char* name; // Backend name ("sqlite", "redis", etc.)
// Database lifecycle
int (*init)(const char* db_path, const char* app_name);
void (*cleanup)(void);
// Configuration management
int (*get_config)(const char* key, char* value, size_t value_size);
int (*set_config)(const char* key, const char* value);
// Rule querying and management
int (*query_rules)(const nostr_request_t* request, nostr_auth_rule_t** rules, int* count);
int (*rule_add)(const nostr_auth_rule_t* rule);
int (*rule_remove)(int rule_id);
int (*rule_update)(const nostr_auth_rule_t* rule);
int (*rule_list)(const char* operation, nostr_auth_rule_t** rules, int* count);
// Caching operations
int (*cache_get)(const char* cache_key, nostr_request_result_t* result);
int (*cache_set)(const char* cache_key, const nostr_request_result_t* result, int ttl);
int (*cache_clear)(void);
} nostr_auth_db_interface_t;
//=============================================================================
// CORE API FUNCTIONS
//=============================================================================
/**
* Initialize the request validator system with application database
*
* @param app_db_path Path to application's SQLite database
* @param app_name Application name for logging/identification
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_request_validator_init(const char* app_db_path, const char* app_name);
/**
* Initialize with shared database (future use)
*
* @param shared_db_path Path to shared authentication database
* @param app_name Application name for logging/identification
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_request_validator_init_shared(const char* shared_db_path, const char* app_name);
/**
* Main request validation function - validates both NOSTR events and authentication rules
*
* @param request Request context with operation, auth header, and resource details
* @param result Result structure with validation outcome and details
* @return NOSTR_SUCCESS on successful validation processing, error code on system failure
*/
int nostr_validate_request(const nostr_request_t* request, nostr_request_result_t* result);
/**
* Check if authentication rules system is enabled
*
* @return 1 if enabled, 0 if disabled
*/
int nostr_auth_rules_enabled(void);
/**
* Cleanup request validator resources
*/
void nostr_request_validator_cleanup(void);
//=============================================================================
// CONVENIENCE FUNCTIONS
//=============================================================================
/**
* Convenience function for upload validation (ginxsom integration)
*
* @param pubkey Uploader public key (optional, extracted from auth if NULL)
* @param auth_header Authorization header with NOSTR event
* @param hash File hash (SHA-256)
* @param mime_type File MIME type
* @param file_size File size in bytes
* @return NOSTR_SUCCESS if allowed, error code if denied
*/
int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
const char* hash, const char* mime_type, long file_size);
/**
* Convenience function for delete validation (ginxsom integration)
*
* @param pubkey Requester public key
* @param auth_header Authorization header with NOSTR event
* @param hash File hash to delete
* @return NOSTR_SUCCESS if allowed, error code if denied
*/
int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const char* hash);
/**
* Convenience function for publish validation (c-relay integration)
*
* @param pubkey Publisher public key
* @param event NOSTR event to publish
* @return NOSTR_SUCCESS if allowed, error code if denied
*/
int nostr_auth_check_publish(const char* pubkey, struct cJSON* event);
//=============================================================================
// RULE MANAGEMENT FUNCTIONS
//=============================================================================
/**
* Add a new authentication rule
*
* @param rule Rule definition to add
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_add(const nostr_auth_rule_t* rule);
/**
* Remove an authentication rule by ID
*
* @param rule_id Rule ID to remove
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_remove(int rule_id);
/**
* Update an existing authentication rule
*
* @param rule Updated rule definition
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_update(const nostr_auth_rule_t* rule);
/**
* List authentication rules for a specific operation
*
* @param operation Target operation ("*" for all operations)
* @param rules Pointer to receive allocated array of rules
* @param count Pointer to receive number of rules returned
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int* count);
/**
* Free rule array allocated by nostr_auth_rule_list
*
* @param rules Rule array to free
* @param count Number of rules in array
*/
void nostr_auth_rules_free(nostr_auth_rule_t* rules, int count);
//=============================================================================
// DATABASE BACKEND MANAGEMENT
//=============================================================================
/**
* Register a database backend implementation
*
* @param backend Backend interface implementation
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_register_db_backend(const nostr_auth_db_interface_t* backend);
/**
* Set active database backend by name
*
* @param backend_name Name of backend to activate
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_set_db_backend(const char* backend_name);
//=============================================================================
// CACHE MANAGEMENT
//=============================================================================
/**
* Clear authentication decision cache
*
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_cache_clear(void);
/**
* Get cache statistics
*
* @param hit_count Pointer to receive cache hit count
* @param miss_count Pointer to receive cache miss count
* @param entries Pointer to receive current number of cache entries
* @return NOSTR_SUCCESS on success, error code on failure
*/
int nostr_auth_cache_stats(int* hit_count, int* miss_count, int* entries);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_REQUEST_VALIDATOR_H */

View File

@@ -9,14 +9,28 @@
#include <stdlib.h>
#include <string.h>
// Include our secp256k1 wrapper for elliptic curve operations
#include "crypto/nostr_secp256k1.h"
// Forward declarations for crypto functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
// Include our self-contained AES implementation for NIP-04
#include "crypto/nostr_aes.h"
// secp256k1 functions
typedef struct {
unsigned char data[64];
} nostr_secp256k1_pubkey;
// Include our ChaCha20 implementation for NIP-44
#include "crypto/nostr_chacha20.h"
typedef struct {
unsigned char data[96];
} nostr_secp256k1_keypair;
int nostr_secp256k1_context_create(void);
void nostr_secp256k1_context_destroy(void);
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
int nostr_secp256k1_ecdh(unsigned char* output, const nostr_secp256k1_pubkey* pubkey, const unsigned char* privkey, void* hashfp, void* data);
int nostr_secp256k1_ec_seckey_verify(const unsigned char* seckey);
int nostr_secp256k1_ec_pubkey_create(nostr_secp256k1_pubkey* pubkey, const unsigned char* privkey);
int nostr_secp256k1_ec_pubkey_serialize_compressed(unsigned char* output, const nostr_secp256k1_pubkey* pubkey);
int nostr_secp256k1_keypair_create(nostr_secp256k1_keypair* keypair, const unsigned char* privkey);
int nostr_secp256k1_schnorrsig_sign32(unsigned char* sig, const unsigned char* msg32, const nostr_secp256k1_keypair* keypair, const unsigned char* aux_rand32);
int nostr_secp256k1_ec_seckey_tweak_add(unsigned char* seckey, const unsigned char* tweak);
// =============================================================================
@@ -328,6 +342,125 @@ int nostr_sha256(const unsigned char* data, size_t len, unsigned char* hash) {
return 0;
}
// =============================================================================
// STREAMING SHA-256 IMPLEMENTATION
// =============================================================================
int nostr_sha256_init(nostr_sha256_ctx_t* ctx) {
if (!ctx) return -1;
// Initialize SHA-256 state
ctx->state[0] = 0x6a09e667;
ctx->state[1] = 0xbb67ae85;
ctx->state[2] = 0x3c6ef372;
ctx->state[3] = 0xa54ff53a;
ctx->state[4] = 0x510e527f;
ctx->state[5] = 0x9b05688c;
ctx->state[6] = 0x1f83d9ab;
ctx->state[7] = 0x5be0cd19;
// Initialize counters and buffer
ctx->bitlen = 0;
ctx->buflen = 0;
return 0;
}
int nostr_sha256_update(nostr_sha256_ctx_t* ctx, const unsigned char* data, size_t len) {
if (!ctx || !data) return -1;
for (size_t i = 0; i < len; i++) {
ctx->buffer[ctx->buflen] = data[i];
ctx->buflen++;
// Process complete blocks
if (ctx->buflen == 64) {
sha256_transform(ctx->state, ctx->buffer);
ctx->bitlen += 512; // 64 bytes * 8 bits
ctx->buflen = 0;
}
}
return 0;
}
int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash) {
if (!ctx || !hash) return -1;
// Calculate final bit length
uint64_t final_bitlen = ctx->bitlen + (ctx->buflen * 8);
// Pad the message
ctx->buffer[ctx->buflen] = 0x80;
ctx->buflen++;
// If not enough space for length, pad and process another block
if (ctx->buflen > 56) {
while (ctx->buflen < 64) {
ctx->buffer[ctx->buflen] = 0x00;
ctx->buflen++;
}
sha256_transform(ctx->state, ctx->buffer);
ctx->buflen = 0;
}
// Pad with zeros up to 56 bytes
while (ctx->buflen < 56) {
ctx->buffer[ctx->buflen] = 0x00;
ctx->buflen++;
}
// Append length as big-endian 64-bit integer
for (int i = 0; i < 8; i++) {
ctx->buffer[56 + i] = (final_bitlen >> (56 - i * 8)) & 0xff;
}
// Process final block
sha256_transform(ctx->state, ctx->buffer);
// Convert state to output bytes
for (int i = 0; i < 8; i++) {
hash[i * 4] = (ctx->state[i] >> 24) & 0xff;
hash[i * 4 + 1] = (ctx->state[i] >> 16) & 0xff;
hash[i * 4 + 2] = (ctx->state[i] >> 8) & 0xff;
hash[i * 4 + 3] = ctx->state[i] & 0xff;
}
// Clear sensitive data
memory_clear(ctx, sizeof(nostr_sha256_ctx_t));
return 0;
}
int nostr_sha256_file_stream(const char* filename, unsigned char* hash) {
if (!filename || !hash) return -1;
FILE* file = fopen(filename, "rb");
if (!file) return -1;
nostr_sha256_ctx_t ctx;
if (nostr_sha256_init(&ctx) != 0) {
fclose(file);
return -1;
}
// Process file in 4KB chunks for memory efficiency
unsigned char buffer[4096];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
if (nostr_sha256_update(&ctx, buffer, bytes_read) != 0) {
fclose(file);
return -1;
}
}
fclose(file);
// Finalize and return result
return nostr_sha256_final(&ctx, hash);
}
// =============================================================================
// HMAC IMPLEMENTATION
// =============================================================================

View File

@@ -45,6 +45,30 @@ void nostr_crypto_cleanup(void);
// SHA-256 hash function
int nostr_sha256(const unsigned char *data, size_t len, unsigned char *hash);
// =============================================================================
// STREAMING SHA-256 FUNCTIONS
// =============================================================================
// SHA-256 streaming context
typedef struct {
uint32_t state[8]; // Current hash state
unsigned char buffer[64]; // Input buffer for incomplete blocks
uint64_t bitlen; // Total bits processed
size_t buflen; // Current buffer length
} nostr_sha256_ctx_t;
// Initialize SHA-256 streaming context
int nostr_sha256_init(nostr_sha256_ctx_t* ctx);
// Update SHA-256 context with new data
int nostr_sha256_update(nostr_sha256_ctx_t* ctx, const unsigned char* data, size_t len);
// Finalize SHA-256 and output hash
int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash);
// Stream SHA-256 hash of a file
int nostr_sha256_file_stream(const char* filename, unsigned char* hash);
// HMAC-SHA256
int nostr_hmac_sha256(const unsigned char *key, size_t key_len,
const unsigned char *data, size_t data_len,

View File

@@ -9,7 +9,15 @@
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "../nostr_core/crypto/nostr_chacha20.h"
// Forward declarations for ChaCha20 functions (private API)
// These functions are implemented in crypto/ but not exposed through public headers
void chacha20_quarter_round(uint32_t state[16], int a, int b, int c, int d);
int chacha20_block(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], uint8_t output[64]);
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
const uint8_t nonce[12], const uint8_t* input,
uint8_t* output, size_t length);
// Helper function to convert hex string to bytes
static int hex_to_bytes(const char* hex, uint8_t* bytes, size_t len) {

View File

@@ -6,3 +6,11 @@
}]
[10:24:53.944] RECV nostr.mom:443: ["EVENT","sync_0_1755354293",{"content":"GM🫡","created_at":1755354265,"id":"3e7c67349dd3e1ccaaf4dcd6f5987451d63561b14cdff6c6e68cc5448ec5acaf","kind":1,"pubkey":"ff2f4cd786e42b4323749c91517ec7baf22dfd035b7a101bea83b6e2bcbacd15","sig":"2278afa7b7f42b68f8b3f393bb72b6af4b2a34b77008e109232e24bcfe8a3d1ce917187ef1ca68f4a69e52c2067c14da03ed63e31e4137b1175f8ee1a08b7c21","tags":[["e","45983e18b7c0f28933ecd1c4ead88eb5561caa465a5bc939914b64fa455aefc3","wss://relay.primal.net","root"],["p","db625e7637543ca7d7be65025834db318a0c7b75b0e23d4fb9e39229f5ba6fa7","","mention"]]}]
[10:24:53.944] SEND nostr.mom:443: ["CLOSE", "sync_0_1755354293"]
=== NOSTR WebSocket Debug Log Started ===
[12:34:34.841] SEND nostr.mom:443: ["REQ", "sync_0_1756830874", {
"kinds": [1],
"limit": 1
}]
[12:34:34.997] RECV nostr.mom:443: ["EVENT","sync_0_1756830874",{"content":"It's a lot of work. 🫂🫂🫂","created_at":1756830871,"id":"dd1db1b8e25c1278b6dc47b2a05633854a1afb936ea035a06182f4e0683a732e","kind":1,"pubkey":"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24","sig":"33ef0e09477fc978fccfe57d40856952af01b7e413a6174cb99e1d39e0639ba75d5c300f74bd2945c48275debb53f80e7f6a5cc91a4511323b756f5e09707969","tags":[["alt","A short note: It's a lot of work. 🫂🫂🫂"],["e","33597a2e46cffec3da6500c5ddc3cfcf8248e065377e9a5abea23cf633a7c134","wss://nos.lol/","root","91c9a5e1a9744114c6fe2d61ae4de82629eaaa0fb52f48288093c7e7e036f832"],["p","91c9a5e1a9744114c6fe2d61ae4de82629eaaa0fb52f48288093c7e7e036f832","wss://nos.lol/"]]}]
[12:34:34.998] SEND nostr.mom:443: ["CLOSE", "sync_0_1756830874"]

View File

@@ -0,0 +1,142 @@
/*
* Enhanced Header Integration Test
*
* Tests that the enhanced nostr_core.h master header provides
* easy access to all documented functionality
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Test the enhanced master header - single include for everything
#include "../nostr_core/nostr_core.h"
int main(void) {
printf("NOSTR Core Library - Enhanced Header Integration Test\n");
printf("====================================================\n\n");
// Initialize crypto subsystem
if (nostr_crypto_init() != 0) {
printf("❌ Failed to initialize crypto subsystem\n");
return 1;
}
printf("✅ Successfully included nostr_core.h master header\n");
printf("✅ Crypto subsystem initialized\n\n");
// Test 1: Basic cryptographic functions are available
printf("=== Test 1: Basic Crypto Functions ===\n");
unsigned char test_data[] = "Hello, NOSTR!";
unsigned char hash[32];
if (nostr_sha256(test_data, strlen((char*)test_data), hash) == 0) {
printf("✅ nostr_sha256() - Single-call SHA-256 works\n");
} else {
printf("❌ nostr_sha256() failed\n");
goto cleanup;
}
// Test 2: Streaming SHA-256 functions are available
printf("\n=== Test 2: Streaming SHA-256 Functions ===\n");
nostr_sha256_ctx_t ctx;
unsigned char streaming_hash[32];
if (nostr_sha256_init(&ctx) == 0 &&
nostr_sha256_update(&ctx, test_data, strlen((char*)test_data)) == 0 &&
nostr_sha256_final(&ctx, streaming_hash) == 0) {
printf("✅ nostr_sha256_init/update/final() - Streaming SHA-256 works\n");
// Verify streaming matches single-call
if (memcmp(hash, streaming_hash, 32) == 0) {
printf("✅ Streaming result matches single-call result\n");
} else {
printf("❌ Streaming result differs from single-call\n");
}
} else {
printf("❌ Streaming SHA-256 functions failed\n");
goto cleanup;
}
// Test 3: Key generation functions are available
printf("\n=== Test 3: Key Generation Functions ===\n");
unsigned char private_key[32] = {
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef
};
unsigned char public_key[32];
if (nostr_ec_private_key_verify(private_key) == 0) {
printf("✅ nostr_ec_private_key_verify() - Private key validation works\n");
} else {
printf("❌ Private key validation failed\n");
goto cleanup;
}
if (nostr_ec_public_key_from_private_key(private_key, public_key) == 0) {
printf("✅ nostr_ec_public_key_from_private_key() - Public key generation works\n");
} else {
printf("❌ Public key generation failed\n");
goto cleanup;
}
// Test 4: Event functions are available
printf("\n=== Test 4: Event Functions ===\n");
cJSON* event = nostr_create_and_sign_event(1, "Test message", NULL, private_key, time(NULL));
if (event) {
printf("✅ nostr_create_and_sign_event() - Event creation works\n");
if (nostr_validate_event(event) == 0) {
printf("✅ nostr_validate_event() - Event validation works\n");
} else {
printf("❌ Event validation failed\n");
}
cJSON_Delete(event);
} else {
printf("❌ Event creation failed\n");
goto cleanup;
}
// Test 5: Utility functions are available
printf("\n=== Test 5: Utility Functions ===\n");
char hex_output[65];
nostr_bytes_to_hex(hash, 32, hex_output);
printf("✅ nostr_bytes_to_hex() - Hash as hex: %.16s...\n", hex_output);
unsigned char hex_back[32];
if (nostr_hex_to_bytes(hex_output, hex_back, 32) == 0) {
printf("✅ nostr_hex_to_bytes() - Hex conversion works\n");
if (memcmp(hash, hex_back, 32) == 0) {
printf("✅ Round-trip hex conversion successful\n");
} else {
printf("❌ Round-trip hex conversion failed\n");
}
} else {
printf("❌ Hex to bytes conversion failed\n");
}
printf("\n====================================================\n");
printf("Enhanced Header Integration Test Results:\n");
printf("✅ Master header includes all functionality\n");
printf("✅ All documented function categories accessible\n");
printf("✅ Single #include provides complete API\n");
printf("✅ Functions work correctly through master header\n");
printf("\nDevelopers can now easily discover and use all\n");
printf("NOSTR Core Library functions with just:\n");
printf(" #include \"nostr_core.h\"\n");
printf("====================================================\n");
cleanup:
nostr_crypto_cleanup();
return 0;
}

View File

@@ -1,92 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core/nip004.h"
#include "../nostr_core/nostr_common.h"
#include "../nostr_core/utils.h"
int main(void) {
printf("=== NIP-04 DEBUG COMPARISON (C) ===\n");
// Initialize NOSTR library - REQUIRED for secp256k1 operations
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
printf("✓ NOSTR library initialized successfully\n");
// Test vectors matching JavaScript
const char* sk1_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
const char* pk1_hex = "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1";
const char* sk2_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
const char* pk2_hex = "dcb33a629560280a0ee3b6b99b68c044fe8914ad8a984001ebf6099a9b474dc3";
const char* plaintext = "nanana";
const char* expectedCiphertext = "d6Joav5EciPI9hdHw31vmQ==?iv=fWs5rfv2+532arG/k83kcA==";
// Convert hex keys to bytes
unsigned char sk1[32], pk1[32], sk2[32], pk2[32];
nostr_hex_to_bytes(sk1_hex, sk1, 32);
nostr_hex_to_bytes(pk1_hex, pk1, 32);
nostr_hex_to_bytes(sk2_hex, sk2, 32);
nostr_hex_to_bytes(pk2_hex, pk2, 32);
// Print keys for comparison
printf("[C] Private Key sk1: %s\n", sk1_hex);
printf("[C] Public Key pk2: %s\n", pk2_hex);
// Allocate output buffer for encryption
char* encrypted = malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE);
if (!encrypted) {
printf("Memory allocation failed\n");
return 1;
}
printf("\n--- ENCRYPTION TEST ---\n");
printf("[C] Encrypting \"%s\" using sk1 -> pk2\n", plaintext);
// Call the encryption function
int result = nostr_nip04_encrypt(sk1, pk2, plaintext, encrypted, NOSTR_NIP04_MAX_ENCRYPTED_SIZE);
if (result == NOSTR_SUCCESS) {
printf("[C] Encrypted Result: %s\n", encrypted);
} else {
printf("Encryption Error: %s\n", nostr_strerror(result));
free(encrypted);
return 1;
}
printf("\n--- DECRYPTION TEST ---\n");
printf("[C] Decrypting \"%s\" using sk2 + pk1\n", expectedCiphertext);
printf("[C] Private Key sk2: %s\n", sk2_hex);
printf("[C] Public Key pk1: %s\n", pk1_hex);
// Allocate output buffer for decryption
char* decrypted = malloc(1000);
if (!decrypted) {
printf("Memory allocation failed\n");
free(encrypted);
return 1;
}
// Call the decryption function
result = nostr_nip04_decrypt(sk2, pk1, expectedCiphertext, decrypted, 1000);
if (result == NOSTR_SUCCESS) {
printf("[C] UTF-8 Decoded: \"%s\"\n", decrypted);
printf("\n--- RESULTS ---\n");
printf("Encryption Success: Generated ciphertext\n");
printf("Decryption Success: %s\n", strcmp(decrypted, plaintext) == 0 ? "true" : "false");
printf("Expected: \"%s\"\n", plaintext);
printf("Got: \"%s\"\n", decrypted);
} else {
printf("Decryption Error: %s\n", nostr_strerror(result));
}
free(encrypted);
free(decrypted);
// Cleanup NOSTR library
nostr_cleanup();
return 0;
}

491
tests/nip13_test.c Normal file
View File

@@ -0,0 +1,491 @@
/*
* NIP-13 Proof of Work Test Suite
* Tests PoW generation, difficulty calculation, nonce extraction, and validation
* 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/nip013.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);
}
}
// Test 1: Difficulty calculation with NIP-13 example
int test_difficulty_calculation_reference(void) {
print_test_header("Difficulty Calculation - NIP-13 Reference");
// Event ID from NIP-13 spec: 000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358
// This should have ~20 leading zero bits as stated in the spec
const char* event_id = "000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358";
printf("Testing event ID: %s\n", event_id);
printf("Expected: ~20 leading zero bits\n");
int difficulty = nostr_calculate_pow_difficulty(event_id);
printf("Actual: %d leading zero bits\n", difficulty);
if (difficulty < 0) {
printf("❌ Error calculating difficulty: %d (%s)\n", difficulty, nostr_strerror(difficulty));
return 0;
}
// The NIP-13 spec example should have around 20 bits
if (difficulty >= 19 && difficulty <= 21) {
printf("✅ Difficulty calculation matches expected range (19-21 bits)\n");
return 1;
} else {
printf("❌ Difficulty %d is outside expected range (19-21 bits)\n", difficulty);
return 0;
}
}
// Test 2: Extract nonce info from NIP-13 reference event
int test_nonce_extraction_reference(void) {
print_test_header("Nonce Extraction - NIP-13 Reference Event");
// Complete event from NIP-13 spec
const char* event_json = "{"
"\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\","
"\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\","
"\"created_at\":1651794653,"
"\"kind\":1,"
"\"tags\":[[\"nonce\",\"776797\",\"20\"]],"
"\"content\":\"It's just me mining my own business\","
"\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\""
"}";
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;
}
uint64_t nonce_value = 0;
int target_difficulty = -1;
int result = nostr_extract_nonce_info(event, &nonce_value, &target_difficulty);
printf("Extraction result: %d (%s)\n", result, nostr_strerror(result));
printf("Expected nonce: 776797\n");
printf("Actual nonce: %llu\n", (unsigned long long)nonce_value);
printf("Expected target: 20\n");
printf("Actual target: %d\n", target_difficulty);
cJSON_Delete(event);
if (result != NOSTR_SUCCESS) {
printf("❌ Failed to extract nonce info\n");
return 0;
}
if (nonce_value == 776797 && target_difficulty == 20) {
printf("✅ Nonce extraction successful\n");
return 1;
} else {
printf("❌ Extracted values don't match expected\n");
return 0;
}
}
// Test 3: PoW validation with reference event
int test_pow_validation_reference(void) {
print_test_header("PoW Validation - NIP-13 Reference Event");
const char* event_json = "{"
"\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\","
"\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\","
"\"created_at\":1651794653,"
"\"kind\":1,"
"\"tags\":[[\"nonce\",\"776797\",\"20\"]],"
"\"content\":\"It's just me mining my own business\","
"\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\""
"}";
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;
}
// Test basic validation (no minimum difficulty)
nostr_pow_result_t result_info;
int validation_result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_BASIC, &result_info);
printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result));
printf("Actual difficulty: %d\n", result_info.actual_difficulty);
printf("Committed target: %d\n", result_info.committed_target);
printf("Nonce value: %llu\n", (unsigned long long)result_info.nonce_value);
printf("Has nonce tag: %d\n", result_info.has_nonce_tag);
printf("Error detail: %s\n", result_info.error_detail);
cJSON_Delete(event);
return (validation_result == NOSTR_SUCCESS && result_info.has_nonce_tag == 1);
}
// Test 4: Generate PoW with our library and validate it
int test_pow_generation_and_validation(void) {
print_test_header("PoW Generation and Validation - Our Library");
// Create a test event for mining
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();
printf("Creating base event...\n");
cJSON* event = nostr_create_and_sign_event(1, "Testing PoW generation", tags, private_key, time(NULL));
if (!event) {
printf("❌ Event creation failed\n");
cJSON_Delete(tags);
return 0;
}
char* original_event_str = cJSON_Print(event);
printf("Original event (no PoW):\n%s\n\n", original_event_str);
free(original_event_str);
// Add PoW with difficulty 8 (reasonable for testing)
printf("Adding PoW with difficulty 8...\n");
int pow_result = nostr_add_proof_of_work(event, private_key, 8, 100000, 10000, 10000, NULL, NULL);
if (pow_result != NOSTR_SUCCESS) {
printf("❌ PoW generation failed: %d (%s)\n", pow_result, nostr_strerror(pow_result));
cJSON_Delete(event);
return 0;
}
char* pow_event_str = cJSON_Print(event);
printf("Event with PoW:\n%s\n\n", pow_event_str);
free(pow_event_str);
// Now validate the PoW
printf("Validating generated PoW...\n");
nostr_pow_result_t result_info;
int validation_result = nostr_validate_pow(event, 8, NOSTR_POW_VALIDATE_FULL, &result_info);
printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result));
printf("Actual difficulty: %d\n", result_info.actual_difficulty);
printf("Committed target: %d\n", result_info.committed_target);
printf("Has nonce tag: %d\n", result_info.has_nonce_tag);
printf("Error detail: %s\n", result_info.error_detail);
cJSON_Delete(event);
if (validation_result == NOSTR_SUCCESS && result_info.actual_difficulty >= 8) {
printf("✅ PoW generation and validation successful\n");
return 1;
} else {
printf("❌ PoW validation failed\n");
return 0;
}
}
// Test 5: Test various validation flag combinations
int test_validation_flags(void) {
print_test_header("Validation Flags - Various Combinations");
// Use NIP-13 reference event
const char* event_json = "{"
"\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\","
"\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\","
"\"created_at\":1651794653,"
"\"kind\":1,"
"\"tags\":[[\"nonce\",\"776797\",\"20\"]],"
"\"content\":\"It's just me mining my own business\","
"\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\""
"}";
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ JSON Parse Error\n");
return 0;
}
int all_passed = 1;
nostr_pow_result_t result_info;
// Test 1: Basic validation
printf("\nSubtest 1: NOSTR_POW_VALIDATE_BASIC\n");
int result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_BASIC, &result_info);
printf("Result: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_SUCCESS) all_passed = 0;
// Test 2: Full validation
printf("\nSubtest 2: NOSTR_POW_VALIDATE_FULL\n");
result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_FULL, &result_info);
printf("Result: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_SUCCESS) all_passed = 0;
// Test 3: Anti-spam validation (should pass since committed target matches actual)
printf("\nSubtest 3: NOSTR_POW_VALIDATE_ANTI_SPAM\n");
result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_ANTI_SPAM, &result_info);
printf("Result: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_SUCCESS) all_passed = 0;
// Test 4: Minimum difficulty requirement (15 bits - should pass)
printf("\nSubtest 4: Minimum difficulty 15 bits\n");
result = nostr_validate_pow(event, 15, NOSTR_POW_VALIDATE_BASIC, &result_info);
printf("Result: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_SUCCESS) all_passed = 0;
// Test 5: Too high minimum difficulty (30 bits - should fail)
printf("\nSubtest 5: Minimum difficulty 30 bits (should fail)\n");
result = nostr_validate_pow(event, 30, NOSTR_POW_VALIDATE_BASIC, &result_info);
printf("Result: %d (%s)\n", result, nostr_strerror(result));
if (result == NOSTR_SUCCESS) {
printf("❌ Should have failed but didn't\n");
all_passed = 0;
} else if (result == NOSTR_ERROR_NIP13_INSUFFICIENT) {
printf("✅ Correctly failed with insufficient difficulty\n");
} else {
printf("❌ Failed with unexpected error\n");
all_passed = 0;
}
cJSON_Delete(event);
return all_passed;
}
// Test 6: 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 event
printf("\nSubtest 1: NULL event\n");
int result = nostr_validate_pow(NULL, 0, NOSTR_POW_VALIDATE_BASIC, NULL);
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: Event without ID
printf("\nSubtest 2: Event without ID\n");
const char* no_id_json = "{\"kind\":1,\"content\":\"test\",\"tags\":[]}";
cJSON* no_id_event = cJSON_Parse(no_id_json);
result = nostr_validate_pow(no_id_event, 0, NOSTR_POW_VALIDATE_BASIC, NULL);
printf("Expected: NOSTR_ERROR_EVENT_INVALID_ID (-31)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_ERROR_EVENT_INVALID_ID) all_passed = 0;
cJSON_Delete(no_id_event);
// Test 3: Event without nonce tag when required
printf("\nSubtest 3: Event without nonce tag when required\n");
const char* no_nonce_json = "{"
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
"\"kind\":1,\"content\":\"test\",\"tags\":[]"
"}";
cJSON* no_nonce_event = cJSON_Parse(no_nonce_json);
result = nostr_validate_pow(no_nonce_event, 0, NOSTR_POW_VALIDATE_NONCE_TAG, NULL);
printf("Expected: NOSTR_ERROR_NIP13_NO_NONCE_TAG (-101)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_ERROR_NIP13_NO_NONCE_TAG) all_passed = 0;
cJSON_Delete(no_nonce_event);
// Test 4: Invalid hex ID
printf("\nSubtest 4: Invalid hex ID\n");
result = nostr_calculate_pow_difficulty("invalid_hex_string");
printf("Expected: NOSTR_ERROR_NIP13_CALCULATION (-104)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_ERROR_NIP13_CALCULATION) all_passed = 0;
// Test 5: Wrong length hex ID
printf("\nSubtest 5: Wrong length hex ID\n");
result = nostr_calculate_pow_difficulty("f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffa"); // 63 chars instead of 64
printf("Expected: NOSTR_ERROR_NIP13_CALCULATION (-104)\n");
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_ERROR_NIP13_CALCULATION) all_passed = 0;
return all_passed;
}
// Test 7: Integration with nak-generated PoW events
int test_nak_integration(void) {
print_test_header("Integration with nak-generated PoW events");
// Generate a test event with nak and PoW difficulty 8
printf("Generating PoW event with nak (difficulty 8)...\n");
// Create a temporary key for this test
char temp_key[] = "/tmp/nip13_test_key_XXXXXX";
int fd = mkstemp(temp_key);
if (fd == -1) {
printf("❌ Failed to create temporary key file\n");
return 0;
}
// Write test key to file (same as used in other tests)
const char* test_key = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
ssize_t bytes_written = write(fd, test_key, strlen(test_key));
close(fd);
if (bytes_written != (ssize_t)strlen(test_key)) {
printf("❌ Failed to write test key to temporary file\n");
unlink(temp_key);
return 0;
}
// Generate event with PoW using nak
char cmd[1024];
snprintf(cmd, sizeof(cmd), "nak event --sec %s -c \"PoW test from nak\" --pow 8 --ts %ld",
test_key, (long)time(NULL));
printf("Executing: %s\n", cmd);
FILE* pipe = popen(cmd, "r");
if (!pipe) {
printf("❌ Failed to execute nak command\n");
unlink(temp_key);
return 0;
}
char event_json[4096] = {0};
if (!fgets(event_json, sizeof(event_json), pipe)) {
printf("❌ Failed to read nak output\n");
pclose(pipe);
unlink(temp_key);
return 0;
}
pclose(pipe);
unlink(temp_key);
// Remove trailing newline
event_json[strcspn(event_json, "\n")] = 0;
printf("nak-generated event:\n%s\n\n", event_json);
// Parse and validate the event
cJSON* event = cJSON_Parse(event_json);
if (!event) {
printf("❌ Failed to parse nak-generated JSON\n");
return 0;
}
// Validate the PoW
nostr_pow_result_t result_info;
int validation_result = nostr_validate_pow(event, 8, NOSTR_POW_VALIDATE_FULL, &result_info);
printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result));
printf("Actual difficulty: %d\n", result_info.actual_difficulty);
printf("Committed target: %d\n", result_info.committed_target);
printf("Has nonce tag: %d\n", result_info.has_nonce_tag);
printf("Error detail: %s\n", result_info.error_detail);
cJSON_Delete(event);
if (validation_result == NOSTR_SUCCESS && result_info.actual_difficulty >= 8) {
printf("✅ nak-generated PoW event validates successfully\n");
return 1;
} else {
printf("❌ nak-generated PoW event validation failed\n");
return 0;
}
}
int main(void) {
printf("=== NIP-13 Proof of Work Test Suite ===\n");
printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\n");
printf("Tests both PoW generation and validation 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: Basic difficulty calculation
test_result = test_difficulty_calculation_reference();
print_test_result(test_result, "Difficulty Calculation - NIP-13 Reference");
if (!test_result) all_passed = 0;
// Test 2: Nonce extraction
test_result = test_nonce_extraction_reference();
print_test_result(test_result, "Nonce Extraction - NIP-13 Reference Event");
if (!test_result) all_passed = 0;
// Test 3: Basic PoW validation
test_result = test_pow_validation_reference();
print_test_result(test_result, "PoW Validation - NIP-13 Reference Event");
if (!test_result) all_passed = 0;
// Test 4: PoW generation and validation
test_result = test_pow_generation_and_validation();
print_test_result(test_result, "PoW Generation and Validation - Our Library");
if (!test_result) all_passed = 0;
// Test 5: Validation flags
test_result = test_validation_flags();
print_test_result(test_result, "Validation Flags - Various Combinations");
if (!test_result) all_passed = 0;
// Test 6: Error conditions
test_result = test_error_conditions();
print_test_result(test_result, "Error Conditions and Edge Cases");
if (!test_result) all_passed = 0;
// Test 7: nak integration (optional - may fail if nak not available)
printf("\n=== Optional nak Integration Test ===\n");
test_result = test_nak_integration();
if (test_result) {
print_test_result(test_result, "Integration with nak-generated PoW events");
} else {
printf("⚠️ SKIP: Integration with nak-generated PoW events (nak may not be available)\n");
// Don't fail the entire test suite for this optional test
}
// 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-13 PoW implementation is working correctly.\n");
printf("✅ PoW generation works\n");
printf("✅ PoW difficulty calculation works\n");
printf("✅ Nonce extraction works\n");
printf("✅ PoW validation 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;
}

690
tests/nip42_test.c Normal file
View File

@@ -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;
}

View File

@@ -1,318 +0,0 @@
/*
* NOSTR Relay Pool Test Program (READ-ONLY)
*
* Tests the relay pool event processing functionality by:
* - Creating a pool with hardcoded relays
* - Subscribing to kind 1 events (text notes) from other users
* - Using the new event processing functions
* - Displaying raw data output without interpretation
*
* IMPORTANT: This test is READ-ONLY and never publishes events.
* It only sends REQ (subscription) messages and receives EVENT responses.
* Any test events seen in output are from other users or previous test runs.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
// Global variables for clean shutdown
static volatile int keep_running = 1;
static nostr_relay_pool_t* g_pool = NULL;
static nostr_pool_subscription_t* g_subscription = NULL;
// Statistics tracking
static int events_received = 0;
static int events_per_relay[3] = {0, 0, 0}; // Track events per relay
static const char* relay_urls[] = {
"wss://relay.laantungir.net",
"ws://127.0.0.1:7777",
"wss://nostr.mom"
};
static const int relay_count = 3;
// Signal handler for clean shutdown
void signal_handler(int sig) {
(void)sig; // Unused parameter
printf("\n🛑 Received shutdown signal, cleaning up...\n");
keep_running = 0;
}
// Event callback - called when events are received
void on_event_received(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data; // Unused parameter
events_received++;
// Track events per relay
for (int i = 0; i < relay_count; i++) {
if (strcmp(relay_url, relay_urls[i]) == 0) {
events_per_relay[i]++;
break;
}
}
// Print raw event data
char* event_json = cJSON_Print(event);
if (event_json) {
printf("\n📨 EVENT from %s:\n", relay_url);
printf("Raw JSON: %s\n", event_json);
printf("---\n");
free(event_json);
}
// Also extract and display key fields for readability
cJSON* id = cJSON_GetObjectItem(event, "id");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
cJSON* content = cJSON_GetObjectItem(event, "content");
printf("📄 Parsed fields:\n");
if (id && cJSON_IsString(id)) {
printf(" ID: %s\n", cJSON_GetStringValue(id));
}
if (pubkey && cJSON_IsString(pubkey)) {
printf(" Author: %s\n", cJSON_GetStringValue(pubkey));
}
if (created_at && cJSON_IsNumber(created_at)) {
time_t timestamp = (time_t)cJSON_GetNumberValue(created_at);
printf(" Created: %s", ctime(&timestamp));
}
if (content && cJSON_IsString(content)) {
const char* text = cJSON_GetStringValue(content);
printf(" Content: %.100s%s\n", text, strlen(text) > 100 ? "..." : "");
}
printf("===============================\n");
}
// EOSE callback - called when all relays have sent "End of Stored Events"
void on_eose_received(void* user_data) {
(void)user_data; // Unused parameter
printf("✅ EOSE: All relays have finished sending stored events\n");
}
// Display relay status
void display_relay_status() {
char** urls;
nostr_pool_relay_status_t* statuses;
int count = nostr_relay_pool_list_relays(g_pool, &urls, &statuses);
if (count > 0) {
printf("\n🔗 RELAY STATUS:\n");
for (int i = 0; i < count; i++) {
const char* status_icon;
const char* status_text;
switch (statuses[i]) {
case NOSTR_POOL_RELAY_CONNECTED:
status_icon = "🟢";
status_text = "Connected";
break;
case NOSTR_POOL_RELAY_CONNECTING:
status_icon = "🟡";
status_text = "Connecting...";
break;
case NOSTR_POOL_RELAY_DISCONNECTED:
status_icon = "🔴";
status_text = "Disconnected";
break;
case NOSTR_POOL_RELAY_ERROR:
status_icon = "";
status_text = "Error";
break;
default:
status_icon = "";
status_text = "Unknown";
break;
}
// Get publish and query latency statistics
double query_latency = nostr_relay_pool_get_relay_query_latency(g_pool, urls[i]);
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, urls[i]);
// Get events count from relay statistics (more accurate)
int relay_events = 0;
if (stats) {
relay_events = stats->events_received;
} else {
// Fallback to local counter
for (int j = 0; j < relay_count; j++) {
if (strcmp(urls[i], relay_urls[j]) == 0) {
relay_events = events_per_relay[j];
break;
}
}
}
// Display status with latency information
if (query_latency >= 0.0) {
printf(" %s %-25s %s (query: %.0fms, events: %d)\n",
status_icon, urls[i], status_text, query_latency, relay_events);
} else {
printf(" %s %-25s %s (query: ---, events: %d)\n",
status_icon, urls[i], status_text, relay_events);
}
// Show additional latency statistics if available
if (stats) {
if (stats->publish_samples > 0) {
printf(" 📊 Publish latency: avg=%.0fms (%d samples)\n",
stats->publish_latency_avg, stats->publish_samples);
}
if (stats->query_samples > 0) {
printf(" 📊 Query latency: avg=%.0fms (%d samples)\n",
stats->query_latency_avg, stats->query_samples);
}
if (stats->events_published > 0) {
printf(" 📤 Published: %d events (%d OK, %d failed)\n",
stats->events_published, stats->events_published_ok,
stats->events_published_failed);
}
}
free(urls[i]);
}
free(urls);
free(statuses);
printf("📊 Total events received: %d\n", events_received);
}
}
int main() {
printf("🚀 NOSTR Relay Pool Test Program\n");
printf("=================================\n");
printf("Testing relays:\n");
for (int i = 0; i < relay_count; i++) {
printf(" - %s\n", relay_urls[i]);
}
printf("\n");
// Set up signal handler for clean shutdown
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Initialize NOSTR core library
printf("🔧 Initializing NOSTR core library...\n");
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "❌ Failed to initialize NOSTR core library\n");
return 1;
}
// Create relay pool
printf("🏊 Creating relay pool...\n");
g_pool = nostr_relay_pool_create();
if (!g_pool) {
fprintf(stderr, "❌ Failed to create relay pool\n");
nostr_cleanup();
return 1;
}
// Add relays to pool
printf(" Adding relays to pool...\n");
for (int i = 0; i < relay_count; i++) {
printf(" Adding: %s\n", relay_urls[i]);
int result = nostr_relay_pool_add_relay(g_pool, relay_urls[i]);
if (result != NOSTR_SUCCESS) {
printf(" ⚠️ Warning: Failed to add relay %s (error: %s)\n",
relay_urls[i], nostr_strerror(result));
}
}
// Create filter for kind 1 events (text notes)
printf("🔍 Creating subscription filter for kind 1 events...\n");
cJSON* filter = cJSON_CreateObject();
if (!filter) {
fprintf(stderr, "❌ Failed to create filter\n");
nostr_relay_pool_destroy(g_pool);
nostr_cleanup();
return 1;
}
// Add kinds array with kind 1 (text notes)
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
// Limit to recent events to avoid flooding
cJSON_AddNumberToObject(filter, "limit", 1);
// Subscribe to events from all relays
printf("📡 Subscribing to events from all relays...\n");
g_subscription = nostr_relay_pool_subscribe(
g_pool,
relay_urls,
relay_count,
filter,
on_event_received,
on_eose_received,
NULL
);
if (!g_subscription) {
fprintf(stderr, "❌ Failed to create subscription\n");
cJSON_Delete(filter);
nostr_relay_pool_destroy(g_pool);
nostr_cleanup();
return 1;
}
printf("✅ Subscription created successfully!\n");
printf("⏱️ Starting event processing...\n");
printf(" (Press Ctrl+C to stop)\n\n");
// Display initial status
display_relay_status();
printf("<EFBFBD> Starting continuous monitoring...\n\n");
// Run event processing loop
time_t last_status_update = time(NULL);
while (keep_running) {
// Process events for 1 second
int events_processed = nostr_relay_pool_run(g_pool, 1000);
// Display status every 5 seconds
if (time(NULL) - last_status_update >= 5) {
display_relay_status();
last_status_update = time(NULL);
}
// Small status indicator
if (events_processed > 0) {
printf(".");
fflush(stdout);
}
}
printf("\n\n🏁 Test completed!\n");
// Final status display
display_relay_status();
// Cleanup
printf("🧹 Cleaning up...\n");
if (g_subscription) {
nostr_pool_subscription_close(g_subscription);
}
if (g_pool) {
nostr_relay_pool_destroy(g_pool);
}
cJSON_Delete(filter);
nostr_cleanup();
printf("✅ Test program finished successfully!\n");
printf("📈 Final stats:\n");
printf(" Total events: %d\n", events_received);
for (int i = 0; i < relay_count; i++) {
printf(" %s: %d events\n", relay_urls[i], events_per_relay[i]);
}
return 0;
}

View File

@@ -1,101 +0,0 @@
/*
* NIP-04 Test Vectors Display - All 6 Test Vectors
* Shows complete test vector integration even if runtime testing has issues
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void display_test_vector(int num, const char* description, const char* sk1, const char* pk1,
const char* sk2, const char* pk2, const char* plaintext, const char* expected) {
printf("=== TEST VECTOR %d: %s ===\n", num, description);
printf("SK1 (Alice): %s\n", sk1);
printf("PK1 (Alice): %s\n", pk1);
printf("SK2 (Bob): %s\n", sk2);
printf("PK2 (Bob): %s\n", pk2);
printf("Plaintext: \"%s\"\n", plaintext);
if (strlen(expected) > 80) {
char truncated[81];
strncpy(truncated, expected, 80);
truncated[80] = '\0';
printf("Expected: %s...\n", truncated);
} else {
printf("Expected: %s\n", expected);
}
printf("\n");
}
int main(void) {
printf("=== NIP-04 Test Vector Collection ===\n");
printf("Complete integration of 6 test vectors (3 original + 3 from nostr-tools)\n\n");
// Original Test Vectors (1-3)
display_test_vector(1, "Basic NIP-04 Encryption",
"91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe",
"b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1",
"96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220",
"dcb33a629560280a0ee3b6b99b68c044fe8914ad8a984001ebf6099a9b474dc3",
"nanana",
"zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ==");
display_test_vector(2, "Large Payload Test (800 characters)",
"91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe",
"b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1",
"96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220",
"dcb33a629560280a0ee3b6b99b68c044fe8914ad8a984001ebf6099a9b474dc3",
"800 'z' characters",
"6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g==");
display_test_vector(3, "Bidirectional Communication",
"91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe",
"b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1",
"96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220",
"dcb33a629560280a0ee3b6b99b68c044fe8914ad8a984001ebf6099a9b474dc3",
"Hello Bob, this is Alice! / Hi Alice, Bob here. Message received!",
"Various encrypted messages");
printf("--- Generated with nostr-tools (Random Keys) ---\n\n");
// New Test Vectors Generated with nostr-tools (4-6)
display_test_vector(4, "Random Keys - Hello, NOSTR!",
"5c5ea5ec3a804533ba8a21ba3dd981fc55a84e854dde53869b3f812ccd788200",
"0988b20763d3f8bc06e88722f2aa6b3caed3cc510e93287e1ee3f70ed22f54d2",
"8e94e91ea679509ec1f5da2be87352ea78acde2b69563c23a41b7f07c0891bc3",
"13747a8025c1196da3e67ecf941aa889c5c4ec6773e7f325f3f8d2435c4603c6",
"Hello, NOSTR!",
"+bqZAkfv/tI4h0XcvB9Baw==?iv=Om7m3at5zjJjxyAQbFY2IQ==");
display_test_vector(5, "Long Message with Emoji",
"51099e755aaab7e8ee1850b683b673c11d09799e85a630e951eb3c92fab4aed3",
"c5fb1cad7b11e3cf7f31d5bf47aaf3398a4803ea786eedfd674f55fa55dcb649",
"41f2788d00bd362ac3c7c784ee46e35b99765a086514ee69cb15de38c072309a",
"ba6773cf6a9b11476f692d4681a2f1e3015d1ee4a8d7c9d0364bed120f225079",
"This is a longer message to test encryption with more content. 🚀",
"3H9WEg9WjjN3r6ZymJt1R4ly3GlzhRR93FaSTGHLeM4oSS3eOnJtdXcO4ftgICMHRYM14WAmDDE9c12V8jhzua8GpnXKIVsNbY+oPF2yRwI=?iv=ztEGlo35pqJKrwZ2ZipsWg==");
display_test_vector(6, "Short Message",
"42c450eaebaee5ad94b602fc9054cde48f66d68c236b547aafee0ff319377290",
"a03f543eeb6c3f1c626181730751c39fd4f9f10455756d99ea855da97cf5076b",
"72f424c96239d271549c648d16635b5603ef32cdcbbff41058d14187b98f30cc",
"1c74b7a1d09ebeaf994a93a859682019930ad4f0f8ac7e65caacbbf4985042e8",
"Short",
"UIN92yHtAfX0vOTmn8VTtg==?iv=ou0QFU5UJUI6W4fUlkiElg==");
printf("=== SUMMARY ===\n");
printf("✅ Successfully generated 3 additional test vectors using nostr-tools\n");
printf("✅ All test vectors use genuine random nsec keys from the JavaScript ecosystem\n");
printf("✅ Test coverage includes: short, medium, long, Unicode, and emoji messages\n");
printf("✅ Enhanced from 3 to 6 comprehensive test vectors\n");
printf("✅ Ready for integration testing once library stability issues are resolved\n");
printf("\n");
printf("Files created:\n");
printf("- test_vector_generator/generate_vectors.js (Vector generation script)\n");
printf("- tests/nip04_test.c (Enhanced with 6 test vectors)\n");
printf("- package.json (Node.js dependencies)\n");
printf("\n");
printf("🎯 Mission accomplished - Enhanced NIP-04 test coverage with nostr-tools vectors!\n");
return 0;
}

View File

@@ -0,0 +1,532 @@
/*
* NOSTR Core Library - Streaming SHA-256 Tests
*
* Comprehensive tests for streaming SHA-256 implementation
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include "../nostr_core/utils.h"
#include "../nostr_core/nostr_common.h"
// Test vectors for SHA-256 (from official NIST test vectors)
typedef struct {
const char* input;
const char* expected_hash;
const char* description;
} sha256_test_vector_t;
static const sha256_test_vector_t test_vectors[] = {
{
"",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"Empty string"
},
{
"a",
"ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
"Single character"
},
{
"abc",
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"Three characters"
},
{
"message digest",
"f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650",
"Message digest"
},
{
"abcdefghijklmnopqrstuvwxyz",
"71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73",
"Alphabet"
},
{
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
"db4bfcbd4da0cd85a60c3c37d3fbd8805c77f15fc6b1fdfe614ee0a7c8fdb4c0",
"Alphanumeric"
},
{
"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
"f371bc4a311f2b009eef952dd83ca80e2b60026c8e935592d0f9c308453c813e",
"Numeric pattern"
}
};
static const size_t num_test_vectors = sizeof(test_vectors) / sizeof(test_vectors[0]);
// Helper function to convert hex string to bytes for comparison
static void hex_to_bytes_helper(const char* hex_str, unsigned char* bytes) {
size_t len = strlen(hex_str) / 2;
for (size_t i = 0; i < len; i++) {
sscanf(hex_str + i * 2, "%02hhx", &bytes[i]);
}
}
// Helper function to print hash in hex format
static void print_hash_hex(const unsigned char* hash, const char* label) {
printf("%s: ", label);
for (int i = 0; i < 32; i++) {
printf("%02x", hash[i]);
}
printf("\n");
}
// Test 1: Basic streaming functionality vs traditional SHA-256
static void test_streaming_vs_traditional(void) {
printf("\n=== Test 1: Streaming vs Traditional SHA-256 ===\n");
int passed = 0, failed = 0;
for (size_t i = 0; i < num_test_vectors; i++) {
const sha256_test_vector_t* tv = &test_vectors[i];
unsigned char traditional_hash[32];
unsigned char streaming_hash[32];
unsigned char expected_hash[32];
printf("Testing: %s\n", tv->description);
printf("Input: \"%s\"\n", tv->input);
// Traditional SHA-256
if (nostr_sha256((const unsigned char*)tv->input, strlen(tv->input), traditional_hash) != 0) {
printf("❌ Traditional SHA-256 failed\n");
failed++;
continue;
}
// Streaming SHA-256
nostr_sha256_ctx_t ctx;
if (nostr_sha256_init(&ctx) != 0) {
printf("❌ Streaming SHA-256 init failed\n");
failed++;
continue;
}
if (nostr_sha256_update(&ctx, (const unsigned char*)tv->input, strlen(tv->input)) != 0) {
printf("❌ Streaming SHA-256 update failed\n");
failed++;
continue;
}
if (nostr_sha256_final(&ctx, streaming_hash) != 0) {
printf("❌ Streaming SHA-256 final failed\n");
failed++;
continue;
}
// Convert expected hash
hex_to_bytes_helper(tv->expected_hash, expected_hash);
// Compare all three results
if (memcmp(traditional_hash, expected_hash, 32) != 0) {
printf("❌ Traditional hash mismatch\n");
print_hash_hex(expected_hash, "Expected");
print_hash_hex(traditional_hash, "Traditional");
failed++;
continue;
}
if (memcmp(streaming_hash, expected_hash, 32) != 0) {
printf("❌ Streaming hash mismatch\n");
print_hash_hex(expected_hash, "Expected");
print_hash_hex(streaming_hash, "Streaming");
failed++;
continue;
}
if (memcmp(traditional_hash, streaming_hash, 32) != 0) {
printf("❌ Traditional vs Streaming mismatch\n");
print_hash_hex(traditional_hash, "Traditional");
print_hash_hex(streaming_hash, "Streaming");
failed++;
continue;
}
printf("✅ All hashes match expected result\n");
printf("Hash: %s\n", tv->expected_hash);
passed++;
}
printf("\nTest 1 Results: %d passed, %d failed\n", passed, failed);
}
// Test 2: Multiple update calls (chunk boundary testing)
static void test_multiple_updates(void) {
printf("\n=== Test 2: Multiple Update Calls ===\n");
const char* test_string = "abcdefghijklmnopqrstuvwxyz";
unsigned char expected_hash[32];
unsigned char streaming_hash[32];
// Get expected result from traditional function
nostr_sha256((const unsigned char*)test_string, strlen(test_string), expected_hash);
printf("Testing alphabet string with multiple update calls\n");
printf("Input: \"%s\"\n", test_string);
// Test various chunk sizes
size_t chunk_sizes[] = {1, 2, 3, 5, 7, 11, 13, 17, 19, 23};
size_t num_chunk_sizes = sizeof(chunk_sizes) / sizeof(chunk_sizes[0]);
int passed = 0, failed = 0;
for (size_t cs_idx = 0; cs_idx < num_chunk_sizes; cs_idx++) {
size_t chunk_size = chunk_sizes[cs_idx];
printf("Testing chunk size: %zu\n", chunk_size);
nostr_sha256_ctx_t ctx;
if (nostr_sha256_init(&ctx) != 0) {
printf("❌ Init failed for chunk size %zu\n", chunk_size);
failed++;
continue;
}
// Process string in chunks
size_t input_len = strlen(test_string);
size_t processed = 0;
while (processed < input_len) {
size_t to_process = (input_len - processed < chunk_size) ?
(input_len - processed) : chunk_size;
if (nostr_sha256_update(&ctx, (const unsigned char*)(test_string + processed), to_process) != 0) {
printf("❌ Update failed at position %zu with chunk size %zu\n", processed, chunk_size);
failed++;
break;
}
processed += to_process;
}
if (processed != input_len) continue; // Skip final if update failed
if (nostr_sha256_final(&ctx, streaming_hash) != 0) {
printf("❌ Final failed for chunk size %zu\n", chunk_size);
failed++;
continue;
}
if (memcmp(streaming_hash, expected_hash, 32) != 0) {
printf("❌ Hash mismatch for chunk size %zu\n", chunk_size);
print_hash_hex(expected_hash, "Expected");
print_hash_hex(streaming_hash, "Streaming");
failed++;
continue;
}
printf("✅ Chunk size %zu: hash matches\n", chunk_size);
passed++;
}
printf("\nTest 2 Results: %d passed, %d failed\n", passed, failed);
}
// Test 3: Large data streaming (memory efficiency test)
static void test_large_data_streaming(void) {
printf("\n=== Test 3: Large Data Streaming ===\n");
// Create a large test pattern (1MB of repeated data)
const size_t total_size = 1024 * 1024; // 1MB
char* large_data = malloc(total_size);
if (!large_data) {
printf("❌ Failed to allocate memory for large data test\n");
return;
}
// Fill with repeating pattern
const char* pattern = "The quick brown fox jumps over the lazy dog. ";
size_t pattern_len = strlen(pattern);
for (size_t i = 0; i < total_size; i++) {
large_data[i] = pattern[i % pattern_len];
}
printf("Testing 1MB of data streaming vs traditional\n");
unsigned char traditional_hash[32];
unsigned char streaming_hash[32];
// Traditional approach (loads entire data into memory - we already have it)
if (nostr_sha256((const unsigned char*)large_data, total_size, traditional_hash) != 0) {
printf("❌ Traditional SHA-256 failed on large data\n");
free(large_data);
return;
}
// Streaming approach (processes in 4KB chunks)
nostr_sha256_ctx_t ctx;
if (nostr_sha256_init(&ctx) != 0) {
printf("❌ Streaming init failed\n");
free(large_data);
return;
}
const size_t chunk_size = 4096; // 4KB chunks
size_t processed = 0;
while (processed < total_size) {
size_t to_process = (total_size - processed < chunk_size) ?
(total_size - processed) : chunk_size;
if (nostr_sha256_update(&ctx, (const unsigned char*)(large_data + processed), to_process) != 0) {
printf("❌ Streaming update failed at position %zu\n", processed);
free(large_data);
return;
}
processed += to_process;
}
if (nostr_sha256_final(&ctx, streaming_hash) != 0) {
printf("❌ Streaming final failed\n");
free(large_data);
return;
}
free(large_data);
// Compare results
if (memcmp(traditional_hash, streaming_hash, 32) != 0) {
printf("❌ Large data hash mismatch\n");
print_hash_hex(traditional_hash, "Traditional");
print_hash_hex(streaming_hash, "Streaming");
return;
}
printf("✅ 1MB data processed successfully with streaming\n");
print_hash_hex(streaming_hash, "Hash");
printf("Memory efficiency: Streaming uses ~4KB buffer vs 1MB for traditional\n");
}
// Test 4: File streaming functionality
static void test_file_streaming(void) {
printf("\n=== Test 4: File Streaming ===\n");
const char* test_filename = "test_streaming_file.tmp";
const char* test_content = "This is a test file for streaming SHA-256 functionality.\n"
"It contains multiple lines to test file I/O.\n"
"The streaming function should read this file in chunks.\n"
"And produce the same hash as if we read it all at once.\n";
// Create test file
FILE* file = fopen(test_filename, "wb");
if (!file) {
printf("❌ Failed to create test file\n");
return;
}
fwrite(test_content, 1, strlen(test_content), file);
fclose(file);
printf("Testing file streaming with content:\n\"%s\"\n", test_content);
// Get expected hash from memory-based function
unsigned char expected_hash[32];
if (nostr_sha256((const unsigned char*)test_content, strlen(test_content), expected_hash) != 0) {
printf("❌ Memory-based hash failed\n");
unlink(test_filename);
return;
}
// Test file streaming
unsigned char file_hash[32];
if (nostr_sha256_file_stream(test_filename, file_hash) != 0) {
printf("❌ File streaming failed\n");
unlink(test_filename);
return;
}
// Compare results
if (memcmp(expected_hash, file_hash, 32) != 0) {
printf("❌ File hash mismatch\n");
print_hash_hex(expected_hash, "Expected");
print_hash_hex(file_hash, "File stream");
unlink(test_filename);
return;
}
printf("✅ File streaming matches memory-based hash\n");
print_hash_hex(file_hash, "Hash");
// Clean up
unlink(test_filename);
// Test with non-existent file
if (nostr_sha256_file_stream("non_existent_file.tmp", file_hash) == 0) {
printf("❌ File streaming should fail for non-existent file\n");
return;
}
printf("✅ File streaming properly handles non-existent files\n");
}
// Test 5: Edge cases and error conditions
static void test_edge_cases(void) {
printf("\n=== Test 5: Edge Cases and Error Conditions ===\n");
nostr_sha256_ctx_t ctx;
unsigned char hash[32];
int passed = 0, failed = 0;
// Test NULL pointer handling
printf("Testing NULL pointer handling:\n");
if (nostr_sha256_init(NULL) == 0) {
printf("❌ Init should fail with NULL context\n");
failed++;
} else {
printf("✅ Init properly rejects NULL context\n");
passed++;
}
if (nostr_sha256_update(&ctx, NULL, 10) == 0) {
printf("❌ Update should fail with NULL data\n");
failed++;
} else {
printf("✅ Update properly rejects NULL data\n");
passed++;
}
if (nostr_sha256_final(&ctx, NULL) == 0) {
printf("❌ Final should fail with NULL output\n");
failed++;
} else {
printf("✅ Final properly rejects NULL output\n");
passed++;
}
if (nostr_sha256_file_stream(NULL, hash) == 0) {
printf("❌ File stream should fail with NULL filename\n");
failed++;
} else {
printf("✅ File stream properly rejects NULL filename\n");
passed++;
}
if (nostr_sha256_file_stream("test.tmp", NULL) == 0) {
printf("❌ File stream should fail with NULL output\n");
failed++;
} else {
printf("✅ File stream properly rejects NULL output\n");
passed++;
}
// Test zero-length updates
printf("\nTesting zero-length operations:\n");
if (nostr_sha256_init(&ctx) != 0) {
printf("❌ Failed to initialize for zero-length test\n");
failed++;
} else {
if (nostr_sha256_update(&ctx, (const unsigned char*)"test", 0) != 0) {
printf("❌ Zero-length update should succeed\n");
failed++;
} else {
printf("✅ Zero-length update handled correctly\n");
passed++;
}
if (nostr_sha256_final(&ctx, hash) != 0) {
printf("❌ Final failed after zero-length update\n");
failed++;
} else {
// Should match empty string hash
unsigned char empty_hash[32];
hex_to_bytes_helper("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", empty_hash);
if (memcmp(hash, empty_hash, 32) != 0) {
printf("❌ Zero-length result doesn't match empty string hash\n");
failed++;
} else {
printf("✅ Zero-length result matches empty string hash\n");
passed++;
}
}
}
printf("\nTest 5 Results: %d passed, %d failed\n", passed, failed);
}
// Test 6: Context reuse and security clearing
static void test_context_security(void) {
printf("\n=== Test 6: Context Security and Reuse ===\n");
nostr_sha256_ctx_t ctx;
unsigned char hash1[32], hash2[32];
const char* test1 = "first test";
const char* test2 = "second test";
// First hash
if (nostr_sha256_init(&ctx) != 0 ||
nostr_sha256_update(&ctx, (const unsigned char*)test1, strlen(test1)) != 0 ||
nostr_sha256_final(&ctx, hash1) != 0) {
printf("❌ First hash computation failed\n");
return;
}
printf("First hash computed successfully\n");
// Check that context is cleared after final (security feature)
// We can't directly inspect the context, but we can try to reuse it
// Second hash with new init (proper way)
if (nostr_sha256_init(&ctx) != 0 ||
nostr_sha256_update(&ctx, (const unsigned char*)test2, strlen(test2)) != 0 ||
nostr_sha256_final(&ctx, hash2) != 0) {
printf("❌ Second hash computation failed\n");
return;
}
printf("Second hash computed successfully\n");
// Verify they're different (they should be)
if (memcmp(hash1, hash2, 32) == 0) {
printf("❌ Hashes should be different for different inputs\n");
return;
}
printf("✅ Context can be reused with proper reinitialization\n");
printf("✅ Context clearing prevents accidental reuse\n");
// Verify hashes against expected values
unsigned char expected1[32], expected2[32];
nostr_sha256((const unsigned char*)test1, strlen(test1), expected1);
nostr_sha256((const unsigned char*)test2, strlen(test2), expected2);
if (memcmp(hash1, expected1, 32) == 0 && memcmp(hash2, expected2, 32) == 0) {
printf("✅ Both streaming hashes match expected values\n");
} else {
printf("❌ Hash verification failed\n");
}
}
int main(void) {
printf("NOSTR Core Library - Streaming SHA-256 Tests\n");
printf("===========================================\n");
// Initialize crypto subsystem
if (nostr_crypto_init() != 0) {
printf("❌ Failed to initialize crypto subsystem\n");
return 1;
}
// Run all tests
test_streaming_vs_traditional();
test_multiple_updates();
test_large_data_streaming();
test_file_streaming();
test_edge_cases();
test_context_security();
// Cleanup
nostr_crypto_cleanup();
printf("\n===========================================\n");
printf("All streaming SHA-256 tests completed.\n");
printf("Review output above for detailed results.\n");
return 0;
}