Files
c-relay-pg/plans/web_of_trust_plan.md

13 KiB

Web of Trust (WoT) Implementation Plan

Feature Description

When enabled, the relay restricts access to pubkeys that the admin follows. The admin's kind 3 (contact list) event is used as the source of truth — all p tags in that event become whitelisted pubkeys. The admin themselves is always whitelisted.

Single config variable wot_enabled with three levels:

Value Mode Effect
0 Off Open relay — anyone can read and write
1 Write-only Only followed pubkeys can publish events. Anyone can read/subscribe.
2 Full Only followed pubkeys can publish AND subscribe. Requires NIP-42 auth for subscriptions. Eliminates subscription churn from anonymous connections.

How It Works

WoT Sync Flow

flowchart TD
    A["Admin publishes kind 3 event\n- contact list -"] --> B{wot_enabled > 0?}
    B -->|No| C["Store event normally"]
    B -->|Yes| D["Extract p tags from kind 3"]
    D --> E["Clear existing WoT whitelist rules"]
    E --> F["Insert new whitelist rules\nfor each followed pubkey"]
    F --> G["Enable auth_enabled"]
    G --> H{wot_enabled == 2?}
    H -->|Yes| I["Enable nip42_auth_required_subscriptions"]
    H -->|No| J["Done - write-only restriction"]

Event Publishing Flow — Write Restriction (wot_enabled >= 1)

flowchart TD
    A["EVENT message arrives"] --> B{auth_enabled?}
    B -->|No| C["Accept event"]
    B -->|Yes| D["check_database_auth_rules - pubkey -"]
    D --> E{Pubkey in whitelist\nor wot_whitelist?}
    E -->|Yes| C
    E -->|No| F{Any whitelist rules exist?}
    F -->|Yes| G["REJECT: not whitelisted"]
    F -->|No| C

Subscription Flow — Read Restriction (wot_enabled == 2)

flowchart TD
    A["REQ message arrives"] --> B{wot_enabled == 2?}
    B -->|No| C["Allow subscription"]
    B -->|Yes| D{NIP-42 authenticated?}
    D -->|No| E["Send AUTH challenge"]
    D -->|Yes| F["check_database_auth_rules\nusing authenticated_pubkey"]
    F --> G{Pubkey in whitelist\nor wot_whitelist?}
    G -->|Yes| C
    G -->|No| H["REJECT: not authorized\nfor subscriptions"]

Design Decisions

Leveraging Existing Infrastructure

The relay already has everything needed:

  1. auth_rules table — stores whitelist/blacklist rules with rule_type, pattern_type, pattern_value
  2. check_database_auth_rules() in request_validator.c:529 — already implements the logic: "if whitelist rules exist and pubkey is not whitelisted, deny"
  3. add_auth_rule_from_config() / remove_auth_rule_from_config() in config.c — already manage auth rules
  4. event_tags table — can efficiently query p tags from kind 3 events
  5. Config systemauth_enabled flag already controls whether auth rules are checked
  6. NIP-42 authnip42_auth_required_subscriptions already gates REQ access, pss->authenticated_pubkey stores the authenticated pubkey

WoT-Specific Whitelist Rules

To distinguish WoT-generated whitelist rules from manually-added ones, we use a new rule_type value: wot_whitelist. This allows:

  • Clearing all WoT rules without affecting manual whitelist/blacklist rules
  • Querying WoT status separately
  • The existing check_database_auth_rules() function already checks for rule_type = 'whitelist' — we need to also match wot_whitelist in the whitelist check

Trigger Mechanism

The WoT sync happens when:

  1. A kind 3 event from the admin is stored — detected in store_event() after successful INSERT
  2. Startup — if wot_enabled > 0, sync from the most recent admin kind 3 event in the database
  3. Admin DM commandwot sync to force a manual resync

What Gets Whitelisted

  • All pubkeys in p tags of the admin's kind 3 event
  • The admin pubkey itself (always whitelisted)
  • The relay pubkey (always whitelisted — for admin DM responses)

What Is NOT Blocked

