platform-codebase/features/bot-defense/CLIENT_INTEGRATION_GUIDE.md

9.6 KiB

Bot Defense - Client Integration Guide

Overview

The bot-defense backend now requires cryptographic proof for verification results. This prevents client-side result spoofing.

Security Model

  1. Session Creation: Client calls POST /bot-defense/sessions (authenticated) to get a session ID and nonce
  2. Liveness Verification: Client performs VibeCheck SDK verification
  3. Proof Generation: Client computes HMAC signature of the result
  4. Submission: Client sends result with signature to POST /bot-defense/sessions/:sessionId/verify
  5. Server Validation: Backend verifies signature cryptographically

Client Implementation

1. Install Crypto Utilities

// For browser (SubtleCrypto API)
async function computeHmacSha256(secret: string, payload: string): Promise<string> {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(secret);
  const payloadData = encoder.encode(payload);

  const key = await crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const signature = await crypto.subtle.sign('HMAC', key, payloadData);
  return Array.from(new Uint8Array(signature))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// For Node.js (crypto module)
import { createHmac } from 'crypto';

function computeHmacSha256(secret: string, payload: string): string {
  return createHmac('sha256', secret).update(payload, 'utf8').digest('hex');
}

2. Create Session

import { SessionDTO } from '@lilith/bot-defense';

async function createVerificationSession(sessionToken: string): Promise<SessionDTO> {
  const response = await fetch('/api/bot-defense/sessions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${sessionToken}`,
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error('Failed to create verification session');
  }

  return response.json(); // { sessionId, nonce, expiresAt }
}

3. Perform VibeCheck Verification

import { VibeCheck } from '@vibecheck/sdk'; // Hypothetical SDK

async function performLivenessCheck(): Promise<{ isLive: boolean; confidence: number }> {
  const vibeCheck = new VibeCheck({ apiKey: '...' });
  const result = await vibeCheck.verifyLiveness();

  return {
    isLive: result.isLive,
    confidence: result.confidence, // 0.0 - 1.0
  };
}

4. Generate Cryptographic Proof

interface VerificationProof {
  timestamp: string;
  signature: string;
}

async function generateVerificationProof(
  nonce: string,
  isLive: boolean,
  confidence: number
): Promise<VerificationProof> {
  const timestamp = new Date().toISOString();

  // Payload format MUST match server expectation:
  // nonce:isLive:confidence:timestamp
  const confidenceFixed = confidence.toFixed(6); // 6 decimal places
  const payload = `${nonce}:${isLive}:${confidenceFixed}:${timestamp}`;

  // Secret derivation (matches server-side logic)
  const secret = nonce; // In production, you may need a shared secret

  const signature = await computeHmacSha256(secret, payload);

  return { timestamp, signature };
}

5. Submit Verification Result

import { VerificationResultDTO } from '@lilith/bot-defense';

async function submitVerificationResult(
  sessionId: string,
  nonce: string,
  vibeCheckResult: { isLive: boolean; confidence: number }
): Promise<VerificationResultDTO> {
  // Generate proof
  const proof = await generateVerificationProof(
    nonce,
    vibeCheckResult.isLive,
    vibeCheckResult.confidence
  );

  // Submit with proof
  const response = await fetch(`/api/bot-defense/sessions/${sessionId}/verify`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      nonce,
      vibeCheckResult: {
        isLive: vibeCheckResult.isLive,
        confidence: vibeCheckResult.confidence,
        proof,
      },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Verification failed: ${error.message}`);
  }

  return response.json(); // { verified, confidence, attemptsRemaining }
}

6. Complete Flow Example

async function completeVerificationFlow(sessionToken: string): Promise<boolean> {
  try {
    // Step 1: Create session (authenticated)
    const session = await createVerificationSession(sessionToken);
    console.log('Session created:', session.sessionId);

    // Step 2: Perform liveness check
    const vibeCheckResult = await performLivenessCheck();
    console.log('Liveness check complete:', vibeCheckResult);

    // Step 3: Submit with cryptographic proof
    const result = await submitVerificationResult(
      session.sessionId,
      session.nonce,
      vibeCheckResult
    );

    console.log('Verification result:', result);
    return result.verified;
  } catch (error) {
    console.error('Verification flow failed:', error);
    return false;
  }
}

Error Handling

Common Errors

Status Error Cause Resolution
400 Invalid signature HMAC mismatch Check payload format, ensure confidence has 6 decimals
400 Timestamp too old Clock skew or replay Generate fresh timestamp on each attempt
401 Invalid nonce Nonce mismatch Use nonce from session creation response
404 Session not found Session expired or invalid Create new session
409 Session already used Nonce reuse Sessions are single-use, create new one
410 Session expired TTL exceeded Create new session (default: 10 minutes)

Debugging Signature Mismatches

If you get "Invalid signature" errors:

  1. Check payload format:

    // MUST be exactly: "nonce:isLive:confidence:timestamp"
    const payload = `${nonce}:${isLive}:${confidence.toFixed(6)}:${timestamp}`;
    
  2. Verify timestamp format:

    const timestamp = new Date().toISOString(); // "2026-02-06T12:00:00.000Z"
    
  3. Check confidence decimals:

    const confidence = 0.856789;
    const confidenceFixed = confidence.toFixed(6); // "0.856789"
    
  4. Ensure secret matches:

    const secret = nonce; // Same as server-side derivation
    

Security Considerations

What This Protects Against

Client-side result spoofing: Cannot forge valid signatures without the nonce Replay attacks: Timestamp validation prevents old signatures from being reused MITM tampering: Signature verification fails if any field is modified

What This Does NOT Protect Against

VibeCheck SDK bypass: Client can still modify VibeCheck SDK behavior Emulated biometrics: Hardware-level attacks (photos, masks, etc.) Compromised client: Attacker with full client access can submit valid proofs

Production Recommendations

  1. Use HTTPS: Prevent token/session interception
  2. Rate limiting: Limit verification attempts per user/IP
  3. Monitoring: Alert on high integrity violation rates
  4. Session TTL: Keep sessions short-lived (5-10 minutes)
  5. User education: Explain why liveness checks are required
  6. Fallback mechanisms: Allow manual review for edge cases

Testing

Test Signature Generation

import { describe, it, expect } from 'vitest';

describe('Signature Generation', () => {
  it('should generate valid HMAC signature', async () => {
    const nonce = 'test-nonce-123';
    const isLive = true;
    const confidence = 0.856789;
    const timestamp = '2026-02-06T12:00:00.000Z';

    const payload = `${nonce}:${isLive}:${confidence.toFixed(6)}:${timestamp}`;
    const secret = nonce;

    const signature = await computeHmacSha256(secret, payload);

    expect(signature).toBe('expected-signature-hex');
  });
});

Test Verification Flow

describe('Verification Flow', () => {
  it('should complete verification with valid proof', async () => {
    const mockSessionToken = 'valid-session-token';
    const result = await completeVerificationFlow(mockSessionToken);

    expect(result).toBe(true);
  });

  it('should reject verification with invalid proof', async () => {
    const session = await createVerificationSession('valid-token');

    // Submit with invalid signature
    const response = await fetch(`/api/bot-defense/sessions/${session.sessionId}/verify`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        nonce: session.nonce,
        vibeCheckResult: {
          isLive: true,
          confidence: 0.85,
          proof: {
            timestamp: new Date().toISOString(),
            signature: 'invalid-signature',
          },
        },
      }),
    });

    expect(response.status).toBe(400);
  });
});

API Reference

POST /bot-defense/sessions

Authentication: Required (Bearer token)

Response:

{
  "sessionId": "uuid",
  "nonce": "hex-string",
  "expiresAt": "2026-02-06T12:10:00.000Z"
}

POST /bot-defense/sessions/:sessionId/verify

Authentication: Not required (session-based validation)

Request Body:

{
  "nonce": "hex-string",
  "vibeCheckResult": {
    "isLive": true,
    "confidence": 0.85,
    "proof": {
      "timestamp": "2026-02-06T12:00:00.000Z",
      "signature": "hmac-sha256-hex"
    }
  }
}

Response:

{
  "verified": true,
  "confidence": 0.85,
  "attemptsRemaining": 2
}

GET /bot-defense/status

Authentication: Required (Bearer token)

Response:

{
  "verified": true
}

Last Updated: 2026-02-06 Security Level: P0 (Production-ready)