feat(fapp): add security model + profile_url for verification
docs/specs/fapp-security.md: - Full threat model for patient protection - 3-level verification roadmap (transparency → endorsements → registry) - UI warning mockups - Technical implementation plan - Honest assessment of limitations SlotAnnounce changes: - Added profile_url field for therapist verification - New with_profile() constructor - profile_url included in signature docs/specs/fapp-protocol.md: - Added Security & Anti-Fraud section - Link to full security spec
This commit is contained in:
@@ -94,6 +94,12 @@ pub struct TimeSlot {
|
|||||||
///
|
///
|
||||||
/// Propagates through the mesh like [`MeshAnnounce`](crate::announce::MeshAnnounce),
|
/// Propagates through the mesh like [`MeshAnnounce`](crate::announce::MeshAnnounce),
|
||||||
/// cached by relay nodes with `CAP_FAPP_RELAY`.
|
/// cached by relay nodes with `CAP_FAPP_RELAY`.
|
||||||
|
///
|
||||||
|
/// # Security Note
|
||||||
|
///
|
||||||
|
/// Patients should verify therapists before booking. The `profile_url` field
|
||||||
|
/// allows cross-referencing with official sources (Jameda, KBV, practice website).
|
||||||
|
/// See `docs/specs/fapp-security.md` for the full security model.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct SlotAnnounce {
|
pub struct SlotAnnounce {
|
||||||
/// Unique announcement ID.
|
/// Unique announcement ID.
|
||||||
@@ -112,6 +118,10 @@ pub struct SlotAnnounce {
|
|||||||
pub slots: Vec<TimeSlot>,
|
pub slots: Vec<TimeSlot>,
|
||||||
/// SHA-256 of the therapist's Approbation number.
|
/// SHA-256 of the therapist's Approbation number.
|
||||||
pub approbation_hash: [u8; 32],
|
pub approbation_hash: [u8; 32],
|
||||||
|
/// Optional URL to therapist's public profile for verification.
|
||||||
|
/// Examples: Jameda profile, KBV listing, practice website.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub profile_url: Option<String>,
|
||||||
/// Monotonically increasing per therapist (dedup/supersede).
|
/// Monotonically increasing per therapist (dedup/supersede).
|
||||||
pub sequence: u64,
|
pub sequence: u64,
|
||||||
/// Time-to-live in hours (default 168 = 7 days).
|
/// Time-to-live in hours (default 168 = 7 days).
|
||||||
@@ -148,6 +158,34 @@ impl SlotAnnounce {
|
|||||||
slots: Vec<TimeSlot>,
|
slots: Vec<TimeSlot>,
|
||||||
approbation_hash: [u8; 32],
|
approbation_hash: [u8; 32],
|
||||||
sequence: u64,
|
sequence: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_profile(
|
||||||
|
identity,
|
||||||
|
fachrichtung,
|
||||||
|
modalitaet,
|
||||||
|
kostentraeger,
|
||||||
|
location_hint,
|
||||||
|
slots,
|
||||||
|
approbation_hash,
|
||||||
|
sequence,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create and sign a new slot announcement with optional profile URL.
|
||||||
|
///
|
||||||
|
/// The `profile_url` allows patients to verify the therapist's identity
|
||||||
|
/// against official sources (Jameda, KBV, practice website).
|
||||||
|
pub fn with_profile(
|
||||||
|
identity: &MeshIdentity,
|
||||||
|
fachrichtung: Vec<Fachrichtung>,
|
||||||
|
modalitaet: Vec<Modalitaet>,
|
||||||
|
kostentraeger: Vec<Kostentraeger>,
|
||||||
|
location_hint: String,
|
||||||
|
slots: Vec<TimeSlot>,
|
||||||
|
approbation_hash: [u8; 32],
|
||||||
|
sequence: u64,
|
||||||
|
profile_url: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let pk = identity.public_key();
|
let pk = identity.public_key();
|
||||||
let therapist_address = compute_address(&pk);
|
let therapist_address = compute_address(&pk);
|
||||||
@@ -170,6 +208,7 @@ impl SlotAnnounce {
|
|||||||
location_hint,
|
location_hint,
|
||||||
slots,
|
slots,
|
||||||
approbation_hash,
|
approbation_hash,
|
||||||
|
profile_url,
|
||||||
sequence,
|
sequence,
|
||||||
ttl_hours: DEFAULT_TTL_HOURS,
|
ttl_hours: DEFAULT_TTL_HOURS,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -219,6 +258,16 @@ impl SlotAnnounce {
|
|||||||
buf.push(0xFF);
|
buf.push(0xFF);
|
||||||
|
|
||||||
buf.extend_from_slice(&self.approbation_hash);
|
buf.extend_from_slice(&self.approbation_hash);
|
||||||
|
|
||||||
|
// profile_url is signed to prevent tampering.
|
||||||
|
if let Some(ref url) = self.profile_url {
|
||||||
|
buf.push(0x01); // present marker
|
||||||
|
buf.extend_from_slice(url.as_bytes());
|
||||||
|
} else {
|
||||||
|
buf.push(0x00); // absent marker
|
||||||
|
}
|
||||||
|
buf.push(0xFF);
|
||||||
|
|
||||||
buf.extend_from_slice(&self.sequence.to_le_bytes());
|
buf.extend_from_slice(&self.sequence.to_le_bytes());
|
||||||
buf.extend_from_slice(&self.ttl_hours.to_le_bytes());
|
buf.extend_from_slice(&self.ttl_hours.to_le_bytes());
|
||||||
buf.extend_from_slice(&self.timestamp.to_le_bytes());
|
buf.extend_from_slice(&self.timestamp.to_le_bytes());
|
||||||
|
|||||||
@@ -136,14 +136,39 @@ Therapist confirms or rejects a reservation.
|
|||||||
- `duration_minutes: u16` — Duration (typically 50 or 25 minutes)
|
- `duration_minutes: u16` — Duration (typically 50 or 25 minutes)
|
||||||
- `slot_type: SlotType` — Type of appointment
|
- `slot_type: SlotType` — Type of appointment
|
||||||
|
|
||||||
## Anti-Spam
|
## Security & Anti-Fraud
|
||||||
|
|
||||||
1. **Approbation hash binding.** The `approbation_hash` field contains SHA-256 of the therapist's Approbation number. While mesh nodes cannot verify this against a registry, it creates accountability — a therapist's identity is tied to a real credential.
|
> **See [fapp-security.md](fapp-security.md) for the full security model.**
|
||||||
|
|
||||||
|
### Patient Protection
|
||||||
|
|
||||||
|
Patients are vulnerable. FAPP must protect against fraudulent "therapists":
|
||||||
|
|
||||||
|
| Threat | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| Fake Therapist | `profile_url` for cross-verification, UI warnings |
|
||||||
|
| Impersonation | Ed25519 signatures, endorsement system (planned) |
|
||||||
|
| Data Harvesting | Anonymous queries, no patient identity in protocol |
|
||||||
|
| Financial Fraud | "Never pay upfront" warnings, reputation (planned) |
|
||||||
|
|
||||||
|
### Verification Levels
|
||||||
|
|
||||||
|
| Level | Mechanism | Trust |
|
||||||
|
|-------|-----------|-------|
|
||||||
|
| 0 | None — only mesh signature | Low |
|
||||||
|
| 1 | Endorsement by trusted relay | Medium |
|
||||||
|
| 2 | Registry verification (KBV) | High |
|
||||||
|
|
||||||
|
**Current implementation:** Level 0 with `profile_url` for transparency.
|
||||||
|
|
||||||
|
### Anti-Spam
|
||||||
|
|
||||||
|
1. **Approbation hash binding.** The `approbation_hash` field contains SHA-256 of the therapist's Approbation number. Creates accountability — therapist identity tied to real credential.
|
||||||
2. **Signature verification.** All SlotAnnounces are Ed25519-signed. Relay nodes reject unsigned or invalid announcements.
|
2. **Signature verification.** All SlotAnnounces are Ed25519-signed. Relay nodes reject unsigned or invalid announcements.
|
||||||
3. **Rate limiting.** Relay nodes enforce a maximum announcement rate per therapist address (e.g., max 10 SlotAnnounces per hour per therapist_address).
|
3. **Rate limiting.** Relay nodes enforce max 10 SlotAnnounces per hour per therapist_address.
|
||||||
4. **Sequence-based dedup.** Each therapist maintains a monotonic sequence counter. Relay nodes only accept announces with sequence >= last seen for that therapist.
|
4. **Sequence-based dedup.** Monotonic counter; relays only accept sequence >= last seen.
|
||||||
5. **TTL enforcement.** Expired announcements are garbage collected. Default TTL is 7 days.
|
5. **TTL enforcement.** Expired announcements are garbage collected. Default 7 days.
|
||||||
6. **Hop limit.** SlotAnnounces have a max_hops field (default 8) to prevent infinite propagation.
|
6. **Hop limit.** max_hops field (default 8) prevents infinite propagation.
|
||||||
|
|
||||||
## Wire Format
|
## Wire Format
|
||||||
|
|
||||||
|
|||||||
211
docs/specs/fapp-security.md
Normal file
211
docs/specs/fapp-security.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# FAPP Security Model — Protecting Patients from Fraud
|
||||||
|
|
||||||
|
## Threat Model
|
||||||
|
|
||||||
|
### Who are we protecting?
|
||||||
|
|
||||||
|
**Patients** seeking psychotherapy are in a vulnerable state. They may be:
|
||||||
|
- Desperate after months of searching
|
||||||
|
- Unfamiliar with the healthcare system
|
||||||
|
- Willing to pay out-of-pocket if GKV slots are scarce
|
||||||
|
- Trusting of anyone who appears professional
|
||||||
|
|
||||||
|
### What are the threats?
|
||||||
|
|
||||||
|
| Threat | Description | Severity |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| **Fake Therapist** | Attacker poses as licensed therapist, collects patient data | CRITICAL |
|
||||||
|
| **Phishing** | Fake slots lead to malicious contact forms | HIGH |
|
||||||
|
| **Financial Fraud** | "Therapist" demands upfront payment | HIGH |
|
||||||
|
| **Data Harvesting** | Collect patient health queries for profiling | MEDIUM |
|
||||||
|
| **Spam Flooding** | Overwhelm mesh with fake announces | MEDIUM |
|
||||||
|
| **Impersonation** | Clone a real therapist's identity | CRITICAL |
|
||||||
|
|
||||||
|
## Current Protections (v1)
|
||||||
|
|
||||||
|
| Protection | Mechanism | Weakness |
|
||||||
|
|------------|-----------|----------|
|
||||||
|
| Approbation Hash | SHA-256 of credential number | **Cannot be verified** — attacker can invent hash |
|
||||||
|
| Ed25519 Signature | Proves control of mesh key | Doesn't prove real-world identity |
|
||||||
|
| Sequence Dedup | Prevents replay | Doesn't prevent new fake announces |
|
||||||
|
| Rate Limiting | Max announces/hour | Attacker can use multiple keys |
|
||||||
|
|
||||||
|
**Honest assessment:** Current protections prevent spam but **do not prevent fraud**.
|
||||||
|
|
||||||
|
## Proposed Security Enhancements
|
||||||
|
|
||||||
|
### Level 1: Transparency (Low Trust, No Verification)
|
||||||
|
|
||||||
|
**Concept:** Make it easy for patients to verify therapists themselves.
|
||||||
|
|
||||||
|
1. **Therapist Profile URL**
|
||||||
|
- SlotAnnounce includes optional `profile_url: String`
|
||||||
|
- Points to therapist's website, Jameda profile, or KV listing
|
||||||
|
- Patient can cross-check before booking
|
||||||
|
|
||||||
|
2. **Approbation Display**
|
||||||
|
- Show first 4 digits of Approbation hash in UI
|
||||||
|
- Patient can ask therapist to confirm during Erstgespräch
|
||||||
|
- Social verification, not cryptographic
|
||||||
|
|
||||||
|
3. **Warning Labels**
|
||||||
|
- UI shows "Unverified Therapist" prominently
|
||||||
|
- Patient must acknowledge risk before reserving
|
||||||
|
|
||||||
|
**Implementation:** ~2 days, no infrastructure changes.
|
||||||
|
|
||||||
|
### Level 2: Web-of-Trust (Medium Trust)
|
||||||
|
|
||||||
|
**Concept:** Trusted nodes vouch for therapists.
|
||||||
|
|
||||||
|
1. **Endorsement Messages**
|
||||||
|
- Trusted relays (e.g., run by patient advocacy groups) sign endorsements
|
||||||
|
- `TherapistEndorsement { therapist_address, endorser_signature, reason }`
|
||||||
|
- Patients can filter by "endorsed by [Patientenberatung]"
|
||||||
|
|
||||||
|
2. **Reputation Scores**
|
||||||
|
- After appointments, patients can rate (anonymously)
|
||||||
|
- Aggregate scores propagate through mesh
|
||||||
|
- New therapists start with "No ratings yet"
|
||||||
|
|
||||||
|
3. **Blocklists**
|
||||||
|
- Community-maintained blocklists of known fraudsters
|
||||||
|
- Relay nodes can subscribe and filter
|
||||||
|
|
||||||
|
**Implementation:** ~2 weeks, requires gossip protocol for endorsements.
|
||||||
|
|
||||||
|
### Level 3: Registry Integration (High Trust)
|
||||||
|
|
||||||
|
**Concept:** Verify against official sources.
|
||||||
|
|
||||||
|
1. **KV-Registry Lookup**
|
||||||
|
- Germany: KBV Arztsuche API (https://www.kbv.de/html/arztsuche.php)
|
||||||
|
- Therapist provides Lebenslange Arztnummer (LANR) or BSNR
|
||||||
|
- Gateway node queries registry, signs attestation
|
||||||
|
|
||||||
|
2. **eHBA Integration** (long-term)
|
||||||
|
- Electronic Health Professional Card
|
||||||
|
- Therapist proves identity via qualified electronic signature
|
||||||
|
- Strongest guarantee, but requires card reader
|
||||||
|
|
||||||
|
3. **Chamber Verification**
|
||||||
|
- Psychotherapeutenkammer publishes member lists
|
||||||
|
- Automated scraping + attestation (legally gray)
|
||||||
|
|
||||||
|
**Implementation:** 1-2 months, requires trusted gateway infrastructure.
|
||||||
|
|
||||||
|
## Recommended Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Ship with Warnings (Now)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ ⚠️ UNVERIFIED THERAPIST │
|
||||||
|
│ │
|
||||||
|
│ This therapist has not been verified. │
|
||||||
|
│ Before booking: │
|
||||||
|
│ • Check their website or Jameda profile │
|
||||||
|
│ • Verify Approbation during first contact │
|
||||||
|
│ • Never pay upfront without meeting │
|
||||||
|
│ │
|
||||||
|
│ [I understand the risks] [Cancel] │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add `profile_url` field to SlotAnnounce
|
||||||
|
- Prominent warnings in UI
|
||||||
|
- Educational content about verification
|
||||||
|
|
||||||
|
### Phase 2: Endorsements (Q2 2026)
|
||||||
|
|
||||||
|
- Partner with 2-3 patient advocacy groups
|
||||||
|
- They run relay nodes with endorsement capability
|
||||||
|
- "Endorsed by Unabhängige Patientenberatung" badge
|
||||||
|
|
||||||
|
### Phase 3: Registry (Q4 2026)
|
||||||
|
|
||||||
|
- Build KBV gateway (if API access granted)
|
||||||
|
- Or: manual verification service (humans check credentials)
|
||||||
|
- Verified badge with expiry
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### SlotAnnounce v2
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SlotAnnounce {
|
||||||
|
// ... existing fields ...
|
||||||
|
|
||||||
|
/// Optional URL to therapist's public profile (Jameda, website, KV listing).
|
||||||
|
pub profile_url: Option<String>,
|
||||||
|
|
||||||
|
/// Optional LANR (Lebenslange Arztnummer) for registry lookup.
|
||||||
|
pub lanr: Option<String>,
|
||||||
|
|
||||||
|
/// Verification level (0 = none, 1 = endorsed, 2 = registry-verified).
|
||||||
|
pub verification_level: u8,
|
||||||
|
|
||||||
|
/// Endorsement signatures from trusted nodes.
|
||||||
|
pub endorsements: Vec<Endorsement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Endorsement {
|
||||||
|
/// Address of the endorsing node.
|
||||||
|
pub endorser_address: [u8; 16],
|
||||||
|
/// Ed25519 signature over (therapist_address, timestamp).
|
||||||
|
pub signature: [u8; 64],
|
||||||
|
/// Unix timestamp of endorsement.
|
||||||
|
pub timestamp: u64,
|
||||||
|
/// Human-readable reason.
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Patient-Side Verification Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Patient receives SlotAnnounce
|
||||||
|
2. UI shows verification_level:
|
||||||
|
- 0: "⚠️ Unverified" (red)
|
||||||
|
- 1: "✓ Endorsed by [name]" (yellow)
|
||||||
|
- 2: "✓✓ Registry Verified" (green)
|
||||||
|
3. Patient can click to see:
|
||||||
|
- Profile URL
|
||||||
|
- Endorsement details
|
||||||
|
- Verification expiry
|
||||||
|
4. Before SlotReserve, patient confirms risk acknowledgment
|
||||||
|
```
|
||||||
|
|
||||||
|
## What We Cannot Prevent
|
||||||
|
|
||||||
|
Even with Level 3 verification:
|
||||||
|
|
||||||
|
1. **Licensed but Unethical Therapist** — Credential is real, behavior is not
|
||||||
|
2. **Session Quality** — Verification proves license, not competence
|
||||||
|
3. **Availability Lies** — Therapist might not actually have slots
|
||||||
|
4. **Price Gouging** — "Selbstzahler" with inflated rates
|
||||||
|
|
||||||
|
**These require reputation systems and patient reviews** — can't be solved cryptographically.
|
||||||
|
|
||||||
|
## Comparison to Existing Systems
|
||||||
|
|
||||||
|
| System | Verification | Privacy | Decentralized |
|
||||||
|
|--------|--------------|---------|---------------|
|
||||||
|
| **Doctolib** | KV registry | Low (tracks searches) | No |
|
||||||
|
| **Jameda** | None (self-reported) | Low | No |
|
||||||
|
| **KBV Arztsuche** | Official | Medium | No |
|
||||||
|
| **FAPP v1** | None | High | Yes |
|
||||||
|
| **FAPP + Level 2** | Endorsements | High | Yes |
|
||||||
|
| **FAPP + Level 3** | Registry | High | Mostly |
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
FAPP's strength is **patient privacy**. We should not sacrifice that for centralized verification.
|
||||||
|
|
||||||
|
**Recommended approach:**
|
||||||
|
1. Ship with strong warnings and profile URLs (transparency)
|
||||||
|
2. Build endorsement network (web-of-trust)
|
||||||
|
3. Add optional registry verification for therapists who want it
|
||||||
|
4. Let patients choose their trust level
|
||||||
|
|
||||||
|
The mesh provides the infrastructure. Trust is a social problem that requires social solutions.
|
||||||
Reference in New Issue
Block a user