Even with WoT enabled, these are always allowed:

  • Admin events (kind 23456) — already bypassed in request_validator.c:303
  • NIP-42 auth events (kind 22242) — already bypassed in request_validator.c:318
  • Kind 3 events from the admin — needed to update the follow list itself
  • Kind 1059 gift wraps addressed to the relay — needed for admin DMs

Files to Modify

File Changes
src/default_config_event.h Add wot_enabled config key (default: 0)
src/config.c Add wot_sync_from_admin_kind3() function
src/main.c Add WoT trigger in store_event() when admin kind 3 is stored, add startup WoT sync
src/request_validator.c Update whitelist SQL to also match wot_whitelist rule type
src/sql_schema.h Update auth_rules CHECK constraint to include wot_whitelist
src/websockets.c Add WoT pubkey check after NIP-42 auth check in REQ handler
src/dm_admin.c Add wot 0, wot 1, wot 2, wot sync, wot status DM commands

Detailed Changes

1. Schema: src/sql_schema.h

Update the auth_rules table CHECK constraint to allow wot_whitelist:

-- Before:
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required'))

-- After:
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required', 'wot_whitelist'))

2. Config: src/default_config_event.h

Add WoT configuration:

// Web of Trust Settings
// 0 = off, 1 = write-only (followed pubkeys can publish), 2 = full (followed pubkeys can publish AND subscribe)
{"wot_enabled", "0"},

3. Core WoT Sync Function: src/config.c

New function wot_sync_from_admin_kind3():

int wot_sync_from_admin_kind3(void) {
    int wot_level = get_config_int("wot_enabled", 0);
    if (wot_level <= 0) return 0; // WoT disabled

    // 1. Get admin pubkey from config
    // 2. Query event_tags for p tags from admin's latest kind 3 event:
    //    SELECT DISTINCT et.tag_value FROM event_tags et
    //    JOIN events e ON et.event_id = e.id
    //    WHERE e.kind = 3 AND e.pubkey = ? AND et.tag_name = 'p'
    //    ORDER BY e.created_at DESC
    // 3. BEGIN TRANSACTION
    // 4. DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'
    // 5. INSERT wot_whitelist for admin pubkey
    // 6. INSERT wot_whitelist for relay pubkey
    // 7. For each p tag: INSERT INTO auth_rules (rule_type, pattern_type, pattern_value)
    //    VALUES ('wot_whitelist', 'pubkey', ?)
    // 8. COMMIT
    // 9. Set auth_enabled = true
    // 10. If wot_level == 2: set nip42_auth_required_subscriptions = true
    // 11. Log count of whitelisted pubkeys
    return 0;
}

4. Trigger on Kind 3 Store: src/main.c

In store_event(), after successful INSERT and after store_event_tags():

// Check if this is a kind 3 event from the admin — trigger WoT sync
if ((int)cJSON_GetNumberValue(kind) == 3) {
    int wot_level = get_config_int("wot_enabled", 0);
    if (wot_level > 0) {
        const char* admin_pubkey = get_config_value("admin_pubkey");
        if (admin_pubkey && strcmp(cJSON_GetStringValue(pubkey), admin_pubkey) == 0) {
            DEBUG_INFO("Admin kind 3 event stored — triggering WoT sync");
            wot_sync_from_admin_kind3();
        }
        if (admin_pubkey) free((char*)admin_pubkey);
    }
}

5. Startup WoT Sync: src/main.c

In main(), after populate_event_tags_from_existing():

// Sync Web of Trust whitelist if enabled
int wot_level = get_config_int("wot_enabled", 0);
if (wot_level > 0) {
    wot_sync_from_admin_kind3();
}

6. Update Whitelist Check: src/request_validator.c

Update the whitelist SQL queries to also match wot_whitelist:

// Before:
"SELECT rule_type FROM auth_rules WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"

// After:
"SELECT rule_type FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"

And the whitelist-exists check:

// Before:
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"

// After:
"SELECT COUNT(*) FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"

7. REQ Handler WoT Check: src/websockets.c

When wot_enabled == 2, add a pubkey whitelist check after the existing NIP-42 auth check in the REQ handler. The existing code at line 903 already requires NIP-42 auth when nip42_auth_required_subscriptions is set. We add a WoT check right after:

