A community based topic aggregation platform built on atproto

Phone Verification Implementation Guide#

Overview#

This document outlines the complete implementation of phone verification for Coves using the hybrid approach: privacy-first storage with cryptographically signed, portable verification badges.

Architecture#

Data Flow#

1. User requests verification (mobile app)
   ↓
2. Coves AppView validates + sends OTP via Telnyx
   ↓
3. User enters OTP code
   ↓
4. AppView validates code + creates signed verification
   ↓
5. AppView writes verification to user's PDS profile
   ↓
6. AppView stores phone_hash in local database
   ↓
7. Third-party clients see verification badge (portable)

Privacy Model#

  • Never stored: Plain phone numbers (anywhere)
  • Hashed in AppView DB: HMAC-SHA256(phone, pepper) for duplicate detection
  • Stored on PDS: Signed verification badge (no phone data)
  • Portable: Badge works across AppViews, can't be forged

Components Implemented#

1. Lexicon Updates#

  • actor/profile.json: Added verifications array
  • verification/requestPhone.json: XRPC endpoint for OTP request
  • verification/verifyPhone.json: XRPC endpoint for OTP validation
  • verification/getStatus.json: XRPC endpoint for verification status

2. Database Schema (005_create_phone_verification_tables.sql)#

phone_verifications              -- Completed verifications
phone_verification_requests      -- Pending OTP requests (10min TTL)
phone_verification_rate_limits   -- Rate limit tracking
phone_verification_audit_log     -- Security audit trail

3. did:web DID for Coves#

  • Location: .well-known/did.json
  • DID: did:web:coves.social
  • Purpose: Signs verification badges
  • Public Key: P-256 EC key (JWK format)

4. Core Service Layer#

  • interfaces.go: Service contracts
  • service.go: Verification logic
  • errors.go: Domain errors

5. SMS Provider (Telnyx)#

  • telnyx/client.go: API integration
  • Why Telnyx: 50% cheaper, owned infrastructure, international support

Environment Configuration#

Required Variables (.env)#

# DID Configuration
VERIFICATION_SERVICE_DID=did:web:coves.social
VERIFICATION_PRIVATE_KEY=<base64-encoded-P256-private-key>

# Telnyx Configuration
TELNYX_API_KEY=<your-api-key>
TELNYX_MESSAGING_PROFILE_ID=<your-profile-id>
TELNYX_FROM_NUMBER=<e164-phone-number>

# Security
PHONE_HASH_PEPPER=<base64-32-bytes>  # NEVER change after initial setup!

Setup Steps#

1. Generate DID Keypair#

# Generate P-256 EC private key
openssl ecparam -name prime256v1 -genkey -noout -out verification-key.pem

# Extract public key
openssl ec -in verification-key.pem -pubout -out verification-key-pub.pem

# Convert to JWK format (use library or online tool for dev)
# Update .well-known/did.json with x, y coordinates

# Store private key in environment
export VERIFICATION_PRIVATE_KEY="$(cat verification-key.pem | base64 -w 0)"

2. Generate Phone Hash Pepper#

export PHONE_HASH_PEPPER="$(openssl rand -base64 32)"

⚠️ CRITICAL: Never change PHONE_HASH_PEPPER after production launch!

3. Configure Telnyx#

  1. Sign up at https://telnyx.com
  2. Create a Messaging Profile
  3. Purchase a phone number
  4. Get API key from dashboard
  5. Add to .env

4. Serve DID Document#

Ensure https://coves.social/.well-known/did.json is publicly accessible with:

Content-Type: application/json
Access-Control-Allow-Origin: *

Implementation Checklist#

