Skip to content

Latest commit

 

History

History
477 lines (437 loc) · 30.3 KB

File metadata and controls

477 lines (437 loc) · 30.3 KB

Webhook Security Flow Diagram

Request Processing Pipeline

┌─────────────────────────────────────────────────────────────────────┐
│                    Incoming Webhook Request                          │
│                                                                       │
│  POST /webhooks/stellarsplit                                         │
│  Headers:                                                            │
│    - x-stellarsplit-signature: <hmac-sha256-hex>                    │
│    - x-stellarsplit-timestamp: <unix-timestamp>                     │
│    - x-stellarsplit-nonce: <unique-id>                              │
│  Body: { event, timestamp, nonce, data }                            │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│              Step 1: Extract & Validate Headers                      │
│                                                                       │
│  ✓ Check x-stellarsplit-signature exists                            │
│  ✓ Check x-stellarsplit-timestamp exists                            │
│  ✓ Check x-stellarsplit-nonce exists                                │
│                                                                       │
│  ❌ Missing → MissingHeaderError (400)                               │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│              Step 2: Validate Timestamp                              │
│                                                                       │
│  now = Math.floor(Date.now() / 1000)                                │
│  timeDiff = Math.abs(now - timestamp)                               │
│                                                                       │
│  if (timeDiff > toleranceSeconds) {                                 │
│    ❌ TimestampOutOfBoundsError (400)                                │
│  }                                                                   │
│                                                                       │
│  ✓ Timestamp within tolerance window                                │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│              Step 3: Check Nonce (Replay Prevention)                │
│                                                                       │
│  if (nonceCache.has(nonce)) {                                       │
│    ❌ ReplayAttackError (400)                                        │
│  }                                                                   │
│                                                                       │
│  ✓ Nonce is unique (not seen before)                                │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│              Step 4: Extract Raw Body                                │
│                                                                       │
│  if (Buffer.isBuffer(req.body)) {                                   │
│    rawBody = req.body.toString('utf8')                              │
│  } else if (typeof req.body === 'string') {                         │
│    rawBody = req.body                                               │
│  } else {                                                            │
│    rawBody = JSON.stringify(req.body)                               │
│  }                                                                   │
│                                                                       │
│  ✓ Raw body extracted for signature verification                    │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│         Step 5: Compute Expected HMAC-SHA256 Signature              │
│                                                                       │
│  key = importKey(secret)                                            │
│  expectedSignature = HMAC-SHA256(key, rawBody)                      │
│                                                                       │
│  ✓ Cryptographic signature computed                                 │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│         Step 6: Constant-Time Signature Comparison                  │
│                                                                       │
│  expectedBytes = hexToBytes(expectedSignature)                      │
│  providedBytes = hexToBytes(signature)                              │
│                                                                       │
│  diff = 0                                                            │
│  for (i = 0; i < length; i++) {                                     │
│    diff |= expectedBytes[i] ^ providedBytes[i]                      │
│  }                                                                   │
│                                                                       │
│  if (diff !== 0) {                                                  │
│    ❌ InvalidSignatureError (400)                                    │
│  }                                                                   │
│                                                                       │
│  ✓ Signature verified (constant-time)                               │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│              Step 7: Parse & Validate Payload                        │
│                                                                       │
│  payload = JSON.parse(rawBody)                                      │
│                                                                       │
│  ✓ Check payload.event is valid                                     │
│  ✓ Check payload.timestamp matches header                           │
│  ✓ Check payload.nonce matches header                               │
│  ✓ Check payload.data exists                                        │
│                                                                       │
│  ❌ Invalid → InvalidPayloadError (400)                              │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│         Step 8: Store Nonce (Prevent Future Replay)                 │
│                                                                       │
│  nonceCache.set(nonce, timestamp)                                   │
│                                                                       │
│  If cache full:                                                      │
│    - Evict oldest nonce (LRU)                                       │
│    - Add new nonce                                                   │
│                                                                       │
│  ✓ Nonce stored in cache                                            │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│         Step 9: Attach Verified Payload to Request                  │
│                                                                       │
│  req.webhookPayload = payload                                       │
│  req.rawWebhookBody = rawBody                                       │
│                                                                       │
│  ✓ Request augmented with verified data                             │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                   Step 10: Pass to Handler                           │
│                                                                       │
│  next()  // Call next middleware                                    │
│                                                                       │
│  ✓ Webhook verified and ready for processing                        │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    User Handler Function                             │
│                                                                       │
│  const { event, data } = req.webhookPayload                         │
│                                                                       │
│  switch (event) {                                                    │
│    case 'invoice.paid':                                             │
│      handleInvoicePaid(data)                                        │
│    case 'invoice.released':                                         │
│      handleInvoiceReleased(data)                                    │
│    // ... other events                                              │
│  }                                                                   │
│                                                                       │
│  res.status(200).json({ received: true })                           │
└─────────────────────────────────────────────────────────────────────┘

