Files
wifi-densepose/examples/edge-net/docs/SECURITY_AUDIT_REPORT.md
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

19 KiB

Edge-Net Relay Security Audit Report

Date: 2026-01-03 Auditor: Code Review Agent Component: Edge-Net WebSocket Relay Server (/relay/index.js) Version: v0.1.0


Executive Summary

A comprehensive security audit was conducted on the Edge-Net relay server, focusing on authentication, authorization, and protection against common attack vectors. The relay implements strong security controls for task assignment and credit distribution, with Firestore-backed QDAG (Quantum Directed Acyclic Graph) ledger as the source of truth.

Overall Security Rating: GOOD (with minor recommendations)

Key Findings

Category Status Details
Task Completion Spoofing SECURE Protected by assignment tracking
Replay Attacks SECURE Protected by completed tasks set
Credit Self-Reporting SECURE Disabled - relay-only crediting
Public Key Spoofing ⚠️ MOSTLY SECURE See recommendations
Rate Limiting IMPLEMENTED Per-node message throttling
Message Size Limits IMPLEMENTED 64KB max payload
Connection Limits IMPLEMENTED 5 connections per IP
Origin Validation IMPLEMENTED CORS whitelist

Security Architecture

1. QDAG Ledger (Source of Truth)

Implementation: Lines 66-142

// Firestore-backed persistent credit ledger
const ledgerCollection = firestore.collection('edge-net-qdag');

// Credits ONLY increase via verified task completions
async function creditAccount(publicKey, amount, taskId) {
  const ledger = await loadLedger(publicKey);
  ledger.earned += amount;
  ledger.tasksCompleted += 1;
  await saveLedger(publicKey, ledger);
}

Security Properties:

  • Credits keyed by public key (identity-based, not node-based)
  • Persistent across sessions (Firestore)
  • Server-side only (clients cannot modify)
  • Atomic operations with in-memory cache

Attack Vector Analysis

🔴 Attack Vector 1: Task Completion Spoofing

Description: Malicious node tries to complete tasks not assigned to them.

Protection Mechanism (Lines 61-64, 222-229, 411-423):

// Track assigned tasks with assignment metadata
const assignedTasks = new Map(); // taskId -> { assignedTo, submitter, maxCredits }

// On task assignment (lines 222-229)
assignedTasks.set(task.id, {
  assignedTo: targetNodeId,
  assignedToPublicKey: targetWs.publicKey,
  submitter: task.submitter,
  maxCredits: task.maxCredits,
  assignedAt: Date.now(),
});

// On task completion (lines 411-423)
const assignment = assignedTasks.get(taskId);
if (!assignment) {
  console.warn(`Task ${taskId} not found or expired`);
  ws.send(JSON.stringify({ type: 'error', message: 'Task not found or expired' }));
  break;
}

if (assignment.assignedTo !== nodeId) {
  console.warn(`Task ${taskId} assigned to ${assignment.assignedTo}, not ${nodeId} - SPOOFING ATTEMPT`);
  ws.send(JSON.stringify({ type: 'error', message: 'Task not assigned to you' }));
  break;
}

Security Assessment: SECURE

  • Assignment tracked with node ID verification
  • Only assigned node can complete task
  • Clear error messages for debugging (but could be more generic for production)

Test Coverage: relay-security.test.ts - "Task Completion Spoofing" suite


🔴 Attack Vector 2: Replay Attacks

Description: Malicious node tries to complete the same task multiple times to earn duplicate credits.

Protection Mechanism (Lines 64, 425-430, 441):

const completedTasks = new Set(); // Prevent double completion

// On task completion (lines 425-430)
if (completedTasks.has(taskId)) {
  console.warn(`Task ${taskId} already completed - REPLAY ATTEMPT from ${nodeId}`);
  ws.send(JSON.stringify({ type: 'error', message: 'Task already completed' }));
  break;
}

// Mark completed BEFORE crediting (line 441)
completedTasks.add(taskId);
assignedTasks.delete(taskId);

Security Assessment: SECURE

  • Tasks marked complete before crediting (prevents race conditions)
  • Permanent replay prevention (Set-based tracking)
  • Assignment deleted after completion

Potential Issue: ⚠️ completedTasks Set grows unbounded

Recommendation: Implement periodic cleanup for old completed tasks:

// Cleanup completed tasks older than 24 hours
setInterval(() => {
  const CLEANUP_AGE = 24 * 60 * 60 * 1000; // 24 hours
  // Track completion timestamps and remove old entries
}, 60 * 60 * 1000); // Every hour