Backend (Go)#

  • Run migration: 005_create_phone_verification_tables.sql
  • Implement PhoneHashProvider (HMAC-SHA256 with pepper)
  • Implement SignatureService (ECDSA P-256 signing)
  • Implement PDSWriter (write to user's PDS via OAuth)
  • Implement VerificationRepository (PostgreSQL)
  • Create XRPC handlers for verification endpoints
  • Add verification routes to main.go
  • Add background cleanup job (expired requests)

Frontend (Mobile)#

  • Add phone input screen with E.164 validation
  • Add OTP entry screen (6-digit code)
  • Call /xrpc/social.coves.verification.requestPhone
  • Call /xrpc/social.coves.verification.verifyPhone
  • Display verification badge on profiles
  • Handle re-verification flow (annual)

Testing#

  • Unit tests for service layer
  • Integration tests for XRPC endpoints
  • Test rate limiting (per phone, per DID)
  • Test signature verification (third-party client)
  • Test PDS write-back
  • Test phone hash collision detection

Security Audit#

  • Verify OTP uses crypto/rand
  • Verify constant-time code comparison (bcrypt)
  • Verify rate limits are enforced
  • Verify phone numbers never logged in plaintext
  • Verify audit logs don't leak sensitive data
  • Verify signatures can't be forged
  • Test SMS provider failover/retry logic

Security Considerations#

Rate Limits#

  • Per Phone: 3 requests/hour (prevents SMS spam to victim)
  • Per DID: 5 requests/day (prevents user abuse)

OTP Security#

  • Length: 6 digits (1M combinations)
  • Expiry: 10 minutes
  • Max Attempts: 3 (then must request new code)
  • Storage: Bcrypt hashed (not reversible)

Phone Hash Security#

  • Algorithm: HMAC-SHA256(phone, pepper)
  • Pepper: 32-byte secret, environment variable
  • Purpose: Prevent rainbow table attacks
  • Uniqueness: One phone = one verified account

Signature Security#

  • Algorithm: ECDSA P-256 (ES256)
  • Payload: type + verifiedBy + verifiedAt + expiresAt + subjectDID
  • Verification: Third-party clients fetch public key from DID document

Annual Re-verification Flow#

  1. 30 days before expiry: Set needsRenewal: true in status API
  2. Show banner in app: "Your verification expires soon, renew now"
  3. User re-verifies: Same flow, new phone allowed
  4. Old verification: Expires, badge removed from profile

Phone Loss/Change Flow#

  1. User reports "lost phone" in app
  2. AppView removes verification from PDS profile
  3. AppView deletes phone_hash from database
  4. User verifies new phone number
  5. Badge restored with new phone_hash

Third-Party Client Integration#

Verifying Badges#

// 1. Fetch user profile from PDS
const profileOwnerDID = 'did:plc:abc123'  // The DID whose profile you're viewing
const profile = await fetchProfileFromPDS(profileOwnerDID)

// 2. Check for phone verification
const phoneVerification = profile.verifications?.find(v => v.type === 'phone')
if (!phoneVerification) {
  // No phone verification
  return false
}

// 3. Fetch Coves DID document
const didDoc = await fetch('https://coves.social/.well-known/did.json').then(r => r.json())
const publicKey = didDoc.verificationMethod[0].publicKeyJwk

// 4. Verify signature (CRITICAL: includes profileOwnerDID to prevent copying)
const payload = phoneVerification.type +
                phoneVerification.verifiedBy +
                phoneVerification.verifiedAt +
                phoneVerification.expiresAt +
                profileOwnerDID  // ← This binds verification to this specific user

const isValid = await verifySignature(payload, phoneVerification.signature, publicKey)

// 5. Check expiry
const expired = new Date(phoneVerification.expiresAt) < new Date()

return isValid && !expired

// If Alice tries to copy this verification to her profile, the signature verification
// will fail because her DID is different from the original subject DID in the payload

Monitoring & Observability#

Key Metrics#

  • SMS delivery rate (should be >99%)
  • Verification completion rate (request → verify)
  • Rate limit hit rate (should be low)
  • Average time to verify
  • Cost per verification

Alerts#

  • SMS delivery failures spike
  • Unusual rate limit hits (possible attack)
  • Signature validation failures (bug or attack)
  • PDS write failures

Audit Log Queries#

-- Failed verification attempts by DID
SELECT did, COUNT(*) as failures
FROM phone_verification_audit_log
WHERE event_type = 'verification_failed'
  AND created_at > NOW() - INTERVAL '24 hours'
GROUP BY did
HAVING COUNT(*) > 5;

-- Rate limit hits (possible attack)
SELECT phone_hash, COUNT(*) as hits
FROM phone_verification_audit_log
WHERE event_type = 'rate_limit_hit'
  AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY phone_hash
ORDER BY hits DESC
LIMIT 10;

Cost Estimation#

Telnyx Pricing (US)#

  • SMS: $0.004/message
  • Monthly estimate: 10,000 verifications = $40/month
  • Annual re-verification: Ongoing cost, plan accordingly

Comparison#

  • Twilio: 0.0079 / m e s s a g e = 0.0079/message = n Implementation Guide
  • AWS SNS: 0.0064 / m e s s a g e = 0.0064/message = n Implementation Guide

Future Enhancements#

v1.1 - Email Verification#

  • Same architecture, different type: "email"
  • Reuse signature service
  • Add email provider (e.g., AWS SES)

v1.2 - Domain Verification#

  • Prove ownership of domain
  • type: "domain"
  • DNS TXT record validation

v1.3 - Government ID#

  • KYC provider integration
  • type: "government_id"
  • Higher trust level

Support#

For questions or issues: