517 lines
16 KiB
Markdown
517 lines
16 KiB
Markdown
# NIP-59 Timestamp Configuration Implementation Plan
|
|
|
|
## Overview
|
|
Add configurable timestamp randomization for NIP-59 gift wraps to improve compatibility with Nostr apps that don't implement timestamp randomization.
|
|
|
|
## Problem Statement
|
|
The NIP-59 protocol specifies that timestamps on gift wraps should have randomness to prevent time-analysis attacks. However, some Nostr platforms don't implement this, causing compatibility issues with direct messaging (NIP-17).
|
|
|
|
## Solution
|
|
Add a configuration parameter `nip59_timestamp_max_delay_sec` that controls the maximum random delay applied to timestamps:
|
|
- **Value = 0**: Use current timestamp (no randomization) for maximum compatibility
|
|
- **Value > 0**: Use random timestamp between now and N seconds ago
|
|
- **Default = 0**: Maximum compatibility mode (no randomization)
|
|
|
|
## Implementation Approach: Option B (Direct Parameter Addition)
|
|
We chose Option B because:
|
|
1. Explicit and stateless - value flows through call chain
|
|
2. Thread-safe by design
|
|
3. No global state needed in nostr_core_lib
|
|
4. DMs are sent rarely, so database query per call is acceptable
|
|
|
|
---
|
|
|
|
## Detailed Implementation Steps
|
|
|
|
### Phase 1: Configuration Setup in c-relay
|
|
|
|
#### 1.1 Add Configuration Parameter
|
|
**File:** `src/default_config_event.h`
|
|
**Location:** Line 82 (after `trust_proxy_headers`)
|
|
|
|
```c
|
|
// NIP-59 Gift Wrap Timestamp Configuration
|
|
{"nip59_timestamp_max_delay_sec", "0"} // Default: 0 (no randomization for compatibility)
|
|
```
|
|
|
|
**Rationale:**
|
|
- Default of 0 seconds (no randomization) for maximum compatibility
|
|
- Placed after proxy settings, before closing brace
|
|
- Follows existing naming convention
|
|
|
|
#### 1.2 Add Configuration Validation
|
|
**File:** `src/config.c`
|
|
**Function:** `validate_config_field()` (around line 923)
|
|
|
|
Add validation case:
|
|
```c
|
|
else if (strcmp(key, "nip59_timestamp_max_delay_sec") == 0) {
|
|
long value = strtol(value_str, NULL, 10);
|
|
if (value < 0 || value > 604800) { // Max 7 days
|
|
snprintf(error_msg, error_size,
|
|
"nip59_timestamp_max_delay_sec must be between 0 and 604800 (7 days)");
|
|
return -1;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rationale:**
|
|
- 0 = no randomization (compatibility mode)
|
|
- 604800 = 7 days maximum (reasonable upper bound)
|
|
- Prevents negative values or excessive delays
|
|
|
|
---
|
|
|
|
### Phase 2: Modify nostr_core_lib Functions
|
|
|
|
#### 2.1 Update random_past_timestamp() Function
|
|
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
|
**Current Location:** Lines 31-36
|
|
|
|
**Current Code:**
|
|
```c
|
|
static time_t random_past_timestamp(void) {
|
|
time_t now = time(NULL);
|
|
// Random time up to 2 days (172800 seconds) in the past
|
|
long random_offset = (long)(rand() % 172800);
|
|
return now - random_offset;
|
|
}
|
|
```
|
|
|
|
**New Code:**
|
|
```c
|
|
static time_t random_past_timestamp(long max_delay_sec) {
|
|
time_t now = time(NULL);
|
|
|
|
// If max_delay_sec is 0, return current timestamp (no randomization)
|
|
if (max_delay_sec == 0) {
|
|
return now;
|
|
}
|
|
|
|
// Random time up to max_delay_sec in the past
|
|
long random_offset = (long)(rand() % max_delay_sec);
|
|
return now - random_offset;
|
|
}
|
|
```
|
|
|
|
**Changes:**
|
|
- Add `long max_delay_sec` parameter
|
|
- Handle special case: `max_delay_sec == 0` returns current time
|
|
- Use `max_delay_sec` instead of hardcoded 172800
|
|
|
|
#### 2.2 Update nostr_nip59_create_seal() Function
|
|
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
|
**Current Location:** Lines 144-215
|
|
|
|
**Function Signature Change:**
|
|
```c
|
|
// OLD:
|
|
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
|
const unsigned char* sender_private_key,
|
|
const unsigned char* recipient_public_key);
|
|
|
|
// NEW:
|
|
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
|
const unsigned char* sender_private_key,
|
|
const unsigned char* recipient_public_key,
|
|
long max_delay_sec);
|
|
```
|
|
|
|
**Code Change at Line 181:**
|
|
```c
|
|
// OLD:
|
|
time_t seal_time = random_past_timestamp();
|
|
|
|
// NEW:
|
|
time_t seal_time = random_past_timestamp(max_delay_sec);
|
|
```
|
|
|
|
#### 2.3 Update nostr_nip59_create_gift_wrap() Function
|
|
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
|
**Current Location:** Lines 220-323
|
|
|
|
**Function Signature Change:**
|
|
```c
|
|
// OLD:
|
|
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
|
const char* recipient_public_key_hex);
|
|
|
|
// NEW:
|
|
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
|
const char* recipient_public_key_hex,
|
|
long max_delay_sec);
|
|
```
|
|
|
|
**Code Change at Line 275:**
|
|
```c
|
|
// OLD:
|
|
time_t wrap_time = random_past_timestamp();
|
|
|
|
// NEW:
|
|
time_t wrap_time = random_past_timestamp(max_delay_sec);
|
|
```
|
|
|
|
#### 2.4 Update nip059.h Header
|
|
**File:** `nostr_core_lib/nostr_core/nip059.h`
|
|
**Locations:** Lines 38-39 and 48
|
|
|
|
**Update Function Declarations:**
|
|
```c
|
|
// Line 38-39: Update nostr_nip59_create_seal
|
|
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
|
const unsigned char* sender_private_key,
|
|
const unsigned char* recipient_public_key,
|
|
long max_delay_sec);
|
|
|
|
// Line 48: Update nostr_nip59_create_gift_wrap
|
|
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
|
const char* recipient_public_key_hex,
|
|
long max_delay_sec);
|
|
```
|
|
|
|
**Update Documentation Comments:**
|
|
```c
|
|
/**
|
|
* NIP-59: Create a seal (kind 13) wrapping a rumor
|
|
*
|
|
* @param rumor The rumor event to seal (cJSON object)
|
|
* @param sender_private_key 32-byte sender private key
|
|
* @param recipient_public_key 32-byte recipient public key (x-only)
|
|
* @param max_delay_sec Maximum random delay in seconds (0 = no randomization)
|
|
* @return cJSON object representing the seal event, or NULL on error
|
|
*/
|
|
|
|
/**
|
|
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
|
|
*
|
|
* @param seal The seal event to wrap (cJSON object)
|
|
* @param recipient_public_key_hex Recipient's public key in hex format
|
|
* @param max_delay_sec Maximum random delay in seconds (0 = no randomization)
|
|
* @return cJSON object representing the gift wrap event, or NULL on error
|
|
*/
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 3: Update NIP-17 Integration
|
|
|
|
#### 3.1 Update nostr_nip17_send_dm() Function
|
|
**File:** `nostr_core_lib/nostr_core/nip017.c`
|
|
**Current Location:** Lines 260-320
|
|
|
|
**Function Signature Change:**
|
|
```c
|
|
// OLD:
|
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
|
const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const unsigned char* sender_private_key,
|
|
cJSON** gift_wraps_out,
|
|
int max_gift_wraps);
|
|
|
|
// NEW:
|
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
|
const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const unsigned char* sender_private_key,
|
|
cJSON** gift_wraps_out,
|
|
int max_gift_wraps,
|
|
long max_delay_sec);
|
|
```
|
|
|
|
**Code Changes:**
|
|
|
|
At line 281 (seal creation):
|
|
```c
|
|
// OLD:
|
|
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key);
|
|
|
|
// NEW:
|
|
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec);
|
|
```
|
|
|
|
At line 287 (gift wrap creation):
|
|
```c
|
|
// OLD:
|
|
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i]);
|
|
|
|
// NEW:
|
|
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec);
|
|
```
|
|
|
|
At line 306 (sender seal creation):
|
|
```c
|
|
// OLD:
|
|
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key);
|
|
|
|
// NEW:
|
|
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec);
|
|
```
|
|
|
|
At line 309 (sender gift wrap creation):
|
|
```c
|
|
// OLD:
|
|
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex);
|
|
|
|
// NEW:
|
|
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec);
|
|
```
|
|
|
|
#### 3.2 Update nip017.h Header
|
|
**File:** `nostr_core_lib/nostr_core/nip017.h`
|
|
**Location:** Lines 102-107
|
|
|
|
**Update Function Declaration:**
|
|
```c
|
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
|
const char** recipient_pubkeys,
|
|
int num_recipients,
|
|
const unsigned char* sender_private_key,
|
|
cJSON** gift_wraps_out,
|
|
int max_gift_wraps,
|
|
long max_delay_sec);
|
|
```
|
|
|
|
**Update Documentation Comment (lines 88-100):**
|
|
```c
|
|
/**
|
|
* NIP-17: Send a direct message to recipients
|
|
*
|
|
* This function creates the appropriate rumor, seals it, gift wraps it,
|
|
* and returns the final gift wrap events ready for publishing.
|
|
*
|
|
* @param dm_event The unsigned DM event (kind 14 or 15)
|
|
* @param recipient_pubkeys Array of recipient public keys (hex strings)
|
|
* @param num_recipients Number of recipients
|
|
* @param sender_private_key 32-byte sender private key
|
|
* @param gift_wraps_out Array to store resulting gift wrap events (caller must free)
|
|
* @param max_gift_wraps Maximum number of gift wraps to create
|
|
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
|
|
* @return Number of gift wrap events created, or -1 on error
|
|
*/
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 4: Update c-relay Call Sites
|
|
|
|
#### 4.1 Update src/api.c
|
|
**Location:** Line 1319
|
|
|
|
**Current Code:**
|
|
```c
|
|
int send_result = nostr_nip17_send_dm(
|
|
dm_response, // dm_event
|
|
recipient_pubkeys, // recipient_pubkeys
|
|
1, // num_recipients
|
|
relay_privkey, // sender_private_key
|
|
gift_wraps, // gift_wraps_out
|
|
1 // max_gift_wraps
|
|
);
|
|
```
|
|
|
|
**New Code:**
|
|
```c
|
|
// Get timestamp delay configuration
|
|
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
|
|
|
int send_result = nostr_nip17_send_dm(
|
|
dm_response, // dm_event
|
|
recipient_pubkeys, // recipient_pubkeys
|
|
1, // num_recipients
|
|
relay_privkey, // sender_private_key
|
|
gift_wraps, // gift_wraps_out
|
|
1, // max_gift_wraps
|
|
max_delay_sec // max_delay_sec
|
|
);
|
|
```
|
|
|
|
#### 4.2 Update src/dm_admin.c
|
|
**Location:** Line 371
|
|
|
|
**Current Code:**
|
|
```c
|
|
int send_result = nostr_nip17_send_dm(
|
|
success_dm, // dm_event
|
|
sender_pubkey_array, // recipient_pubkeys
|
|
1, // num_recipients
|
|
relay_privkey, // sender_private_key
|
|
success_gift_wraps, // gift_wraps_out
|
|
1 // max_gift_wraps
|
|
);
|
|
```
|
|
|
|
**New Code:**
|
|
```c
|
|
// Get timestamp delay configuration
|
|
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
|
|
|
int send_result = nostr_nip17_send_dm(
|
|
success_dm, // dm_event
|
|
sender_pubkey_array, // recipient_pubkeys
|
|
1, // num_recipients
|
|
relay_privkey, // sender_private_key
|
|
success_gift_wraps, // gift_wraps_out
|
|
1, // max_gift_wraps
|
|
max_delay_sec // max_delay_sec
|
|
);
|
|
```
|
|
|
|
**Note:** Both files already include `config.h`, so `get_config_int()` is available.
|
|
|
|
---
|
|
|
|
## Testing Plan
|
|
|
|
### Test Case 1: No Randomization (Compatibility Mode)
|
|
**Configuration:** `nip59_timestamp_max_delay_sec = 0`
|
|
|
|
**Expected Behavior:**
|
|
- Gift wrap timestamps should equal current time
|
|
- Seal timestamps should equal current time
|
|
- No random delay applied
|
|
|
|
**Test Command:**
|
|
```bash
|
|
# Set config via admin API
|
|
# Send test DM
|
|
# Verify timestamps are current (within 1 second of send time)
|
|
```
|
|
|
|
### Test Case 2: Custom Delay
|
|
**Configuration:** `nip59_timestamp_max_delay_sec = 1000`
|
|
|
|
**Expected Behavior:**
|
|
- Gift wrap timestamps should be between now and 1000 seconds ago
|
|
- Seal timestamps should be between now and 1000 seconds ago
|
|
- Random delay applied within specified range
|
|
|
|
**Test Command:**
|
|
```bash
|
|
# Set config via admin API
|
|
# Send test DM
|
|
# Verify timestamps are in past but within 1000 seconds
|
|
```
|
|
|
|
### Test Case 3: Default Behavior
|
|
**Configuration:** `nip59_timestamp_max_delay_sec = 0` (default)
|
|
|
|
**Expected Behavior:**
|
|
- Gift wrap timestamps should equal current time
|
|
- Seal timestamps should equal current time
|
|
- No randomization (maximum compatibility)
|
|
|
|
**Test Command:**
|
|
```bash
|
|
# Use default config
|
|
# Send test DM
|
|
# Verify timestamps are current (within 1 second of send time)
|
|
```
|
|
|
|
### Test Case 4: Configuration Validation
|
|
**Test Invalid Values:**
|
|
- Negative value: Should be rejected
|
|
- Value > 604800: Should be rejected
|
|
- Valid boundary values (0, 604800): Should be accepted
|
|
|
|
### Test Case 5: Interoperability
|
|
**Test with Other Nostr Clients:**
|
|
- Send DM with `max_delay_sec = 0` to clients that don't randomize
|
|
- Send DM with `max_delay_sec = 172800` to clients that do randomize
|
|
- Verify both scenarios work correctly
|
|
|
|
---
|
|
|
|
## Documentation Updates
|
|
|
|
### Update docs/configuration_guide.md
|
|
|
|
Add new section:
|
|
|
|
```markdown
|
|
### NIP-59 Gift Wrap Timestamp Configuration
|
|
|
|
#### nip59_timestamp_max_delay_sec
|
|
- **Type:** Integer
|
|
- **Default:** 0 (no randomization)
|
|
- **Range:** 0 to 604800 (7 days)
|
|
- **Description:** Controls timestamp randomization for NIP-59 gift wraps
|
|
|
|
The NIP-59 protocol recommends randomizing timestamps on gift wraps to prevent
|
|
time-analysis attacks. However, some Nostr platforms don't implement this,
|
|
causing compatibility issues.
|
|
|
|
**Values:**
|
|
- `0` (default): No randomization - uses current timestamp (maximum compatibility)
|
|
- `1-604800`: Random timestamp between now and N seconds ago
|
|
|
|
**Use Cases:**
|
|
- Keep default `0` for maximum compatibility with clients that don't randomize
|
|
- Set to `172800` for privacy per NIP-59 specification (2 days randomization)
|
|
- Set to custom value (e.g., `3600`) for 1-hour randomization window
|
|
|
|
**Example:**
|
|
```json
|
|
["nip59_timestamp_max_delay_sec", "0"] // Default: compatibility mode
|
|
["nip59_timestamp_max_delay_sec", "3600"] // 1 hour randomization
|
|
["nip59_timestamp_max_delay_sec", "172800"] // 2 days randomization
|
|
```
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### nostr_core_lib Changes
|
|
- [ ] Modify `random_past_timestamp()` to accept `max_delay_sec` parameter
|
|
- [ ] Update `nostr_nip59_create_seal()` signature and implementation
|
|
- [ ] Update `nostr_nip59_create_gift_wrap()` signature and implementation
|
|
- [ ] Update `nip059.h` function declarations and documentation
|
|
- [ ] Update `nostr_nip17_send_dm()` signature and implementation
|
|
- [ ] Update `nip017.h` function declaration and documentation
|
|
|
|
### c-relay Changes
|
|
- [ ] Add `nip59_timestamp_max_delay_sec` to `default_config_event.h`
|
|
- [ ] Add validation in `config.c` for new parameter
|
|
- [ ] Update `src/api.c` call site to pass `max_delay_sec`
|
|
- [ ] Update `src/dm_admin.c` call site to pass `max_delay_sec`
|
|
|
|
### Testing
|
|
- [ ] Test with `max_delay_sec = 0` (no randomization)
|
|
- [ ] Test with `max_delay_sec = 1000` (custom delay)
|
|
- [ ] Test with `max_delay_sec = 172800` (default behavior)
|
|
- [ ] Test configuration validation (invalid values)
|
|
- [ ] Test interoperability with other Nostr clients
|
|
|
|
### Documentation
|
|
- [ ] Update `docs/configuration_guide.md`
|
|
- [ ] Add this implementation plan to docs
|
|
- [ ] Update README if needed
|
|
|
|
---
|
|
|
|
## Rollback Plan
|
|
|
|
If issues arise:
|
|
1. Revert nostr_core_lib changes (git revert in submodule)
|
|
2. Revert c-relay changes
|
|
3. Configuration parameter will be ignored if not used
|
|
4. Default behavior (0) provides maximum compatibility
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- The configuration is read on each DM send, allowing runtime changes
|
|
- No restart required when changing `nip59_timestamp_max_delay_sec`
|
|
- Thread-safe by design (no global state)
|
|
- Default value of 0 provides maximum compatibility with other Nostr clients
|
|
- Can be changed to 172800 or other values for NIP-59 privacy features
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [NIP-59: Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md)
|
|
- [NIP-17: Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/17.md)
|
|
- [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md) |