Security Layers

┌─────────────────────────────────────────────────────────────────────┐
│  Layer 1: Transport Security (HTTPS/TLS)                            │
│  ─────────────────────────────────────────────────────────────────  │
│  Encryption in transit, certificate validation                      │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Layer 2: HMAC-SHA256 Signature Verification                        │
│  ─────────────────────────────────────────────────────────────────  │
│  Cryptographic proof of authenticity and integrity                  │
│  - Prevents payload tampering                                       │
│  - Requires shared secret                                           │
│  - 256-bit security strength                                        │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Layer 3: Constant-Time Comparison                                  │
│  ─────────────────────────────────────────────────────────────────  │
│  Timing attack mitigation                                           │
│  - No early returns on mismatch                                     │
│  - Bitwise XOR accumulation                                         │
│  - Comparison time independent of differences                       │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Layer 4: Timestamp Validation                                      │
│  ─────────────────────────────────────────────────────────────────  │
│  Time-based freshness check                                         │
│  - Rejects old requests                                             │
│  - Configurable tolerance (default: 5 min)                          │
│  - Handles clock skew                                               │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Layer 5: Nonce-Based Replay Prevention                             │
│  ─────────────────────────────────────────────────────────────────  │
│  Request deduplication                                              │
│  - LRU cache tracks seen nonces                                     │
│  - O(1) lookup and insertion                                        │
│  - Automatic eviction of oldest                                     │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Layer 6: Input Validation & Sanitization                           │
│  ─────────────────────────────────────────────────────────────────  │
│  Schema and type validation                                         │
│  - Required fields check                                            │
│  - Event type validation                                            │
│  - Data structure verification                                      │
│  - Header-payload consistency                                       │
└─────────────────────────────────────────────────────────────────────┘

LRU Cache Operation

Initial State (empty, capacity=3):
┌─────────┬─────────┬─────────┐
│ Empty   │ Empty   │ Empty   │
└─────────┴─────────┴─────────┘

After nonce_1:
┌─────────┬─────────┬─────────┐
│ nonce_1 │ Empty   │ Empty   │
└─────────┴─────────┴─────────┘
  ↑ Oldest           Newest →

After nonce_2:
┌─────────┬─────────┬─────────┐
│ nonce_1 │ nonce_2 │ Empty   │
└─────────┴─────────┴─────────┘
  ↑ Oldest           Newest →

After nonce_3:
┌─────────┬─────────┬─────────┐
│ nonce_1 │ nonce_2 │ nonce_3 │
└─────────┴─────────┴─────────┘
  ↑ Oldest           Newest →

After nonce_4 (cache full, evict oldest):
┌─────────┬─────────┬─────────┐
│ nonce_2 │ nonce_3 │ nonce_4 │
└─────────┴─────────┴─────────┘
  ↑ Oldest           Newest →
  (nonce_1 evicted)

Replay Attempt with nonce_3:
┌─────────┬─────────┬─────────┐
│ nonce_2 │ nonce_3 │ nonce_4 │
└─────────┴─────────┴─────────┘
            ↑ Found!
  ❌ ReplayAttackError thrown

Replay Attempt with nonce_1:
┌─────────┬─────────┬─────────┐
│ nonce_2 │ nonce_3 │ nonce_4 │
└─────────┴─────────┴─────────┘
  Not found (was evicted)
  ✓ Allowed (outside window)

Timing Attack Mitigation

Vulnerable Approach (Early Return)

// ❌ VULNERABLE: Returns early on first mismatch
function vulnerableCompare(a: Uint8Array, b: Uint8Array): boolean {
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      return false;  // ❌ Early return leaks timing info
    }
  }
  return true;
}

Attack: Attacker can measure response time to determine where bytes differ.

Secure Approach (Constant-Time)

// ✓ SECURE: Always processes all bytes
function constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean {
  let diff = 0;
  for (let i = 0; i < a.length; i++) {
    diff |= (a[i] ?? 0) ^ (b[i] ?? 0);  // ✓ Always processes all bytes
  }
  return diff === 0;
}

Protection: Comparison time is always O(n), independent of where differences occur.

HMAC-SHA256 Signature Flow

                    Sender Side
