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
verificationsarray - ✅ 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#
- Sign up at https://telnyx.com
- Create a Messaging Profile
- Purchase a phone number
- Get API key from dashboard
- 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#
- 30 days before expiry: Set
needsRenewal: truein status API - Show banner in app: "Your verification expires soon, renew now"
- User re-verifies: Same flow, new phone allowed
- Old verification: Expires, badge removed from profile
Phone Loss/Change Flow#
- User reports "lost phone" in app
- AppView removes verification from PDS profile
- AppView deletes phone_hash from database
- User verifies new phone number
- 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: n Implementation Guide
- AWS SNS: 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:
- GitHub Issues: https://github.com/coves-social/coves/issues
- Discord: [your-discord-link]
- Email: support@coves.social