Verification Security Model#
Attack Prevention: Signature Copying#
The Threat#
Question: What if Bob copies Alice's verification to his profile?
// Alice's profile (did:plc:alice123)
{
"verifications": [
{
"type": "phone",
"verifiedBy": "did:web:coves.social",
"verifiedAt": "2025-01-15T10:00:00Z",
"expiresAt": "2026-01-15T10:00:00Z",
"signature": "abc123..."
}
]
}
// Bob copies this to his profile (did:plc:bob456)
// Will this work? NO!
The Defense#
The signature is created over this payload:
type + verifiedBy + verifiedAt + expiresAt + subjectDID
Critical: subjectDID is the DID of the person being verified.
Example#
Alice's signature payload:
"phone" + "did:web:coves.social" + "2025-01-15T10:00:00Z" + "2026-01-15T10:00:00Z" + "did:plc:alice123"
→ Signature: "abc123..."
Bob tries to use Alice's verification:
Bob's DID: did:plc:bob456
Alice's signature: "abc123..."
Third-party app verifies:
payload = "phone" + "did:web:coves.social" + "2025-01-15T10:00:00Z" + "2026-01-15T10:00:00Z" + "did:plc:bob456"
verify(payload, "abc123...", publicKey) → FALSE ❌
The signature doesn't match because Bob's DID is different from Alice's DID!
Implementation#
Service (when creating signature):
// service.go:216-222
verificationData := &VerificationData{
Type: "phone",
VerifiedBy: s.signer.GetVerifierDID(),
VerifiedAt: now,
ExpiresAt: expiresAt,
SubjectDID: did, // ← This binds it to the user
}
Third-party app (when verifying):
// Reconstruct the EXACT same payload
const payload = verification.type +
verification.verifiedBy +
verification.verifiedAt +
verification.expiresAt +
profileOwnerDID // ← MUST use the profile owner's DID
// Verify signature
const isValid = verifyECDSA(payload, verification.signature, publicKey)
Other Attack Vectors#
1. Replay Attack (Using old verification)#
Defense: Expiry timestamp (expiresAt) is part of signed payload
- Clients check
expiresAt < now()→ reject - Annual re-verification ensures freshness
2. Forged Signature#
Defense: ECDSA P-256 signature with private key known only to Coves
- Attacker would need Coves' private key
- Private key stored in secure environment (secrets manager)
- Can't be brute-forced (256-bit security)
3. Man-in-the-Middle (DID document swap)#
Defense: HTTPS + optional DID pinning
- DID document served over HTTPS
- Clients can pin Coves' DID public key in app
- DNS/TLS security prevents MITM
4. Verification Badge Removal#
Defense: User controls their own PDS
- Only the user (or authorized apps with OAuth) can modify their profile
- AppView writes via OAuth (with user consent)
- Other apps can't remove verifications without permission
5. Phone Number Reuse (Carrier recycling)#
Defense: Expiry + re-verification
- Verifications expire after 1 year
- If carrier recycles number to new person, old verification expires
- New person must verify to get new badge
6. SMS Interception (SIM swap)#
Defense: Rate limiting + audit logging
- Max 3 OTP requests per phone per hour
- Audit logs track suspicious patterns
- Future: Add 2FA backup methods (email, authenticator)
7. Phone Hash Rainbow Table#
Defense: HMAC with secret pepper
phoneHash = HMAC-SHA256(phoneNumber, secretPepper)
- Even if DB is leaked, attacker can't reverse hashes without pepper
- Pepper stored separately (environment variable)
- Prevents bulk phone number enumeration
Verification Lifecycle Security#
Creation#
- ✅ User authenticates with OAuth
- ✅ AppView verifies phone ownership (OTP)
- ✅ AppView creates signature (includes subject DID)
- ✅ AppView writes to user's PDS (OAuth scope)
- ✅ AppView stores phone_hash (duplicate prevention)
Validation (Third-party apps)#
- ✅ Fetch user's profile from PDS
- ✅ Extract verification from
verificationsarray - ✅ Fetch Coves DID document (public key)
- ✅ Reconstruct payload (including profile owner DID)
- ✅ Verify signature matches
- ✅ Check expiry timestamp
Revocation#
- ✅ User loses phone → requests revocation
- ✅ AppView removes verification from PDS
- ✅ AppView deletes phone_hash from DB
- ✅ Third-party apps see no verification (fetching latest from PDS)
did:web Security#
Why .well-known/did.json is Safe#
Q: Can someone just change the DID document? A: No, because:
- Domain ownership: Only owner of
coves.socialcan serve files athttps://coves.social/.well-known/did.json - HTTPS: TLS prevents MITM attacks
- Key rotation: If private key compromised, we rotate (keep old key for 30 days for existing verifications)
Alternative: DNS TXT Record#
We could also add a DNS TXT record for extra verification:
_did.coves.social TXT "did=did:web:coves.social key=<public-key-fingerprint>"
This provides defense-in-depth (DNS + HTTPS both need to be compromised).
Comparison to Other Verification Systems#
Twitter Blue (Centralized)#
- ❌ Phone stored on Twitter servers
- ❌ Not portable (can't take verification to other apps)
- ❌ Twitter controls badge (can remove)
- ✅ Simple to verify (just trust Twitter)
Bluesky Verification (Domain-based)#
- ✅ Decentralized (DNS TXT record)
- ✅ Portable (works across apps)
- ❌ Only works for domain owners
- ❌ No phone verification
Coves Verification (Hybrid)#
- ✅ Privacy-first (phone never stored)
- ✅ Portable (PDS-stored, cryptographically signed)
- ✅ Works for non-domain owners
- ✅ Third-party verifiable (public key in DID document)
- ✅ User-controlled (badge on their PDS)
- ⚠️ Requires crypto verification (more complex for clients)
Security Checklist#
When implementing, ensure:
- Private key stored securely (secrets manager, not in code)
- Phone hash pepper never changes (backup safely)
- OTP generated with
crypto/rand(notmath/rand) - OTP comparison is constant-time (bcrypt)
- Subject DID included in signature payload
- Third-party apps verify signature correctly
- Rate limits enforced at network edge
- Audit logs don't leak phone numbers
- HTTPS enforced for DID document
- PDS writes use OAuth (user consent)
Future Enhancements#
1. Multi-factor Verification#
Require 2+ verification types:
- Phone + Email
- Phone + Domain
- Government ID + Phone
2. Verification Revocation List#
Publish revoked verifications at https://coves.social/.well-known/revocations.json
- Third-party apps can check if verification was revoked
- User privacy preserved (DID hash, not full DID)
3. Hardware Security Module (HSM)#
Store signing key in HSM for production:
- AWS CloudHSM
- Google Cloud KMS
- Azure Key Vault
Prevents key extraction even with server compromise.
Questions?#
Open an issue or ask on Discord!