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:
2026-04-01 07:56:19 +02:00
parent 12846bd2a0
commit 56331632fd
3 changed files with 291 additions and 6 deletions

View File

@@ -94,6 +94,12 @@ pub struct TimeSlot {
///
/// Propagates through the mesh like [`MeshAnnounce`](crate::announce::MeshAnnounce),
/// 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)]
pub struct SlotAnnounce {
/// Unique announcement ID.
@@ -112,6 +118,10 @@ pub struct SlotAnnounce {
pub slots: Vec<TimeSlot>,
/// SHA-256 of the therapist's Approbation number.
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).
pub sequence: u64,
/// Time-to-live in hours (default 168 = 7 days).
@@ -148,6 +158,34 @@ impl SlotAnnounce {
slots: Vec<TimeSlot>,
approbation_hash: [u8; 32],
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 {
let pk = identity.public_key();
let therapist_address = compute_address(&pk);
@@ -170,6 +208,7 @@ impl SlotAnnounce {
location_hint,
slots,
approbation_hash,
profile_url,
sequence,
ttl_hours: DEFAULT_TTL_HOURS,
timestamp,
@@ -219,6 +258,16 @@ impl SlotAnnounce {
buf.push(0xFF);
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.ttl_hours.to_le_bytes());
buf.extend_from_slice(&self.timestamp.to_le_bytes());