Test Coverage: relay-security.test.ts - "Replay Attacks" suite


🔴 Attack Vector 3: Credit Self-Reporting

Description: Malicious client tries to submit their own credit values to inflate balance.

Protection Mechanism (Lines 612-622):

case 'ledger_update':
  // DEPRECATED: Clients cannot self-report credits
  {
    console.warn(`[QDAG] REJECTED ledger_update from ${nodeId} - clients cannot self-report credits`);
    ws.send(JSON.stringify({
      type: 'error',
      message: 'Credit self-reporting disabled. Credits earned via task completions only.',
    }));
  }
  break;

Security Assessment: SECURE

  • ledger_update messages explicitly rejected
  • Only creditAccount() function can increase earned credits
  • creditAccount() only called after verified task completion (lines 450-451)
  • Firestore ledger is source of truth

Additional Security:

  • ledger_sync is read-only (lines 570-610) - returns balance from Firestore
  • No client-submitted credit values accepted anywhere

Test Coverage: relay-security.test.ts - "Credit Self-Reporting" suite


🔴 Attack Vector 4: Public Key Spoofing

Description: Malicious node tries to use another user's public key to claim their credits or identity.

Protection Mechanism: Lines 360-366, 432-438, 584

Current Implementation:

// On registration (lines 360-366)
if (message.publicKey) {
  ws.publicKey = message.publicKey;
  console.log(`Node registered: ${nodeId} with identity ${message.publicKey.slice(0, 16)}...`);
}

// On task completion (lines 432-438)
const processorPublicKey = assignment.assignedToPublicKey || ws.publicKey;
if (!processorPublicKey) {
  ws.send(JSON.stringify({ type: 'error', message: 'Public key required for credit' }));
  break;
}

// Credits go to the PUBLIC KEY stored in assignment
await creditAccount(processorPublicKey, rewardRuv, taskId);

Security Assessment: ⚠️ MOSTLY SECURE (with caveats)

What IS Secure:

  • Credits keyed by public key (same identity = same balance everywhere)
  • Public key stored at assignment time (prevents mid-flight changes)
  • Credits awarded to assignment.assignedToPublicKey (not current ws.publicKey)
  • Multiple nodes can share public key (multi-device support by design)

What IS NOT Fully Protected:

  • ⚠️ No cryptographic signature verification (Line 281-286)
// CURRENT: Placeholder validation
function validateSignature(nodeId, message, signature, publicKey) {
  // In production, verify Ed25519 signature from PiKey
  // For now, accept if nodeId matches registered node
  return nodes.has(nodeId);
}
  • ⚠️ Clients can claim any public key without proving ownership
  • ⚠️ This allows "read-only spoofing" - checking another user's balance

Impact Assessment:

Scenario Risk Level Impact
Checking another user's balance 🟡 LOW Privacy leak, but no financial impact
Claiming another user's public key at registration 🟡 LOW Cannot steal existing credits (assigned at task time)
Earning credits to someone else's key 🟡 LOW Self-harm (attacker works for victim's benefit)
Completing tasks assigned to victim 🔴 NONE Protected by assignedTo node ID check

Recommendations:

  1. Implement Ed25519 Signature Verification (CRITICAL for production):
import { verify } from '@noble/ed25519';

async function validateSignature(message, signature, publicKey) {
  try {
    const msgHash = createHash('sha256').update(JSON.stringify(message)).digest();
    return await verify(signature, msgHash, publicKey);
  } catch {
    return false;
  }
}

// Require signature on sensitive operations
case 'task_complete':
  if (!message.signature || !validateSignature(message, message.signature, ws.publicKey)) {
    ws.send(JSON.stringify({ type: 'error', message: 'Invalid signature' }));
    break;
  }
  // ... rest of task completion logic
  1. Add Challenge-Response on Registration:
case 'register':
  // Send challenge
  const challenge = randomBytes(32).toString('hex');
  challenges.set(nodeId, challenge);
  ws.send(JSON.stringify({
    type: 'challenge',
    challenge: challenge,
  }));
  break;

case 'challenge_response':
  const challenge = challenges.get(nodeId);
  if (!validateSignature({ challenge }, message.signature, message.publicKey)) {
    ws.close(4003, 'Invalid signature');
    break;
  }
  // Complete registration...
  1. Rate-limit balance queries to prevent enumeration attacks:
case 'ledger_sync':
  if (!checkRateLimit(`${nodeId}-ledger-sync`, 10, 60000)) { // 10 per minute
    ws.send(JSON.stringify({ type: 'error', message: 'Balance query rate limit' }));
    break;
  }