┌──────────────────────────────────────────┐
│                                          │
│  payload = {                             │
│    event: "invoice.paid",                │
│    timestamp: 1721318400,                │
│    nonce: "uuid-v4",                     │
│    data: { ... }                         │
│  }                                       │
│                                          │
│  rawPayload = JSON.stringify(payload)    │
│                                          │
│  signature = HMAC-SHA256(secret, rawPayload)
│                                          │
│  headers = {                             │
│    "x-stellarsplit-signature": signature,│
│    "x-stellarsplit-timestamp": timestamp,│
│    "x-stellarsplit-nonce": nonce         │
│  }                                       │
│                                          │
│  POST /webhook with headers & body       │
│                                          │
└──────────────┬───────────────────────────┘
               │
               │  HTTPS Request
               │
               ▼
┌──────────────────────────────────────────┐
│              Receiver Side                │
│                                          │
│  1. Extract rawPayload from request      │
│  2. Extract signature from header        │
│  3. Compute expectedSig = HMAC-SHA256(secret, rawPayload)
│  4. Compare expectedSig with signature   │
│     (constant-time)                      │
│                                          │
│  if (match) {                            │
│    ✓ Signature valid                     │
│    ✓ Payload authentic                   │
│    ✓ Not tampered                        │
│  } else {                                │
│    ❌ Signature invalid                   │
│  }                                       │
└──────────────────────────────────────────┘

Error Response Flow

┌─────────────────────┐
│ Validation Error    │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────────────────────────┐
│  Determine Error Type                   │
├─────────────────────────────────────────┤
│  • MissingHeaderError                   │
│  • InvalidSignatureError                │
│  • TimestampOutOfBoundsError            │
│  • ReplayAttackError                    │
│  • InvalidPayloadError                  │
└──────────┬──────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────┐
│  Format Error Response                  │
├─────────────────────────────────────────┤
│  {                                      │
│    "error": "InvalidSignatureError",    │
│    "message": "Invalid webhook...",     │
│    "code": "VALIDATION_ERROR"           │
│  }                                      │
└──────────┬──────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────┐
│  Return HTTP 400 Bad Request            │
└─────────────────────────────────────────┘

Configuration Options Impact

toleranceSeconds

Timeline:
                    Request Timestamp
                          ↓
Past ◄──────────────────[●]──────────────────► Future
      ↑                                   ↑
   -toleranceSeconds              +toleranceSeconds
      
      ◄───────── Acceptance Window ─────────►
      
✓ Inside window: Accept
❌ Outside window: TimestampOutOfBoundsError

nonceWindowSize

Cache Size = 3:

Request Stream:   A  →  B  →  C  →  D  →  E  →  A
Cache State:     [A] → [AB] → [ABC] → [BCD] → [CDE] → [CDEA]
                                       ↑ A evicted    ↑ A re-added

Replay Tests:
- Replay B after D: ✓ Still in cache → ❌ Rejected
- Replay A after D: ✓ Not in cache → ✓ Accepted (evicted)
- Replay E after E: ✓ In cache → ❌ Rejected

Performance Characteristics

Request Processing Time

Component              Time (μs)    Notes
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Header Extraction         ~1       Constant
Timestamp Check           ~1       Arithmetic
Nonce Lookup              ~5       Hash map O(1)
HMAC-SHA256             ~50       Crypto operation
Constant-Time Compare    ~10       Array iteration
Payload Parse            ~20       JSON.parse()
Validation               ~5       Type checks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total                   ~92       ~0.1ms per request

Throughput: ~10,000 requests/second

Memory Usage

Component              Memory      Scaling
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Middleware Closure       ~1 KB     Fixed
LRU Cache (1000)        ~50 KB    O(nonceWindowSize)
Request Buffer        Variable    O(payload size)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Per-Request Overhead    ~0.1 KB   Temporary objects

High-Volume Scenario (10,000 nonces):
LRU Cache: ~500 KB (still very efficient)

Quick Reference

Successful Verification

Request → Headers OK → Timestamp OK → Nonce Unique 
  → Signature Valid → Payload Valid → Handler Called

Common Rejection Scenarios

Missing Header → MissingHeaderError (400)
Old Timestamp → TimestampOutOfBoundsError (400)
Duplicate Nonce → ReplayAttackError (400)
Bad Signature → InvalidSignatureError (400)
Malformed Payload → InvalidPayloadError (400)

Security Guarantees

✓ Payload Authenticity (via HMAC-SHA256)
✓ Payload Integrity (via HMAC-SHA256)
✓ Request Freshness (via timestamp)
✓ No Replay Attacks (via nonce cache)
✓ No Timing Attacks (via constant-time compare)