// Existing NIP-42 auth check (line 903):
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
    // ... send AUTH challenge or NOTICE ...
    return 0;
}

// NEW: WoT read restriction check (wot_enabled == 2)
if (pss && pss->authenticated && get_config_int("wot_enabled", 0) == 2) {
    // Client is authenticated — check if their pubkey is in the WoT whitelist
    int wot_result = check_database_auth_rules(pss->authenticated_pubkey, "subscription", NULL);
    if (wot_result != NOSTR_SUCCESS) {
        send_notice_message(wsi, pss, "restricted: your pubkey is not in this relay's web of trust");
        DEBUG_INFO("REQ rejected: pubkey %s not in WoT whitelist", pss->authenticated_pubkey);
        cJSON_Delete(json);
        return 0;
    }
}

8. Admin DM Commands: src/dm_admin.c

Add plain text DM commands:

Command Action
wot 0 or wot off Disable WoT, delete all wot_whitelist rules, reset nip42_auth_required_subscriptions
wot 1 or wot write Write-only WoT — sync follow list, set auth_enabled=true
wot 2 or wot full Full WoT — sync follow list, set auth_enabled=true, set nip42_auth_required_subscriptions=true
wot sync Force resync from admin's kind 3 event
wot status Show WoT level (0/1/2), count of whitelisted pubkeys

9. NIP-11 Update

When WoT is enabled (level 1 or 2), the relay should indicate this in NIP-11 relay info. Add to the limitation object:

"auth_required": true

Edge Cases

  1. Admin has no kind 3 event in database: WoT sync does nothing, logs a warning. No whitelist rules created = open relay.

  2. Admin updates follow list: New kind 3 event triggers full resync — old WoT rules are cleared, new ones inserted. This is atomic (transaction).

  3. WoT disabled (set to 0): Clears all wot_whitelist rules. Manual whitelist rules are preserved. nip42_auth_required_subscriptions is reset to false.

  4. Admin unfollows someone: Next kind 3 event triggers resync, the unfollowed pubkey's wot_whitelist rule is removed (full clear + reinsert).

  5. Kind 1059 gift wraps: These need special handling — the relay needs to accept gift wraps addressed to it even from non-whitelisted pubkeys (for admin DMs). The is_nip17_gift_wrap_for_relay() check should bypass WoT.

  6. Read restriction without NIP-42 support (level 2): If a client doesn't support NIP-42, they cannot authenticate and therefore cannot subscribe. This is by design — it's the most effective way to reduce subscription churn from anonymous connections.

  7. NIP-11 discovery: Clients can check NIP-11 to see if auth_required is true before connecting, avoiding wasted connections.

  8. Changing level from 2 to 1: nip42_auth_required_subscriptions is reset to false, allowing anonymous subscriptions again. WoT whitelist rules remain for write restriction.

Testing Strategy

  1. Enable write-only WoT via DM: wot 1
  2. Verify admin can still publish events
  3. Verify followed pubkeys can publish events
  4. Verify non-followed pubkeys are rejected for EVENT
  5. Verify non-followed pubkeys can still subscribe (REQ)
  6. Enable full WoT via DM: wot 2
  7. Verify unauthenticated clients get AUTH challenge on REQ
  8. Verify authenticated non-followed pubkeys are rejected for REQ
  9. Verify authenticated followed pubkeys can subscribe
  10. Verify wot status shows correct level and count
  11. Publish new kind 3 event, verify auto-resync
  12. wot 0 — verify all pubkeys can publish and subscribe again
  13. Verify admin DMs still work when WoT is enabled

Performance Considerations

  • WoT sync is O(n) where n = number of follows (typically 100-2000)
  • Uses a transaction for bulk insert — fast even for large follow lists
  • Auth rule check is already indexed: idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value)
  • No additional per-event overhead — the existing check_database_auth_rules() already runs on every event when auth is enabled
  • Level 2 directly addresses CPU load: With wot_enabled=2, unauthenticated connections cannot create subscriptions, eliminating the ~120 REQ/minute churn from anonymous connections that was causing 99% CPU