Test Coverage: relay-security.test.ts - "Public Key Spoofing" suite


Additional Security Features

5. Rate Limiting

Implementation: Lines 45-46, 265-279, 346-350

const rateLimits = new Map();
const RATE_LIMIT_MAX = 100; // max messages per window
const RATE_LIMIT_WINDOW = 60000; // 1 minute

function checkRateLimit(nodeId) {
  const now = Date.now();
  const limit = rateLimits.get(nodeId) || { count: 0, windowStart: now };

  if (now - limit.windowStart > RATE_LIMIT_WINDOW) {
    limit.count = 0;
    limit.windowStart = now;
  }

  limit.count++;
  rateLimits.set(nodeId, limit);

  return limit.count <= RATE_LIMIT_MAX;
}

Security Assessment: GOOD

  • Per-node rate limiting (not global)
  • Sliding window implementation
  • Enforced after registration

Recommendations:

  • Consider adaptive rate limits based on node reputation
  • Add separate limits for expensive operations (task_submit, ledger_sync)

6. Message Size Limits

Implementation: Lines 21, 318, 338-342

const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB

ws._maxPayload = MAX_MESSAGE_SIZE;

if (data.length > MAX_MESSAGE_SIZE) {
  ws.send(JSON.stringify({ type: 'error', message: 'Message too large' }));
  return;
}

Security Assessment: GOOD

  • Prevents DoS via large payloads
  • Enforced at both WebSocket and application layer
  • 64KB is reasonable for control messages

7. Connection Limits

Implementation: Lines 22, 308-315, 671-677

const MAX_CONNECTIONS_PER_IP = 5;
const ipConnections = new Map();

// On connection
const ipCount = ipConnections.get(clientIP) || 0;
if (ipCount >= MAX_CONNECTIONS_PER_IP) {
  console.log(`Rejected connection: too many from ${clientIP}`);
  ws.close(4002, 'Too many connections');
  return;
}
ipConnections.set(clientIP, ipCount + 1);

// On close
const currentCount = ipConnections.get(clientIP) || 1;
if (currentCount <= 1) {
  ipConnections.delete(clientIP);
} else {
  ipConnections.set(clientIP, currentCount - 1);
}

Security Assessment: GOOD

  • Prevents connection flooding from single IP
  • Properly tracks connection/disconnection
  • 5 connections is reasonable for multi-device scenarios

Potential Issue: ⚠️ Does not defend against distributed attacks (multiple IPs)

Recommendations:

  • Add global connection limit (e.g., max 1000 total connections)
  • Implement connection rate limiting (max N new connections per minute per IP)

8. Origin Validation

Implementation: Lines 27-37, 255-263, 301-306

const ALLOWED_ORIGINS = new Set([
  'http://localhost:3000',
  'https://edge-net.ruv.io',
  // ... other allowed origins
]);

function isOriginAllowed(origin) {
  if (!origin) return true; // Allow Node.js connections
  if (ALLOWED_ORIGINS.has(origin)) return true;
  if (origin.startsWith('http://localhost:')) return true; // Dev mode
  return false;
}

if (!isOriginAllowed(origin)) {
  ws.close(4001, 'Unauthorized origin');
  return;
}

Security Assessment: GOOD for browser connections

Recommendations:

  • ⚠️ if (!origin) return true allows any non-browser client (CLI, scripts)
  • Consider requiring API key or authentication for non-browser connections:
function isOriginAllowed(origin, headers) {
  if (!origin) {
    // Non-browser connection - require API key
    const apiKey = headers['x-api-key'];
    return apiKey && validateAPIKey(apiKey);
  }
  return ALLOWED_ORIGINS.has(origin) || origin.startsWith('http://localhost:');
}

9. Task Expiration

Implementation: Lines 243-253

// Cleanup old assigned tasks (expire after 5 minutes)
setInterval(() => {
  const now = Date.now();
  const TASK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
  for (const [taskId, task] of assignedTasks) {
    if (now - task.assignedAt > TASK_TIMEOUT) {
      assignedTasks.delete(taskId);
      console.log(`Task ${taskId} expired (not completed in time)`);
    }
  }
}, 60000); // Check every minute

Security Assessment: GOOD

  • Prevents stale task assignments
  • 5-minute timeout is reasonable
  • Automatically cleans up abandoned tasks

Recommendation: ⚠️ Consider re-queuing expired tasks:

