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),
|
||||
/// 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());
|
||||
|
||||
Reference in New Issue
Block a user