A community based topic aggregation platform built on atproto

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#

  1. ✅ User authenticates with OAuth
  2. ✅ AppView verifies phone ownership (OTP)
  3. ✅ AppView creates signature (includes subject DID)
  4. ✅ AppView writes to user's PDS (OAuth scope)
  5. ✅ AppView stores phone_hash (duplicate prevention)

Validation (Third-party apps)#

  1. ✅ Fetch user's profile from PDS
  2. ✅ Extract verification from verifications array
  3. ✅ Fetch Coves DID document (public key)
  4. ✅ Reconstruct payload (including profile owner DID)
  5. ✅ Verify signature matches
  6. ✅ Check expiry timestamp

Revocation#

  1. ✅ User loses phone → requests revocation
  2. ✅ AppView removes verification from PDS
  3. ✅ AppView deletes phone_hash from DB
  4. ✅ 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:

  1. Domain ownership: Only owner of coves.social can serve files at https://coves.social/.well-known/did.json
  2. HTTPS: TLS prevents MITM attacks
  3. 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 (not math/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!