Skip to main content

syncweb_core/indexing/
wot.rs

1//! Local Web-of-Trust policy and signed metadata for indexed content.
2//!
3//! Content hashes establish integrity, while this module records who made a
4//! claim and lets each indexer decide which claims are trusted. Records are
5//! signed before they enter the SQLite/FTS index; moderation and revocation
6//! decisions remain local policy state.
7
8use std::{
9    collections::{HashMap, HashSet},
10    sync::{Arc, Mutex},
11    time::{SystemTime, UNIX_EPOCH},
12};
13
14use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
15use iroh::PublicKey;
16use iroh_blobs::Hash;
17use iroh_docs::NamespaceId;
18use serde::{Deserialize, Serialize};
19
20use crate::{
21    error::{Result, SyncwebError},
22    gossip::SignedGossipMessage,
23    indexing::{IndexingDatabase, IndexingService},
24};
25
26const METADATA_CONTEXT: &[u8] = b"syncweb/wot/metadata/v1\0";
27const DELEGATION_CONTEXT: &[u8] = b"syncweb/wot/delegation/v1\0";
28const REVOCATION_CONTEXT: &[u8] = b"syncweb/wot/revocation/v1\0";
29const MODERATION_CONTEXT: &[u8] = b"syncweb/wot/moderation/v1\0";
30const ATTESTATION_CONTEXT: &[u8] = b"syncweb/wot/attestation/v1\0";
31const PROVIDER_TRUST_CONTEXT: &[u8] = b"syncweb/wot/provider-trust/v1\0";
32
33/// Signed metadata appended to a content hash.
34#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
35#[non_exhaustive]
36pub struct MetadataEntry {
37    pub content: Hash,
38    pub key: String,
39    pub value: String,
40    pub author: String,
41    pub sequence: u64,
42    pub created_at: u64,
43    pub signature: Option<String>,
44}
45
46/// Alias emphasizing that the entry belongs to the Web-of-Trust layer.
47pub type WotMetadata = MetadataEntry;
48
49impl MetadataEntry {
50    /// Create and sign metadata with an Ed25519 author key.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if metadata fields are invalid or signing fails.
55    pub fn new(
56        content: Hash,
57        key: impl Into<String>,
58        value: impl Into<String>,
59        sequence: u64,
60        signing_key: &SigningKey,
61    ) -> Result<Self> {
62        Self::new_with_time(content, key, value, sequence, current_epoch_seconds(), signing_key)
63    }
64
65    /// Create and sign metadata with an explicit timestamp.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if metadata fields are invalid or signing fails.
70    pub fn new_with_time(
71        content: Hash,
72        key: impl Into<String>,
73        value: impl Into<String>,
74        sequence: u64,
75        created_at: u64,
76        signing_key: &SigningKey,
77    ) -> Result<Self> {
78        let mut entry = Self {
79            content,
80            key: key.into(),
81            value: value.into(),
82            author: author_id(&signing_key.verifying_key()),
83            sequence,
84            created_at,
85            signature: None,
86        };
87        entry.sign(signing_key)?;
88        Ok(entry)
89    }
90
91    /// Create unsigned metadata for a specific author identity.
92    ///
93    /// Call [`Self::sign`] before accepting or serializing the entry.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if metadata fields are invalid.
98    pub fn unsigned(
99        content: Hash,
100        key: impl Into<String>,
101        value: impl Into<String>,
102        sequence: u64,
103        created_at: u64,
104        author: &VerifyingKey,
105    ) -> Result<Self> {
106        let entry = Self {
107            content,
108            key: key.into(),
109            value: value.into(),
110            author: author_id(author),
111            sequence,
112            created_at,
113            signature: None,
114        };
115        entry.validate()?;
116        Ok(entry)
117    }
118
119    /// Sign the entry with its declared author.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if the signing key does not match the author.
124    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
125        ensure_signer(&self.author, &signing_key.verifying_key())?;
126        self.signature = Some(sign_text(METADATA_CONTEXT, &self.unsigned_bytes()?, signing_key));
127        Ok(())
128    }
129
130    /// Verify the metadata signature and structure.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the metadata or signature is invalid.
135    pub fn verify_signature(&self) -> Result<()> {
136        self.validate()?;
137        verify_text(
138            METADATA_CONTEXT,
139            &self.unsigned_bytes()?,
140            &self.author,
141            self.signature.as_deref(),
142        )
143    }
144
145    /// Validate non-cryptographic metadata fields.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if a required field or author identity is invalid.
150    pub fn validate(&self) -> Result<()> {
151        if self.key.trim().is_empty() {
152            return Err(SyncwebError::InvalidConfig(
153                "Web-of-Trust metadata key cannot be empty".to_owned(),
154            ));
155        }
156        if self.sequence == 0 {
157            return Err(SyncwebError::InvalidConfig(
158                "Web-of-Trust metadata sequence must be greater than zero".to_owned(),
159            ));
160        }
161        let _author = parse_author(&self.author)?;
162        Ok(())
163    }
164
165    /// Encode the entry as JSON.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if the entry is invalid or cannot be serialized.
170    pub fn to_bytes(&self) -> Result<Vec<u8>> {
171        self.validate()?;
172        serde_json::to_vec(self)
173            .map_err(|error| SyncwebError::operation("failed to serialize Web-of-Trust metadata", error))
174    }
175
176    /// Decode an entry from JSON.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the entry is malformed.
181    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
182        let entry: Self = serde_json::from_slice(bytes.as_ref())
183            .map_err(|error| SyncwebError::operation("failed to deserialize Web-of-Trust metadata", error))?;
184        entry.validate()?;
185        Ok(entry)
186    }
187
188    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
189        let mut unsigned = self.clone();
190        unsigned.signature = None;
191        signed_bytes(METADATA_CONTEXT, &unsigned)
192    }
193}
194
195/// A signed delegation from one trusted author to another.
196#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
197#[non_exhaustive]
198pub struct TrustDelegation {
199    pub delegator: String,
200    pub delegate: String,
201    pub scope: Option<Hash>,
202    pub max_depth: Option<u32>,
203    pub sequence: u64,
204    pub issued_at: u64,
205    pub expires_at: u64,
206    pub revoked_at: Option<u64>,
207    pub signature: Option<String>,
208}
209
210impl TrustDelegation {
211    /// Create and sign a delegation.
212    ///
213    /// `scope` restricts the delegate to one content hash; `None` delegates
214    /// generally.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if identities or timestamps are invalid.
219    pub fn new(
220        delegate: &VerifyingKey,
221        scope: Option<Hash>,
222        sequence: u64,
223        expires_at: u64,
224        signing_key: &SigningKey,
225    ) -> Result<Self> {
226        Self::new_with_time(
227            delegate,
228            scope,
229            sequence,
230            current_epoch_seconds(),
231            expires_at,
232            signing_key,
233        )
234    }
235
236    /// Create and sign a delegation with an explicit issue time.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if identities or timestamps are invalid.
241    pub fn new_with_time(
242        delegate: &VerifyingKey,
243        scope: Option<Hash>,
244        sequence: u64,
245        issued_at: u64,
246        expires_at: u64,
247        signing_key: &SigningKey,
248    ) -> Result<Self> {
249        let mut delegation = Self {
250            delegator: author_id(&signing_key.verifying_key()),
251            delegate: author_id(delegate),
252            scope,
253            max_depth: None,
254            sequence,
255            issued_at,
256            expires_at,
257            revoked_at: None,
258            signature: None,
259        };
260        delegation.sign(signing_key)?;
261        Ok(delegation)
262    }
263
264    /// Set a maximum delegation chain depth and re-sign.
265    ///
266    /// # Errors
267    ///
268    /// Returns an error if the signing key does not match the delegator.
269    pub fn with_max_depth(mut self, max_depth: u32, signing_key: &SigningKey) -> Result<Self> {
270        self.max_depth = Some(max_depth);
271        self.sign(signing_key)?;
272        Ok(self)
273    }
274
275    /// Sign the delegation with its declared delegator.
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if the signing key does not match the delegator.
280    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
281        ensure_signer(&self.delegator, &signing_key.verifying_key())?;
282        self.signature = Some(sign_text(DELEGATION_CONTEXT, &self.unsigned_bytes()?, signing_key));
283        Ok(())
284    }
285
286    /// Verify the delegation signature and structure.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error if the delegation or signature is invalid.
291    pub fn verify_signature(&self) -> Result<()> {
292        self.validate()?;
293        verify_text(
294            DELEGATION_CONTEXT,
295            &self.unsigned_bytes()?,
296            &self.delegator,
297            self.signature.as_deref(),
298        )
299    }
300
301    /// Verify the delegation and require it to be active at `now`.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the signature is invalid or the delegation expired.
306    pub fn verify_at(&self, now: u64) -> Result<()> {
307        self.verify_signature()?;
308        if self.revoked_at.is_some_and(|revoked_at| revoked_at <= now) {
309            return Err(SyncwebError::InvalidConfig(
310                "trust delegation has been revoked".to_owned(),
311            ));
312        }
313        if self.expires_at <= now {
314            return Err(SyncwebError::InvalidConfig("trust delegation has expired".to_owned()));
315        }
316        Ok(())
317    }
318
319    /// Validate identities, sequence, and timestamps.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error if a delegation field is invalid.
324    pub fn validate(&self) -> Result<()> {
325        if self.sequence == 0 {
326            return Err(SyncwebError::InvalidConfig(
327                "trust delegation sequence must be greater than zero".to_owned(),
328            ));
329        }
330        if self.expires_at <= self.issued_at {
331            return Err(SyncwebError::InvalidConfig(
332                "trust delegation expiration must be after its issue time".to_owned(),
333            ));
334        }
335        if self.delegator == self.delegate {
336            return Err(SyncwebError::InvalidConfig(
337                "trust delegation cannot delegate to itself".to_owned(),
338            ));
339        }
340        let _delegator = parse_author(&self.delegator)?;
341        let _delegate = parse_author(&self.delegate)?;
342        Ok(())
343    }
344
345    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
346        let mut unsigned = self.clone();
347        unsigned.signature = None;
348        signed_bytes(DELEGATION_CONTEXT, &unsigned)
349    }
350}
351
352/// The result of evaluating a local trust policy.
353#[derive(Clone, Copy, Debug, Eq, PartialEq)]
354#[non_exhaustive]
355pub enum TrustDecision {
356    TrustedRoot,
357    TrustedDelegation,
358    Untrusted,
359    Revoked,
360}
361
362impl TrustDecision {
363    #[must_use]
364    pub const fn is_trusted(self) -> bool {
365        matches!(self, Self::TrustedRoot | Self::TrustedDelegation)
366    }
367}
368
369/// Local trust roots and cryptographically signed delegations.
370#[derive(Clone, Debug, Default)]
371pub struct TrustPolicy {
372    roots: HashSet<String>,
373    delegations: HashMap<String, Vec<TrustDelegation>>,
374    revoked_authors: HashSet<String>,
375}
376
377impl TrustPolicy {
378    /// Create an empty local policy.
379    #[must_use]
380    pub fn new() -> Self {
381        Self::default()
382    }
383
384    /// Create a policy with one trusted root.
385    #[must_use]
386    pub fn with_root(signing_key: &SigningKey) -> Self {
387        let mut policy = Self::new();
388        policy.trust_author(&signing_key.verifying_key());
389        policy
390    }
391
392    /// Trust an author directly as a local root.
393    pub fn trust_author(&mut self, author: &VerifyingKey) {
394        self.roots.insert(author_id(author));
395        self.revoked_authors.remove(&author_id(author));
396    }
397
398    /// Revoke a local trust root or delegate identity.
399    pub fn revoke_author(&mut self, author: &VerifyingKey) {
400        self.revoked_authors.insert(author_id(author));
401    }
402
403    /// Add a signed delegation issued by an already trusted author.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if the delegation is invalid, expired, or not issued
408    /// by an identity trusted for its scope.
409    pub fn add_delegation(&mut self, delegation: TrustDelegation) -> Result<bool> {
410        self.add_delegation_at(delegation, current_epoch_seconds())
411    }
412
413    /// Revoke an active delegation matching the delegate, scope, and delegator.
414    ///
415    /// Returns `true` if any delegation was marked revoked.
416    ///
417    /// # Errors
418    ///
419    /// This method does not return errors.
420    pub fn revoke_delegation(&mut self, delegator: &str, delegate: &str, scope: Option<&Hash>) -> Result<bool> {
421        self.revoke_delegation_at(delegator, delegate, scope, current_epoch_seconds())
422    }
423
424    /// Revoke a delegation using an explicit revocation time.
425    ///
426    /// Returns `true` if any delegation was marked revoked.
427    ///
428    /// # Errors
429    ///
430    /// This method does not return errors.
431    pub fn revoke_delegation_at(
432        &mut self,
433        delegator: &str,
434        delegate: &str,
435        scope: Option<&Hash>,
436        at: u64,
437    ) -> Result<bool> {
438        let mut changed = false;
439        if let Some(delegations) = self.delegations.get_mut(delegator) {
440            for delegation in delegations.iter_mut() {
441                if delegation.delegate == delegate
442                    && delegation.scope.as_ref() == scope
443                    && delegation.revoked_at.is_none()
444                {
445                    delegation.revoked_at = Some(at);
446                    changed = true;
447                }
448            }
449        }
450        Ok(changed)
451    }
452
453    /// Add a signed delegation using an explicit validation time.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error if the delegation is invalid, expired, or not issued
458    /// by an identity trusted for its scope.
459    pub fn add_delegation_at(&mut self, delegation: TrustDelegation, now: u64) -> Result<bool> {
460        delegation.verify_at(now)?;
461        let scope = delegation.scope;
462        if !self.is_trusted_for_at(&delegation.delegator, scope.as_ref(), now) {
463            return Err(SyncwebError::InvalidIdentity(
464                "trust delegation issuer is not trusted for its scope".to_owned(),
465            ));
466        }
467        let entries = self.delegations.entry(delegation.delegator.clone()).or_default();
468        if entries.iter().any(|existing| {
469            existing.delegate == delegation.delegate
470                && existing.scope == delegation.scope
471                && existing.sequence >= delegation.sequence
472        }) {
473            return Ok(false);
474        }
475        entries.retain(|existing| {
476            !(existing.delegate == delegation.delegate
477                && existing.scope == delegation.scope
478                && existing.sequence < delegation.sequence)
479        });
480        entries.push(delegation);
481        Ok(true)
482    }
483
484    /// Return the local trust decision for an author.
485    #[must_use]
486    pub fn evaluate(&self, author: &str) -> TrustDecision {
487        self.evaluate_at(author, current_epoch_seconds())
488    }
489
490    /// Return the local trust decision at an explicit time.
491    #[must_use]
492    pub fn evaluate_at(&self, author: &str, now: u64) -> TrustDecision {
493        self.evaluate_for_at(author, None, now)
494    }
495
496    /// Return the local trust decision for an author and optional content.
497    #[must_use]
498    pub fn evaluate_for(&self, author: &str, content: Option<&Hash>) -> TrustDecision {
499        self.evaluate_for_at(author, content, current_epoch_seconds())
500    }
501
502    /// Return the local trust decision for an author and content at an
503    /// explicit time.
504    #[must_use]
505    pub fn evaluate_for_at(&self, author: &str, content: Option<&Hash>, now: u64) -> TrustDecision {
506        if self.revoked_authors.contains(author) {
507            return TrustDecision::Revoked;
508        }
509        if self.roots.contains(author) {
510            return TrustDecision::TrustedRoot;
511        }
512        if self.reaches_trusted_root(author, content, now, 0, &mut HashSet::new()) {
513            TrustDecision::TrustedDelegation
514        } else {
515            TrustDecision::Untrusted
516        }
517    }
518
519    /// Return whether an author is trusted for the requested content.
520    #[must_use]
521    pub fn is_trusted_for(&self, author: &str, content: Option<&Hash>) -> bool {
522        self.is_trusted_for_at(author, content, current_epoch_seconds())
523    }
524
525    /// Return whether an author is trusted at an explicit time.
526    #[must_use]
527    pub fn is_trusted_for_at(&self, author: &str, content: Option<&Hash>, now: u64) -> bool {
528        self.evaluate_for_at(author, content, now).is_trusted()
529    }
530
531    fn reaches_trusted_root(
532        &self,
533        target: &str,
534        content: Option<&Hash>,
535        now: u64,
536        depth: u32,
537        visited: &mut HashSet<String>,
538    ) -> bool {
539        if self.revoked_authors.contains(target) {
540            return false;
541        }
542        if self.roots.contains(target) {
543            return true;
544        }
545        if !visited.insert(target.to_owned()) {
546            return false;
547        }
548        self.delegations.iter().any(|(delegator, delegations)| {
549            delegations.iter().any(|delegation| {
550                if delegation.delegate != target
551                    || delegation.revoked_at.is_some_and(|revoked_at| revoked_at <= now)
552                    || delegation.expires_at <= now
553                    || (delegation.scope.is_some() && delegation.scope.as_ref() != content)
554                {
555                    return false;
556                }
557                let next_depth = depth.saturating_add(1);
558                if delegation.max_depth.is_some_and(|max_depth| next_depth > max_depth) {
559                    return false;
560                }
561                let mut branch_visited = visited.clone();
562                self.reaches_trusted_root(delegator, content, now, next_depth, &mut branch_visited)
563            })
564        })
565    }
566}
567
568/// A publisher-signed self-revocation for a content hash.
569#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
570#[non_exhaustive]
571pub struct RevocationRecord {
572    pub content: Hash,
573    pub publisher: String,
574    pub sequence: u64,
575    pub revoked_at: u64,
576    pub reason: String,
577    pub signature: Option<String>,
578}
579
580impl RevocationRecord {
581    /// Create and sign a self-revocation.
582    ///
583    /// # Errors
584    ///
585    /// Returns an error if the sequence or reason is invalid.
586    pub fn new(content: Hash, sequence: u64, reason: impl Into<String>, signing_key: &SigningKey) -> Result<Self> {
587        let mut record = Self {
588            content,
589            publisher: author_id(&signing_key.verifying_key()),
590            sequence,
591            revoked_at: current_epoch_seconds(),
592            reason: reason.into(),
593            signature: None,
594        };
595        record.sign(signing_key)?;
596        Ok(record)
597    }
598
599    /// Sign the revocation with the declared publisher.
600    ///
601    /// # Errors
602    ///
603    /// Returns an error if the signing key does not match the publisher.
604    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
605        ensure_signer(&self.publisher, &signing_key.verifying_key())?;
606        self.signature = Some(sign_text(REVOCATION_CONTEXT, &self.unsigned_bytes()?, signing_key));
607        Ok(())
608    }
609
610    /// Verify the publisher signature.
611    ///
612    /// # Errors
613    ///
614    /// Returns an error if the record or signature is invalid.
615    pub fn verify_signature(&self) -> Result<()> {
616        self.validate()?;
617        verify_text(
618            REVOCATION_CONTEXT,
619            &self.unsigned_bytes()?,
620            &self.publisher,
621            self.signature.as_deref(),
622        )
623    }
624
625    /// Validate revocation fields.
626    ///
627    /// # Errors
628    ///
629    /// Returns an error if the sequence, reason, or publisher is invalid.
630    pub fn validate(&self) -> Result<()> {
631        if self.sequence == 0 {
632            return Err(SyncwebError::InvalidConfig(
633                "revocation sequence must be greater than zero".to_owned(),
634            ));
635        }
636        if self.reason.trim().is_empty() {
637            return Err(SyncwebError::InvalidConfig(
638                "revocation reason cannot be empty".to_owned(),
639            ));
640        }
641        let _publisher = parse_author(&self.publisher)?;
642        Ok(())
643    }
644
645    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
646        let mut unsigned = self.clone();
647        unsigned.signature = None;
648        signed_bytes(REVOCATION_CONTEXT, &unsigned)
649    }
650}
651
652/// Moderation outcomes that can be attached to a content hash.
653#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
654#[non_exhaustive]
655pub enum ModerationAction {
656    Show,
657    Hide,
658    Warn,
659    Quarantine,
660    Restore,
661}
662
663/// The evaluated moderation decision.
664pub type ModerationDecision = ModerationAction;
665
666impl ModerationAction {
667    #[must_use]
668    pub const fn hides_content(&self) -> bool {
669        matches!(self, Self::Hide | Self::Quarantine)
670    }
671
672    #[must_use]
673    pub const fn is_visible(self) -> bool {
674        !self.hides_content()
675    }
676}
677
678/// The area in which a moderation decision applies.
679#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
680#[non_exhaustive]
681pub enum ModerationScope {
682    #[default]
683    Global,
684    Network(String),
685    Folder(NamespaceId),
686    File {
687        namespace_id: NamespaceId,
688        key: Vec<u8>,
689    },
690}
691
692impl ModerationScope {
693    #[must_use]
694    pub fn applies_to(&self, context: &ModerationContext) -> bool {
695        match self {
696            Self::Global => true,
697            Self::Network(network) => context.network.as_deref() == Some(network),
698            Self::Folder(namespace_id) => context.namespace_id == Some(*namespace_id),
699            Self::File { namespace_id, key } => {
700                context.namespace_id == Some(*namespace_id) && context.key.as_deref() == Some(key.as_slice())
701            }
702        }
703    }
704
705    #[must_use]
706    pub const fn specificity(&self) -> u8 {
707        match self {
708            Self::Global => 0,
709            Self::Network(_) => 1,
710            Self::Folder(_) => 2,
711            Self::File { .. } => 3,
712        }
713    }
714}
715
716/// Context used to evaluate scoped moderation records.
717#[derive(Clone, Debug, Eq, PartialEq)]
718#[non_exhaustive]
719pub struct ModerationContext {
720    pub content: Hash,
721    pub network: Option<String>,
722    pub namespace_id: Option<NamespaceId>,
723    pub key: Option<Vec<u8>>,
724}
725
726impl ModerationContext {
727    #[must_use]
728    pub const fn new(content: Hash) -> Self {
729        Self {
730            content,
731            network: None,
732            namespace_id: None,
733            key: None,
734        }
735    }
736
737    #[must_use]
738    pub fn in_network(mut self, network: impl Into<String>) -> Self {
739        self.network = Some(network.into());
740        self
741    }
742
743    #[must_use]
744    pub fn for_file(mut self, namespace_id: NamespaceId, key: impl AsRef<[u8]>) -> Self {
745        self.namespace_id = Some(namespace_id);
746        self.key = Some(key.as_ref().to_vec());
747        self
748    }
749}
750
751/// A signed moderation decision.
752#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
753#[non_exhaustive]
754pub struct ModerationRecord {
755    pub content: Hash,
756    pub moderator: String,
757    pub action: ModerationAction,
758    #[serde(default)]
759    pub scope: ModerationScope,
760    pub sequence: u64,
761    pub created_at: u64,
762    pub reason: String,
763    pub signature: Option<String>,
764}
765
766impl ModerationRecord {
767    /// Create and sign a moderation record.
768    ///
769    /// # Errors
770    ///
771    /// Returns an error if moderation fields are invalid.
772    pub fn new(
773        content: Hash,
774        action: ModerationAction,
775        sequence: u64,
776        reason: impl Into<String>,
777        signing_key: &SigningKey,
778    ) -> Result<Self> {
779        let mut record = Self {
780            content,
781            moderator: author_id(&signing_key.verifying_key()),
782            action,
783            scope: ModerationScope::Global,
784            sequence,
785            created_at: current_epoch_seconds(),
786            reason: reason.into(),
787            signature: None,
788        };
789        record.sign(signing_key)?;
790        Ok(record)
791    }
792
793    /// Create and sign a global moderation record with an explicit timestamp.
794    ///
795    /// # Errors
796    ///
797    /// Returns an error if moderation fields are invalid or signing fails.
798    pub fn new_with_time(
799        content: Hash,
800        action: ModerationAction,
801        sequence: u64,
802        created_at: u64,
803        reason: impl Into<String>,
804        signing_key: &SigningKey,
805    ) -> Result<Self> {
806        Self::new_scoped_with_time(
807            content,
808            ModerationScope::Global,
809            action,
810            sequence,
811            created_at,
812            reason,
813            signing_key,
814        )
815    }
816
817    /// Create and sign a moderation record for a specific scope.
818    ///
819    /// # Errors
820    ///
821    /// Returns an error if moderation fields are invalid.
822    pub fn new_scoped(
823        content: Hash,
824        scope: ModerationScope,
825        action: ModerationAction,
826        sequence: u64,
827        reason: impl Into<String>,
828        signing_key: &SigningKey,
829    ) -> Result<Self> {
830        Self::new_scoped_with_time(
831            content,
832            scope,
833            action,
834            sequence,
835            current_epoch_seconds(),
836            reason,
837            signing_key,
838        )
839    }
840
841    /// Create and sign a scoped moderation record with an explicit timestamp.
842    ///
843    /// # Errors
844    ///
845    /// Returns an error if moderation fields are invalid.
846    pub fn new_scoped_with_time(
847        content: Hash,
848        scope: ModerationScope,
849        action: ModerationAction,
850        sequence: u64,
851        created_at: u64,
852        reason: impl Into<String>,
853        signing_key: &SigningKey,
854    ) -> Result<Self> {
855        let mut record = Self {
856            content,
857            moderator: author_id(&signing_key.verifying_key()),
858            action,
859            scope,
860            sequence,
861            created_at,
862            reason: reason.into(),
863            signature: None,
864        };
865        record.sign(signing_key)?;
866        Ok(record)
867    }
868
869    /// Return a copy of this record with a different scope, re-signing it.
870    ///
871    /// # Errors
872    ///
873    /// Returns an error if the record cannot be signed.
874    pub fn with_scope(mut self, scope: ModerationScope, signing_key: &SigningKey) -> Result<Self> {
875        self.scope = scope;
876        self.sign(signing_key)?;
877        Ok(self)
878    }
879
880    /// Sign the moderation record.
881    ///
882    /// # Errors
883    ///
884    /// Returns an error if the signing key does not match the moderator.
885    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
886        ensure_signer(&self.moderator, &signing_key.verifying_key())?;
887        self.signature = Some(sign_text(MODERATION_CONTEXT, &self.unsigned_bytes()?, signing_key));
888        Ok(())
889    }
890
891    /// Verify the moderation signature.
892    ///
893    /// # Errors
894    ///
895    /// Returns an error if the record or signature is invalid.
896    pub fn verify_signature(&self) -> Result<()> {
897        self.validate()?;
898        verify_text(
899            MODERATION_CONTEXT,
900            &self.unsigned_bytes()?,
901            &self.moderator,
902            self.signature.as_deref(),
903        )
904    }
905
906    /// Validate moderation fields.
907    ///
908    /// # Errors
909    ///
910    /// Returns an error if a field is invalid.
911    pub fn validate(&self) -> Result<()> {
912        if self.sequence == 0 {
913            return Err(SyncwebError::InvalidConfig(
914                "moderation sequence must be greater than zero".to_owned(),
915            ));
916        }
917        if self.reason.trim().is_empty() {
918            return Err(SyncwebError::InvalidConfig(
919                "moderation reason cannot be empty".to_owned(),
920            ));
921        }
922        if let ModerationScope::Network(network) = &self.scope
923            && network.trim().is_empty()
924        {
925            return Err(SyncwebError::InvalidConfig(
926                "moderation network scope cannot be empty".to_owned(),
927            ));
928        }
929        if let ModerationScope::File { key, .. } = &self.scope
930            && key.is_empty()
931        {
932            return Err(SyncwebError::InvalidConfig(
933                "moderation file scope cannot be empty".to_owned(),
934            ));
935        }
936        let _moderator = parse_author(&self.moderator)?;
937        Ok(())
938    }
939
940    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
941        let mut unsigned = self.clone();
942        unsigned.signature = None;
943        signed_bytes(MODERATION_CONTEXT, &unsigned)
944    }
945}
946
947/// Types of signed provenance metadata.
948#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
949#[non_exhaustive]
950pub enum AttestationKind {
951    License,
952    Provenance,
953    Derivative,
954    Other(String),
955}
956
957impl std::fmt::Display for AttestationKind {
958    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
959        match self {
960            Self::License => write!(f, "License"),
961            Self::Provenance => write!(f, "Provenance"),
962            Self::Derivative => write!(f, "Derivative"),
963            Self::Other(value) => write!(f, "{value}"),
964        }
965    }
966}
967
968/// A signed license, provenance, or derivative attestation.
969#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
970#[non_exhaustive]
971pub struct Attestation {
972    pub content: Hash,
973    pub issuer: String,
974    pub kind: AttestationKind,
975    pub value: String,
976    pub sequence: u64,
977    pub issued_at: u64,
978    pub signature: Option<String>,
979}
980
981impl Attestation {
982    /// Create and sign an attestation.
983    ///
984    /// # Errors
985    ///
986    /// Returns an error if attestation fields are invalid.
987    pub fn new(
988        content: Hash,
989        kind: AttestationKind,
990        value: impl Into<String>,
991        sequence: u64,
992        signing_key: &SigningKey,
993    ) -> Result<Self> {
994        let mut attestation = Self {
995            content,
996            issuer: author_id(&signing_key.verifying_key()),
997            kind,
998            value: value.into(),
999            sequence,
1000            issued_at: current_epoch_seconds(),
1001            signature: None,
1002        };
1003        attestation.sign(signing_key)?;
1004        Ok(attestation)
1005    }
1006
1007    /// Sign the attestation with the declared issuer.
1008    ///
1009    /// # Errors
1010    ///
1011    /// Returns an error if the signing key does not match the issuer.
1012    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
1013        ensure_signer(&self.issuer, &signing_key.verifying_key())?;
1014        self.signature = Some(sign_text(ATTESTATION_CONTEXT, &self.unsigned_bytes()?, signing_key));
1015        Ok(())
1016    }
1017
1018    /// Verify the attestation signature.
1019    ///
1020    /// # Errors
1021    ///
1022    /// Returns an error if the attestation or signature is invalid.
1023    pub fn verify_signature(&self) -> Result<()> {
1024        self.validate()?;
1025        verify_text(
1026            ATTESTATION_CONTEXT,
1027            &self.unsigned_bytes()?,
1028            &self.issuer,
1029            self.signature.as_deref(),
1030        )
1031    }
1032
1033    /// Validate attestation fields.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns an error if a field is invalid.
1038    pub fn validate(&self) -> Result<()> {
1039        if self.sequence == 0 {
1040            return Err(SyncwebError::InvalidConfig(
1041                "attestation sequence must be greater than zero".to_owned(),
1042            ));
1043        }
1044        if self.value.trim().is_empty() {
1045            return Err(SyncwebError::InvalidConfig(
1046                "attestation value cannot be empty".to_owned(),
1047            ));
1048        }
1049        let _issuer = parse_author(&self.issuer)?;
1050        Ok(())
1051    }
1052
1053    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
1054        let mut unsigned = self.clone();
1055        unsigned.signature = None;
1056        signed_bytes(ATTESTATION_CONTEXT, &unsigned)
1057    }
1058}
1059
1060/// A manually authored reliability opinion about a provider.
1061#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1062#[non_exhaustive]
1063pub enum ProviderTrustAction {
1064    Trust,
1065    Distrust,
1066    Vouch,
1067    Warn,
1068}
1069
1070/// A signed provider reliability record in the local Web-of-Trust.
1071#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1072#[non_exhaustive]
1073pub struct ProviderTrustRecord {
1074    pub provider: PublicKey,
1075    pub action: ProviderTrustAction,
1076    pub scope: Option<Hash>,
1077    pub issuer: String,
1078    pub sequence: u64,
1079    pub issued_at: u64,
1080    pub expires_at: Option<u64>,
1081    pub reason: String,
1082    pub signature: Option<String>,
1083}
1084
1085impl ProviderTrustRecord {
1086    /// Create and sign a provider trust record using the current time.
1087    ///
1088    /// # Errors
1089    ///
1090    /// Returns an error if the record is invalid or signing fails.
1091    pub fn new(
1092        provider: PublicKey,
1093        action: ProviderTrustAction,
1094        scope: Option<Hash>,
1095        sequence: u64,
1096        expires_at: Option<u64>,
1097        reason: impl Into<String>,
1098        signing_key: &SigningKey,
1099    ) -> Result<Self> {
1100        let mut record = Self {
1101            provider,
1102            action,
1103            scope,
1104            issuer: author_id(&signing_key.verifying_key()),
1105            sequence,
1106            issued_at: current_epoch_seconds(),
1107            expires_at,
1108            reason: reason.into(),
1109            signature: None,
1110        };
1111        record.sign(signing_key)?;
1112        Ok(record)
1113    }
1114
1115    /// Create and sign a provider trust record with an explicit issue time.
1116    ///
1117    /// # Errors
1118    ///
1119    /// Returns an error if the record is invalid or signing fails.
1120    pub fn new_with_time(
1121        provider: PublicKey,
1122        action: ProviderTrustAction,
1123        scope: Option<Hash>,
1124        sequence: u64,
1125        issued_at: u64,
1126        reason: impl Into<String>,
1127        signing_key: &SigningKey,
1128    ) -> Result<Self> {
1129        let mut record = Self {
1130            provider,
1131            action,
1132            scope,
1133            issuer: author_id(&signing_key.verifying_key()),
1134            sequence,
1135            issued_at,
1136            expires_at: None,
1137            reason: reason.into(),
1138            signature: None,
1139        };
1140        record.sign(signing_key)?;
1141        Ok(record)
1142    }
1143
1144    /// Add or replace an expiration time and re-sign the record.
1145    ///
1146    /// # Errors
1147    ///
1148    /// Returns an error when the expiration is invalid or signing fails.
1149    pub fn with_expiration(mut self, expires_at: Option<u64>, signing_key: &SigningKey) -> Result<Self> {
1150        self.expires_at = expires_at;
1151        self.sign(signing_key)?;
1152        Ok(self)
1153    }
1154
1155    /// Sign the record with its declared issuer.
1156    ///
1157    /// # Errors
1158    ///
1159    /// Returns an error when the signing key does not match the issuer.
1160    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
1161        ensure_signer(&self.issuer, &signing_key.verifying_key())?;
1162        self.signature = Some(sign_text(PROVIDER_TRUST_CONTEXT, &self.unsigned_bytes()?, signing_key));
1163        Ok(())
1164    }
1165
1166    /// Verify the cryptographic signature.
1167    ///
1168    /// # Errors
1169    ///
1170    /// Returns an error when the record is malformed or unsigned.
1171    pub fn verify_signature(&self) -> Result<()> {
1172        self.validate()?;
1173        verify_text(
1174            PROVIDER_TRUST_CONTEXT,
1175            &self.unsigned_bytes()?,
1176            &self.issuer,
1177            self.signature.as_deref(),
1178        )
1179    }
1180
1181    /// Verify the signature and require the record to be active now.
1182    ///
1183    /// # Errors
1184    ///
1185    /// Returns an error when the record is malformed, unsigned, or expired.
1186    pub fn verify(&self) -> Result<()> {
1187        self.verify_at(current_epoch_seconds())
1188    }
1189
1190    /// Verify the signature and reject expired records.
1191    ///
1192    /// # Errors
1193    ///
1194    /// Returns an error when the record is malformed, unsigned, or expired.
1195    pub fn verify_at(&self, now: u64) -> Result<()> {
1196        self.verify_signature()?;
1197        if self.expires_at.is_some_and(|expires_at| expires_at <= now) {
1198            return Err(SyncwebError::InvalidConfig(
1199                "provider trust record has expired".to_owned(),
1200            ));
1201        }
1202        Ok(())
1203    }
1204
1205    /// Validate non-cryptographic record fields.
1206    ///
1207    /// # Errors
1208    ///
1209    /// Returns an error when an identity, sequence, reason, or timestamp is invalid.
1210    pub fn validate(&self) -> Result<()> {
1211        if self.sequence == 0 {
1212            return Err(SyncwebError::InvalidConfig(
1213                "provider trust record sequence must be greater than zero".to_owned(),
1214            ));
1215        }
1216        if self.reason.trim().is_empty() {
1217            return Err(SyncwebError::InvalidConfig(
1218                "provider trust record reason cannot be empty".to_owned(),
1219            ));
1220        }
1221        if self.expires_at.is_some_and(|expires_at| expires_at <= self.issued_at) {
1222            return Err(SyncwebError::InvalidConfig(
1223                "provider trust record expiration must be after issue time".to_owned(),
1224            ));
1225        }
1226        VerifyingKey::from_bytes(self.provider.as_bytes())
1227            .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid provider identity: {error}")))?;
1228        parse_author(&self.issuer)?;
1229        Ok(())
1230    }
1231
1232    /// Encode the record as JSON.
1233    ///
1234    /// # Errors
1235    ///
1236    /// Returns an error when the record is invalid or cannot be serialized.
1237    pub fn to_bytes(&self) -> Result<Vec<u8>> {
1238        self.validate()?;
1239        serde_json::to_vec(self)
1240            .map_err(|error| SyncwebError::operation("failed to serialize provider trust record", error))
1241    }
1242
1243    /// Decode a record from JSON.
1244    ///
1245    /// # Errors
1246    ///
1247    /// Returns an error when the bytes do not contain a valid record.
1248    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
1249        let record: Self = serde_json::from_slice(bytes.as_ref())
1250            .map_err(|error| SyncwebError::operation("failed to deserialize provider trust record", error))?;
1251        record.validate()?;
1252        Ok(record)
1253    }
1254
1255    fn unsigned_bytes(&self) -> Result<Vec<u8>> {
1256        let mut unsigned = self.clone();
1257        unsigned.signature = None;
1258        signed_bytes(PROVIDER_TRUST_CONTEXT, &unsigned)
1259    }
1260}
1261
1262/// Result of evaluating manual provider trust records.
1263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1264#[non_exhaustive]
1265pub enum ProviderTrustDecision {
1266    Trusted,
1267    Distrusted,
1268    Unknown,
1269    Conflicting,
1270}
1271
1272impl ProviderTrustDecision {
1273    #[must_use]
1274    pub const fn is_trusted(self) -> bool {
1275        matches!(self, Self::Trusted)
1276    }
1277
1278    #[must_use]
1279    pub const fn is_distrusted(self) -> bool {
1280        matches!(self, Self::Distrusted)
1281    }
1282}
1283
1284#[derive(Clone, Debug, Default)]
1285struct WotState {
1286    revocations: HashMap<Hash, RevocationRecord>,
1287    moderation: HashMap<Hash, Vec<ModerationRecord>>,
1288    attestations: HashMap<(Hash, String, u64), Attestation>,
1289    provider_trust: HashMap<PublicKey, Vec<ProviderTrustRecord>>,
1290}
1291
1292/// Indexing-layer service for trusted metadata and local governance records.
1293#[derive(Clone)]
1294pub struct WotService {
1295    database: IndexingDatabase,
1296    policy: Arc<Mutex<TrustPolicy>>,
1297    state: Arc<Mutex<WotState>>,
1298}
1299
1300impl std::fmt::Debug for WotService {
1301    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1302        formatter.debug_struct("WotService").finish_non_exhaustive()
1303    }
1304}
1305
1306impl WotService {
1307    /// Create a Web-of-Trust service using an existing indexing service.
1308    #[must_use]
1309    pub fn new(indexing: &IndexingService, policy: TrustPolicy) -> Self {
1310        Self::with_database(indexing.database().clone(), policy)
1311    }
1312
1313    /// Create a service backed by an indexing database.
1314    #[must_use]
1315    pub fn with_database(database: IndexingDatabase, policy: TrustPolicy) -> Self {
1316        Self {
1317            database,
1318            policy: Arc::new(Mutex::new(policy)),
1319            state: Arc::new(Mutex::new(WotState::default())),
1320        }
1321    }
1322
1323    /// Create an isolated in-memory Web-of-Trust service.
1324    ///
1325    /// # Errors
1326    ///
1327    /// Returns an error if `SQLite` cannot initialize.
1328    pub fn in_memory(policy: TrustPolicy) -> Result<Self> {
1329        Ok(Self::with_database(IndexingDatabase::in_memory()?, policy))
1330    }
1331
1332    #[must_use]
1333    pub const fn database(&self) -> &IndexingDatabase {
1334        &self.database
1335    }
1336
1337    /// Return a copy of the current local trust policy.
1338    ///
1339    /// # Errors
1340    ///
1341    /// Returns an error if policy state is poisoned.
1342    pub fn policy(&self) -> Result<TrustPolicy> {
1343        Ok(self
1344            .policy
1345            .lock()
1346            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1347            .clone())
1348    }
1349
1350    /// Add a local trusted root.
1351    ///
1352    /// # Errors
1353    ///
1354    /// Returns an error if policy state is poisoned.
1355    pub fn trust_author(&self, author: &VerifyingKey) -> Result<()> {
1356        self.policy
1357            .lock()
1358            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1359            .trust_author(author);
1360        Ok(())
1361    }
1362
1363    /// Add a signed delegation to the local policy.
1364    ///
1365    /// # Errors
1366    ///
1367    /// Returns an error if the delegation is invalid or its issuer is not
1368    /// locally trusted.
1369    pub fn add_delegation(&self, delegation: TrustDelegation) -> Result<bool> {
1370        self.add_delegation_at(delegation, current_epoch_seconds())
1371    }
1372
1373    /// Add a signed delegation using an explicit validation time.
1374    ///
1375    /// # Errors
1376    ///
1377    /// Returns an error if the delegation is invalid or its issuer is not
1378    /// locally trusted.
1379    pub fn add_delegation_at(&self, delegation: TrustDelegation, now: u64) -> Result<bool> {
1380        self.policy
1381            .lock()
1382            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1383            .add_delegation_at(delegation, now)
1384    }
1385
1386    /// Revoke a delegation matching the delegate, scope, and delegator.
1387    ///
1388    /// Returns `true` if any delegation was marked revoked.
1389    ///
1390    /// # Errors
1391    ///
1392    /// Returns an error if the policy lock is poisoned.
1393    pub fn revoke_delegation(&self, delegator: &str, delegate: &str, scope: Option<&Hash>) -> Result<bool> {
1394        self.revoke_delegation_at(delegator, delegate, scope, current_epoch_seconds())
1395    }
1396
1397    /// Revoke a delegation using an explicit revocation time.
1398    ///
1399    /// Returns `true` if any delegation was marked revoked.
1400    ///
1401    /// # Errors
1402    ///
1403    /// Returns an error if the policy lock is poisoned.
1404    pub fn revoke_delegation_at(&self, delegator: &str, delegate: &str, scope: Option<&Hash>, at: u64) -> Result<bool> {
1405        self.policy
1406            .lock()
1407            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1408            .revoke_delegation_at(delegator, delegate, scope, at)
1409    }
1410
1411    /// Apply a signed manual provider trust record from a trusted issuer.
1412    ///
1413    /// A provider may always publish a signed self-distrust, which allows a
1414    /// compromised or retiring provider to withdraw its own availability.
1415    ///
1416    /// # Errors
1417    ///
1418    /// Returns an error when the record is invalid or its issuer is untrusted.
1419    pub fn apply_provider_trust(&self, record: ProviderTrustRecord) -> Result<bool> {
1420        let now = current_epoch_seconds();
1421        record.verify_at(now)?;
1422        let issuer_key = parse_author(&record.issuer)?;
1423        let is_self_distrust =
1424            issuer_key.as_bytes() == record.provider.as_bytes() && record.action == ProviderTrustAction::Distrust;
1425        if !is_self_distrust
1426            && !self
1427                .policy()?
1428                .is_trusted_for_at(&record.issuer, record.scope.as_ref(), now)
1429        {
1430            return Err(SyncwebError::InvalidIdentity(
1431                "provider trust record issuer is not trusted for its scope".to_owned(),
1432            ));
1433        }
1434        let mut state = self
1435            .state
1436            .lock()
1437            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1438        let records = state.provider_trust.entry(record.provider).or_default();
1439        if records.iter().any(|existing| {
1440            existing.issuer == record.issuer && existing.scope == record.scope && existing.sequence >= record.sequence
1441        }) {
1442            return Ok(false);
1443        }
1444        records.retain(|existing| {
1445            !(existing.issuer == record.issuer && existing.scope == record.scope && existing.sequence < record.sequence)
1446        });
1447        records.push(record);
1448        drop(state);
1449        Ok(true)
1450    }
1451
1452    /// Evaluate the newest applicable provider trust records.
1453    /// # Errors
1454    ///
1455    /// Returns an error when the `WoT` state lock is poisoned.
1456    pub fn evaluate_provider_trust(
1457        &self,
1458        provider: PublicKey,
1459        hash: Option<&Hash>,
1460        now: u64,
1461    ) -> Result<ProviderTrustDecision> {
1462        let records = self
1463            .state
1464            .lock()
1465            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1466            .provider_trust
1467            .get(&provider)
1468            .cloned()
1469            .unwrap_or_default();
1470        let maybe_latest_sequence = records
1471            .iter()
1472            .filter(|record| {
1473                record.expires_at.is_none_or(|expires_at| expires_at > now)
1474                    && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1475            })
1476            .map(|record| record.sequence)
1477            .max();
1478        let Some(latest_sequence) = maybe_latest_sequence else {
1479            return Ok(ProviderTrustDecision::Unknown);
1480        };
1481        let latest = records
1482            .iter()
1483            .filter(|record| {
1484                record.sequence == latest_sequence
1485                    && record.expires_at.is_none_or(|expires_at| expires_at > now)
1486                    && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1487            })
1488            .collect::<Vec<_>>();
1489        let trusted = latest
1490            .iter()
1491            .any(|record| matches!(record.action, ProviderTrustAction::Trust | ProviderTrustAction::Vouch));
1492        let distrusted = latest
1493            .iter()
1494            .any(|record| matches!(record.action, ProviderTrustAction::Distrust));
1495        Ok(match (trusted, distrusted) {
1496            (true, true) => ProviderTrustDecision::Conflicting,
1497            (true, false) => ProviderTrustDecision::Trusted,
1498            (false, true) => ProviderTrustDecision::Distrusted,
1499            (false, false) => ProviderTrustDecision::Unknown,
1500        })
1501    }
1502
1503    /// Evaluate a provider trust decision for a concrete content hash.
1504    ///
1505    /// # Errors
1506    ///
1507    /// Returns an error when the `WoT` state lock is poisoned.
1508    pub fn evaluate_provider_trust_for_hash(
1509        &self,
1510        provider: PublicKey,
1511        hash: Hash,
1512        now: u64,
1513    ) -> Result<ProviderTrustDecision> {
1514        self.evaluate_provider_trust(provider, Some(&hash), now)
1515    }
1516
1517    /// Return all records for a provider, newest first.
1518    ///
1519    /// # Errors
1520    ///
1521    /// Returns an error when the `WoT` state lock is poisoned.
1522    pub fn provider_trust_records(&self, provider: PublicKey) -> Result<Vec<ProviderTrustRecord>> {
1523        let mut records = self
1524            .state
1525            .lock()
1526            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1527            .provider_trust
1528            .get(&provider)
1529            .cloned()
1530            .unwrap_or_default();
1531        records.sort_by(|left, right| {
1532            right
1533                .sequence
1534                .cmp(&left.sequence)
1535                .then_with(|| right.issued_at.cmp(&left.issued_at))
1536        });
1537        Ok(records)
1538    }
1539
1540    /// Append metadata after signature and local trust checks.
1541    ///
1542    /// # Errors
1543    ///
1544    /// Returns an error if the metadata is not signed, trusted, or the content
1545    /// has been self-revoked.
1546    pub fn append_metadata(&self, entry: &MetadataEntry) -> Result<bool> {
1547        entry.verify_signature()?;
1548        let now = current_epoch_seconds();
1549        let policy = self
1550            .policy
1551            .lock()
1552            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?;
1553        if !policy.is_trusted_for_at(&entry.author, Some(&entry.content), now) {
1554            return Err(SyncwebError::InvalidIdentity(
1555                "metadata author is not trusted for this content".to_owned(),
1556            ));
1557        }
1558        drop(policy);
1559        if self.is_revoked(&entry.content)? {
1560            return Err(SyncwebError::InvalidConfig(
1561                "metadata cannot be appended to self-revoked content".to_owned(),
1562            ));
1563        }
1564        self.database.append_wot_metadata(entry)
1565    }
1566
1567    /// Search trusted metadata while applying local revocation and moderation.
1568    ///
1569    /// # Errors
1570    ///
1571    /// Returns an error if the query or index is invalid.
1572    pub fn search_metadata(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1573        let records = self.database.search_wot_metadata(query, limit)?;
1574        let state = self
1575            .state
1576            .lock()
1577            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1578        Ok(records
1579            .into_iter()
1580            .filter(|entry| !state.revocations.contains_key(&entry.content))
1581            .filter(|entry| {
1582                state.moderation.get(&entry.content).is_none_or(|moderation_records| {
1583                    moderation_records
1584                        .iter()
1585                        .filter(|record| matches!(&record.scope, ModerationScope::Global))
1586                        .max_by_key(|record| record.sequence)
1587                        .is_none_or(|record| !record.action.hides_content())
1588                })
1589            })
1590            .collect())
1591    }
1592
1593    /// Alias for [`Self::search_metadata`].
1594    ///
1595    /// # Errors
1596    ///
1597    /// Returns an error if the query or index is invalid.
1598    pub fn search(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1599        self.search_metadata(query, limit)
1600    }
1601
1602    /// Accept a publisher's self-revocation.
1603    ///
1604    /// # Errors
1605    ///
1606    /// Returns an error if the signature is invalid or an older sequence was
1607    /// already recorded.
1608    pub fn revoke_self(&self, record: RevocationRecord) -> Result<bool> {
1609        record.verify_signature()?;
1610        let mut state = self
1611            .state
1612            .lock()
1613            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1614        if state
1615            .revocations
1616            .get(&record.content)
1617            .is_some_and(|existing| existing.sequence >= record.sequence)
1618        {
1619            return Ok(false);
1620        }
1621        state.revocations.insert(record.content, record);
1622        drop(state);
1623        Ok(true)
1624    }
1625
1626    /// Return whether a content hash has a valid self-revocation.
1627    ///
1628    /// # Errors
1629    ///
1630    /// Returns an error if state is poisoned.
1631    pub fn is_revoked(&self, content: &Hash) -> Result<bool> {
1632        Ok(self
1633            .state
1634            .lock()
1635            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1636            .revocations
1637            .contains_key(content))
1638    }
1639
1640    /// Accept a trusted moderation record.
1641    ///
1642    /// # Errors
1643    ///
1644    /// Returns an error if the signature is invalid, the moderator is not
1645    /// trusted, or an older sequence was already recorded.
1646    pub fn apply_moderation(&self, record: ModerationRecord) -> Result<bool> {
1647        record.verify_signature()?;
1648        let now = current_epoch_seconds();
1649        if !self
1650            .policy()?
1651            .is_trusted_for_at(&record.moderator, Some(&record.content), now)
1652        {
1653            return Err(SyncwebError::InvalidIdentity(
1654                "moderator is not trusted for this content".to_owned(),
1655            ));
1656        }
1657        let mut state = self
1658            .state
1659            .lock()
1660            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1661        let records = state.moderation.entry(record.content).or_default();
1662        if records
1663            .iter()
1664            .filter(|existing| existing.scope == record.scope)
1665            .any(|existing| existing.sequence >= record.sequence)
1666        {
1667            return Ok(false);
1668        }
1669        records.push(record);
1670        drop(state);
1671        Ok(true)
1672    }
1673
1674    /// Return the latest moderation record for a content hash.
1675    ///
1676    /// # Errors
1677    ///
1678    /// Returns an error if state is poisoned.
1679    pub fn moderation(&self, content: &Hash) -> Result<Option<ModerationRecord>> {
1680        Ok(self
1681            .state
1682            .lock()
1683            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1684            .moderation
1685            .get(content)
1686            .and_then(|records| {
1687                records
1688                    .iter()
1689                    .max_by_key(|record| (record.sequence, record.scope.specificity()))
1690                    .cloned()
1691            }))
1692    }
1693
1694    /// Return all moderation records for a content hash, newest first.
1695    ///
1696    /// # Errors
1697    ///
1698    /// Returns an error if state is poisoned.
1699    pub fn moderation_records(&self, content: &Hash) -> Result<Vec<ModerationRecord>> {
1700        let mut records = self
1701            .state
1702            .lock()
1703            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1704            .moderation
1705            .get(content)
1706            .cloned()
1707            .unwrap_or_default();
1708        records.sort_by(|left, right| {
1709            right
1710                .sequence
1711                .cmp(&left.sequence)
1712                .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1713        });
1714        Ok(records)
1715    }
1716
1717    /// List all moderation records, optionally restricted to one content hash.
1718    ///
1719    /// # Errors
1720    ///
1721    /// Returns an error if state is poisoned.
1722    pub fn list_moderation(&self, content: Option<&Hash>) -> Result<Vec<ModerationRecord>> {
1723        let state = self
1724            .state
1725            .lock()
1726            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1727        let mut records = state
1728            .moderation
1729            .iter()
1730            .filter(|(hash, _)| content.is_none_or(|requested| requested == *hash))
1731            .flat_map(|(_, records)| records.iter().cloned())
1732            .collect::<Vec<_>>();
1733        drop(state);
1734        records.sort_by(|left, right| {
1735            right
1736                .sequence
1737                .cmp(&left.sequence)
1738                .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1739        });
1740        Ok(records)
1741    }
1742
1743    /// Evaluate the latest applicable moderation decision for a context.
1744    ///
1745    /// More recent decisions win; when sequence numbers tie, the more
1746    /// specific scope wins.
1747    ///
1748    /// # Errors
1749    ///
1750    /// Returns an error if state is poisoned.
1751    pub fn moderation_decision(&self, context: &ModerationContext) -> Result<ModerationAction> {
1752        let selected_record = self
1753            .state
1754            .lock()
1755            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1756            .moderation
1757            .get(&context.content)
1758            .and_then(|records| {
1759                records
1760                    .iter()
1761                    .filter(|record| record.scope.applies_to(context))
1762                    .max_by_key(|record| (record.sequence, record.scope.specificity()))
1763                    .cloned()
1764            });
1765        Ok(selected_record.map_or(ModerationAction::Show, |selected| selected.action))
1766    }
1767
1768    /// Alias for [`Self::moderation_decision`].
1769    ///
1770    /// # Errors
1771    ///
1772    /// Returns an error if state is poisoned.
1773    pub fn evaluate_moderation(&self, context: &ModerationContext) -> Result<ModerationAction> {
1774        self.moderation_decision(context)
1775    }
1776
1777    /// Store an attestation after cryptographic and local trust checks.
1778    ///
1779    /// # Errors
1780    ///
1781    /// Returns an error if the attestation is invalid or its issuer is not
1782    /// trusted.
1783    pub fn append_attestation(&self, attestation: Attestation) -> Result<bool> {
1784        attestation.verify_signature()?;
1785        let now = current_epoch_seconds();
1786        if !self
1787            .policy()?
1788            .is_trusted_for_at(&attestation.issuer, Some(&attestation.content), now)
1789        {
1790            return Err(SyncwebError::InvalidIdentity(
1791                "attestation issuer is not trusted for this content".to_owned(),
1792            ));
1793        }
1794        let mut state = self
1795            .state
1796            .lock()
1797            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1798        let key = (attestation.content, attestation.issuer.clone(), attestation.sequence);
1799        if state.attestations.contains_key(&key) {
1800            return Ok(false);
1801        }
1802        state.attestations.insert(key, attestation);
1803        drop(state);
1804        Ok(true)
1805    }
1806
1807    /// Verify an attestation's cryptographic signature without applying local
1808    /// trust policy.
1809    ///
1810    /// # Errors
1811    ///
1812    /// Returns an error if the attestation is malformed or tampered with.
1813    pub fn verify_attestation(&self, attestation: &Attestation) -> Result<()> {
1814        attestation.verify_signature()
1815    }
1816}
1817
1818fn signed_bytes<T: Serialize>(context: &[u8], value: &T) -> Result<Vec<u8>> {
1819    let encoded = serde_json::to_vec(value)
1820        .map_err(|error| SyncwebError::operation("failed to serialize signed Web-of-Trust record", error))?;
1821    let mut bytes = Vec::with_capacity(context.len().saturating_add(encoded.len()));
1822    bytes.extend_from_slice(context);
1823    bytes.extend_from_slice(&encoded);
1824    Ok(bytes)
1825}
1826
1827fn sign_text(context: &[u8], unsigned: &[u8], signing_key: &SigningKey) -> String {
1828    let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1829    bytes.extend_from_slice(context);
1830    bytes.extend_from_slice(unsigned);
1831    hex::encode(signing_key.sign(&bytes).to_bytes())
1832}
1833
1834fn verify_text(context: &[u8], unsigned: &[u8], author: &str, signature: Option<&str>) -> Result<()> {
1835    let signature_text = signature
1836        .ok_or_else(|| SyncwebError::InvalidConfig("signed Web-of-Trust record has no signature".to_owned()))?;
1837    let signature_bytes = hex::decode(signature_text)
1838        .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature encoding: {error}")))?;
1839    let parsed_signature = Signature::from_slice(&signature_bytes)
1840        .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature: {error}")))?;
1841    let verifying_key = parse_author(author)?;
1842    let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1843    bytes.extend_from_slice(context);
1844    bytes.extend_from_slice(unsigned);
1845    verifying_key
1846        .verify(&bytes, &parsed_signature)
1847        .map_err(|error| SyncwebError::InvalidConfig(format!("Web-of-Trust signature is invalid: {error}")))
1848}
1849
1850fn ensure_signer(author: &str, signing_key: &VerifyingKey) -> Result<()> {
1851    if author == author_id(signing_key) {
1852        Ok(())
1853    } else {
1854        Err(SyncwebError::InvalidIdentity(
1855            "Web-of-Trust signer does not match the declared author".to_owned(),
1856        ))
1857    }
1858}
1859
1860fn author_id(author: &VerifyingKey) -> String {
1861    hex::encode(author.to_bytes())
1862}
1863
1864fn parse_author(author: &str) -> Result<VerifyingKey> {
1865    let bytes = hex::decode(author)
1866        .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author encoding: {error}")))?;
1867    let key_bytes: [u8; 32] = bytes.try_into().map_err(|error: Vec<u8>| {
1868        SyncwebError::InvalidIdentity(format!("Web-of-Trust author must be 32 bytes, got {}", error.len()))
1869    })?;
1870    VerifyingKey::from_bytes(&key_bytes)
1871        .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author key: {error}")))
1872}
1873
1874impl SignedGossipMessage for Attestation {
1875    fn verify_signature(&self) -> Result<()> {
1876        self.verify_signature()
1877    }
1878}
1879
1880/// Deterministic gossip topic for broadcasting attestations.
1881pub const ATTESTATION_GOSSIP_TOPIC: &[u8] = b"syncweb/attestations/v1";
1882
1883fn current_epoch_seconds() -> u64 {
1884    SystemTime::now()
1885        .duration_since(UNIX_EPOCH)
1886        .map_or(0, |duration| duration.as_secs())
1887}