if (now - task.assignedAt > TASK_TIMEOUT) {
  assignedTasks.delete(taskId);

  // Re-queue task if original submitter still connected
  if (nodes.has(task.submitter)) {
    taskQueue.push({
      id: taskId,
      submitter: task.submitter,
      // ... task details
    });
    console.log(`Task ${taskId} re-queued after timeout`);
  }
}

10. Heartbeat Timeout

Implementation: Lines 25, 320-329

const CONNECTION_TIMEOUT = 30000; // 30s heartbeat timeout

let heartbeatTimeout;
const resetHeartbeat = () => {
  clearTimeout(heartbeatTimeout);
  heartbeatTimeout = setTimeout(() => {
    console.log(`Node ${nodeId} timed out`);
    ws.terminate();
  }, CONNECTION_TIMEOUT);
};
resetHeartbeat();

// Reset on any message
ws.on('message', async (data) => {
  resetHeartbeat();
  // ...
});

Security Assessment: GOOD

  • Prevents zombie connections
  • 30-second timeout with implicit heartbeat (any message resets)
  • Explicit heartbeat message type also supported (lines 651-657)

Remaining Vulnerabilities

🔴 CRITICAL (Production Blockers)

  1. No Cryptographic Signature Verification
    • Impact: Cannot prove public key ownership
    • Mitigation: Implement Ed25519 signature validation (see recommendations above)
    • Priority: CRITICAL for production

🟡 MEDIUM (Should Address)

  1. Unbounded completedTasks Set Growth

    • Impact: Memory leak over time
    • Mitigation: Implement periodic cleanup of old completed tasks
    • Priority: MEDIUM
  2. No Global Connection Limit

    • Impact: Distributed attack from many IPs could exhaust resources
    • Mitigation: Add global max connection limit
    • Priority: MEDIUM
  3. Permissive Non-Browser Access

    • Impact: Any script can connect without authentication
    • Mitigation: Require API key for non-browser connections
    • Priority: MEDIUM (depends on use case)

🟢 LOW (Nice to Have)

  1. Error Messages Reveal Internal State

    • Impact: Attackers can infer system behavior from detailed errors
    • Mitigation: Use generic error messages in production, detailed in logs
    • Priority: LOW
  2. No Firestore Access Control Validation

    • Impact: Assumes Firestore security rules are correctly configured
    • Mitigation: Document required Firestore security rules
    • Priority: LOW (infrastructure concern)

Firestore Security Rules Required

The relay assumes these Firestore security rules are in place:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /edge-net-qdag/{publicKey} {
      // Only server (via Admin SDK) can write
      allow read: if false;  // Not public
      allow write: if false; // Server-only
    }
  }
}

Validation: ⚠️ Not tested in this audit - infrastructure team should verify.


Test Coverage Summary

Created comprehensive security test suite: /tests/relay-security.test.ts

Test Suites:

  1. Task Completion Spoofing (2 tests)
  2. Replay Attacks (1 test)
  3. Credit Self-Reporting (2 tests)
  4. Public Key Spoofing (2 tests)
  5. Rate Limiting (1 test)
  6. Message Size Limits (1 test)
  7. Connection Limits (1 test)
  8. Task Expiration (1 test)
  9. Combined Attack Scenario (1 test)

Total: 12 security tests

To Run Tests:

cd /workspaces/ruvector/examples/edge-net
npm install --save-dev @jest/globals ws @types/ws
npm test tests/relay-security.test.ts

Recommendations Summary

Immediate (Before Production)

  1. Implement Ed25519 signature verification for public key ownership
  2. Add challenge-response on node registration
  3. Implement completedTasks cleanup to prevent memory leak

Short-term (1-2 weeks)

  1. Add global connection limit (e.g., 1000 max total)
  2. Require API keys for non-browser connections
  3. Add separate rate limits for expensive operations
  4. Rate-limit balance queries to prevent enumeration

Long-term (1-3 months)

  1. Implement reputation-based rate limiting
  2. Add connection rate limiting (per IP)
  3. Use generic error messages in production
  4. Document Firestore security rules and validate configuration
  5. Add metrics and monitoring for attack detection

Conclusion

The Edge-Net relay server demonstrates strong security fundamentals with excellent protection against:

  • Task completion spoofing
  • Replay attacks
  • Credit self-reporting
  • Basic DoS attacks

The QDAG ledger architecture is well-designed with Firestore as source of truth and server-side-only crediting.

Primary concern: Lack of cryptographic signature verification means public key ownership is not proven. This is acceptable for testing/development but MUST be implemented before production deployment.

Overall Assessment: System is secure for testing/development. Implement critical recommendations before production.


Audit Completed: 2026-01-03 Next Review: After signature verification implementation