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
- Session Creation: Client calls
POST /bot-defense/sessions(authenticated) to get a session ID and nonce - Liveness Verification: Client performs VibeCheck SDK verification
- Proof Generation: Client computes HMAC signature of the result
- Submission: Client sends result with signature to
POST /bot-defense/sessions/:sessionId/verify - 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:
-
Check payload format:
// MUST be exactly: "nonce:isLive:confidence:timestamp" const payload = `${nonce}:${isLive}:${confidence.toFixed(6)}:${timestamp}`; -
Verify timestamp format:
const timestamp = new Date().toISOString(); // "2026-02-06T12:00:00.000Z" -
Check confidence decimals:
const confidence = 0.856789; const confidenceFixed = confidence.toFixed(6); // "0.856789" -
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
- Use HTTPS: Prevent token/session interception
- Rate limiting: Limit verification attempts per user/IP
- Monitoring: Alert on high integrity violation rates
- Session TTL: Keep sessions short-lived (5-10 minutes)
- User education: Explain why liveness checks are required
- 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)