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        let mut state = WotState::default();
1317        if let Ok(records) = database.load_provider_trust_records() {
1318            let now = current_epoch_seconds();
1319            for record in records {
1320                if record.expires_at.is_some_and(|e| e <= now) {
1321                    continue;
1322                }
1323                if record.verify_signature().is_err() {
1324                    tracing::warn!("skipping provider trust record with invalid signature on load");
1325                    continue;
1326                }
1327                state.provider_trust.entry(record.provider).or_default().push(record);
1328            }
1329        }
1330        Self {
1331            database,
1332            policy: Arc::new(Mutex::new(policy)),
1333            state: Arc::new(Mutex::new(state)),
1334        }
1335    }
1336
1337    /// Create an isolated in-memory Web-of-Trust service.
1338    ///
1339    /// # Errors
1340    ///
1341    /// Returns an error if `SQLite` cannot initialize.
1342    pub fn in_memory(policy: TrustPolicy) -> Result<Self> {
1343        Ok(Self::with_database(IndexingDatabase::in_memory()?, policy))
1344    }
1345
1346    #[must_use]
1347    pub const fn database(&self) -> &IndexingDatabase {
1348        &self.database
1349    }
1350
1351    /// Return a copy of the current local trust policy.
1352    ///
1353    /// # Errors
1354    ///
1355    /// Returns an error if policy state is poisoned.
1356    pub fn policy(&self) -> Result<TrustPolicy> {
1357        Ok(self
1358            .policy
1359            .lock()
1360            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1361            .clone())
1362    }
1363
1364    /// Add a local trusted root.
1365    ///
1366    /// # Errors
1367    ///
1368    /// Returns an error if policy state is poisoned.
1369    pub fn trust_author(&self, author: &VerifyingKey) -> Result<()> {
1370        self.policy
1371            .lock()
1372            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1373            .trust_author(author);
1374        Ok(())
1375    }
1376
1377    /// Add a signed delegation to the local policy.
1378    ///
1379    /// # Errors
1380    ///
1381    /// Returns an error if the delegation is invalid or its issuer is not
1382    /// locally trusted.
1383    pub fn add_delegation(&self, delegation: TrustDelegation) -> Result<bool> {
1384        self.add_delegation_at(delegation, current_epoch_seconds())
1385    }
1386
1387    /// Add a signed delegation using an explicit validation time.
1388    ///
1389    /// # Errors
1390    ///
1391    /// Returns an error if the delegation is invalid or its issuer is not
1392    /// locally trusted.
1393    pub fn add_delegation_at(&self, delegation: TrustDelegation, now: u64) -> Result<bool> {
1394        self.policy
1395            .lock()
1396            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1397            .add_delegation_at(delegation, now)
1398    }
1399
1400    /// Revoke a delegation matching the delegate, scope, and delegator.
1401    ///
1402    /// Returns `true` if any delegation was marked revoked.
1403    ///
1404    /// # Errors
1405    ///
1406    /// Returns an error if the policy lock is poisoned.
1407    pub fn revoke_delegation(&self, delegator: &str, delegate: &str, scope: Option<&Hash>) -> Result<bool> {
1408        self.revoke_delegation_at(delegator, delegate, scope, current_epoch_seconds())
1409    }
1410
1411    /// Revoke a delegation using an explicit revocation time.
1412    ///
1413    /// Returns `true` if any delegation was marked revoked.
1414    ///
1415    /// # Errors
1416    ///
1417    /// Returns an error if the policy lock is poisoned.
1418    pub fn revoke_delegation_at(&self, delegator: &str, delegate: &str, scope: Option<&Hash>, at: u64) -> Result<bool> {
1419        self.policy
1420            .lock()
1421            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1422            .revoke_delegation_at(delegator, delegate, scope, at)
1423    }
1424
1425    /// Apply a signed manual provider trust record from a trusted issuer.
1426    ///
1427    /// A provider may always publish a signed self-distrust, which allows a
1428    /// compromised or retiring provider to withdraw its own availability.
1429    ///
1430    /// # Errors
1431    ///
1432    /// Returns an error when the record is invalid or its issuer is untrusted.
1433    pub fn apply_provider_trust(&self, record: ProviderTrustRecord) -> Result<bool> {
1434        let now = current_epoch_seconds();
1435        record.verify_at(now)?;
1436        let issuer_key = parse_author(&record.issuer)?;
1437        let is_self_distrust =
1438            issuer_key.as_bytes() == record.provider.as_bytes() && record.action == ProviderTrustAction::Distrust;
1439        if !is_self_distrust
1440            && !self
1441                .policy()?
1442                .is_trusted_for_at(&record.issuer, record.scope.as_ref(), now)
1443        {
1444            return Err(SyncwebError::InvalidIdentity(
1445                "provider trust record issuer is not trusted for its scope".to_owned(),
1446            ));
1447        }
1448        let all_records = {
1449            let mut state = self
1450                .state
1451                .lock()
1452                .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1453            let records = state.provider_trust.entry(record.provider).or_default();
1454            if records.iter().any(|existing| {
1455                existing.issuer == record.issuer
1456                    && existing.scope == record.scope
1457                    && existing.sequence >= record.sequence
1458            }) {
1459                return Ok(false);
1460            }
1461            records.retain(|existing| {
1462                !(existing.issuer == record.issuer
1463                    && existing.scope == record.scope
1464                    && existing.sequence < record.sequence)
1465            });
1466            records.push(record);
1467            state
1468                .provider_trust
1469                .values()
1470                .flat_map(|v| v.iter().cloned())
1471                .collect::<Vec<_>>()
1472        };
1473        self.database.save_provider_trust_records(&all_records)?;
1474        Ok(true)
1475    }
1476
1477    /// Evaluate the newest applicable provider trust records.
1478    /// # Errors
1479    ///
1480    /// Returns an error when the `WoT` state lock is poisoned.
1481    pub fn evaluate_provider_trust(
1482        &self,
1483        provider: PublicKey,
1484        hash: Option<&Hash>,
1485        now: u64,
1486    ) -> Result<ProviderTrustDecision> {
1487        let records = self
1488            .state
1489            .lock()
1490            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1491            .provider_trust
1492            .get(&provider)
1493            .cloned()
1494            .unwrap_or_default();
1495        let maybe_latest_sequence = records
1496            .iter()
1497            .filter(|record| {
1498                record.expires_at.is_none_or(|expires_at| expires_at > now)
1499                    && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1500            })
1501            .map(|record| record.sequence)
1502            .max();
1503        let Some(latest_sequence) = maybe_latest_sequence else {
1504            return Ok(ProviderTrustDecision::Unknown);
1505        };
1506        let latest = records
1507            .iter()
1508            .filter(|record| {
1509                record.sequence == latest_sequence
1510                    && record.expires_at.is_none_or(|expires_at| expires_at > now)
1511                    && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1512            })
1513            .collect::<Vec<_>>();
1514        let trusted = latest
1515            .iter()
1516            .any(|record| matches!(record.action, ProviderTrustAction::Trust | ProviderTrustAction::Vouch));
1517        let distrusted = latest
1518            .iter()
1519            .any(|record| matches!(record.action, ProviderTrustAction::Distrust));
1520        Ok(match (trusted, distrusted) {
1521            (true, true) => ProviderTrustDecision::Conflicting,
1522            (true, false) => ProviderTrustDecision::Trusted,
1523            (false, true) => ProviderTrustDecision::Distrusted,
1524            (false, false) => ProviderTrustDecision::Unknown,
1525        })
1526    }
1527
1528    /// Evaluate a provider trust decision for a concrete content hash.
1529    ///
1530    /// # Errors
1531    ///
1532    /// Returns an error when the `WoT` state lock is poisoned.
1533    pub fn evaluate_provider_trust_for_hash(
1534        &self,
1535        provider: PublicKey,
1536        hash: Hash,
1537        now: u64,
1538    ) -> Result<ProviderTrustDecision> {
1539        self.evaluate_provider_trust(provider, Some(&hash), now)
1540    }
1541
1542    /// Return all records for a provider, newest first.
1543    ///
1544    /// # Errors
1545    ///
1546    /// Returns an error when the `WoT` state lock is poisoned.
1547    pub fn provider_trust_records(&self, provider: PublicKey) -> Result<Vec<ProviderTrustRecord>> {
1548        let mut records = self
1549            .state
1550            .lock()
1551            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1552            .provider_trust
1553            .get(&provider)
1554            .cloned()
1555            .unwrap_or_default();
1556        records.sort_by(|left, right| {
1557            right
1558                .sequence
1559                .cmp(&left.sequence)
1560                .then_with(|| right.issued_at.cmp(&left.issued_at))
1561        });
1562        Ok(records)
1563    }
1564
1565    /// Append metadata after signature and local trust checks.
1566    ///
1567    /// # Errors
1568    ///
1569    /// Returns an error if the metadata is not signed, trusted, or the content
1570    /// has been self-revoked.
1571    pub fn append_metadata(&self, entry: &MetadataEntry) -> Result<bool> {
1572        entry.verify_signature()?;
1573        let now = current_epoch_seconds();
1574        let policy = self
1575            .policy
1576            .lock()
1577            .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?;
1578        if !policy.is_trusted_for_at(&entry.author, Some(&entry.content), now) {
1579            return Err(SyncwebError::InvalidIdentity(
1580                "metadata author is not trusted for this content".to_owned(),
1581            ));
1582        }
1583        drop(policy);
1584        if self.is_revoked(&entry.content)? {
1585            return Err(SyncwebError::InvalidConfig(
1586                "metadata cannot be appended to self-revoked content".to_owned(),
1587            ));
1588        }
1589        self.database.append_wot_metadata(entry)
1590    }
1591
1592    /// Search trusted metadata while applying local revocation and moderation.
1593    ///
1594    /// # Errors
1595    ///
1596    /// Returns an error if the query or index is invalid.
1597    pub fn search_metadata(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1598        let records = self.database.search_wot_metadata(query, limit)?;
1599        let state = self
1600            .state
1601            .lock()
1602            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1603        Ok(records
1604            .into_iter()
1605            .filter(|entry| !state.revocations.contains_key(&entry.content))
1606            .filter(|entry| {
1607                state.moderation.get(&entry.content).is_none_or(|moderation_records| {
1608                    moderation_records
1609                        .iter()
1610                        .filter(|record| matches!(&record.scope, ModerationScope::Global))
1611                        .max_by_key(|record| record.sequence)
1612                        .is_none_or(|record| !record.action.hides_content())
1613                })
1614            })
1615            .collect())
1616    }
1617
1618    /// Alias for [`Self::search_metadata`].
1619    ///
1620    /// # Errors
1621    ///
1622    /// Returns an error if the query or index is invalid.
1623    pub fn search(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1624        self.search_metadata(query, limit)
1625    }
1626
1627    /// Accept a publisher's self-revocation.
1628    ///
1629    /// # Errors
1630    ///
1631    /// Returns an error if the signature is invalid or an older sequence was
1632    /// already recorded.
1633    pub fn revoke_self(&self, record: RevocationRecord) -> Result<bool> {
1634        record.verify_signature()?;
1635        let mut state = self
1636            .state
1637            .lock()
1638            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1639        if state
1640            .revocations
1641            .get(&record.content)
1642            .is_some_and(|existing| existing.sequence >= record.sequence)
1643        {
1644            return Ok(false);
1645        }
1646        state.revocations.insert(record.content, record);
1647        drop(state);
1648        Ok(true)
1649    }
1650
1651    /// Return whether a content hash has a valid self-revocation.
1652    ///
1653    /// # Errors
1654    ///
1655    /// Returns an error if state is poisoned.
1656    pub fn is_revoked(&self, content: &Hash) -> Result<bool> {
1657        Ok(self
1658            .state
1659            .lock()
1660            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1661            .revocations
1662            .contains_key(content))
1663    }
1664
1665    /// Accept a trusted moderation record.
1666    ///
1667    /// # Errors
1668    ///
1669    /// Returns an error if the signature is invalid, the moderator is not
1670    /// trusted, or an older sequence was already recorded.
1671    pub fn apply_moderation(&self, record: ModerationRecord) -> Result<bool> {
1672        record.verify_signature()?;
1673        let now = current_epoch_seconds();
1674        if !self
1675            .policy()?
1676            .is_trusted_for_at(&record.moderator, Some(&record.content), now)
1677        {
1678            return Err(SyncwebError::InvalidIdentity(
1679                "moderator is not trusted for this content".to_owned(),
1680            ));
1681        }
1682        let mut state = self
1683            .state
1684            .lock()
1685            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1686        let records = state.moderation.entry(record.content).or_default();
1687        if records
1688            .iter()
1689            .filter(|existing| existing.scope == record.scope)
1690            .any(|existing| existing.sequence >= record.sequence)
1691        {
1692            return Ok(false);
1693        }
1694        records.push(record);
1695        drop(state);
1696        Ok(true)
1697    }
1698
1699    /// Return the latest moderation record for a content hash.
1700    ///
1701    /// # Errors
1702    ///
1703    /// Returns an error if state is poisoned.
1704    pub fn moderation(&self, content: &Hash) -> Result<Option<ModerationRecord>> {
1705        Ok(self
1706            .state
1707            .lock()
1708            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1709            .moderation
1710            .get(content)
1711            .and_then(|records| {
1712                records
1713                    .iter()
1714                    .max_by_key(|record| (record.sequence, record.scope.specificity()))
1715                    .cloned()
1716            }))
1717    }
1718
1719    /// Return all moderation records for a content hash, newest first.
1720    ///
1721    /// # Errors
1722    ///
1723    /// Returns an error if state is poisoned.
1724    pub fn moderation_records(&self, content: &Hash) -> Result<Vec<ModerationRecord>> {
1725        let mut records = self
1726            .state
1727            .lock()
1728            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1729            .moderation
1730            .get(content)
1731            .cloned()
1732            .unwrap_or_default();
1733        records.sort_by(|left, right| {
1734            right
1735                .sequence
1736                .cmp(&left.sequence)
1737                .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1738        });
1739        Ok(records)
1740    }
1741
1742    /// List all moderation records, optionally restricted to one content hash.
1743    ///
1744    /// # Errors
1745    ///
1746    /// Returns an error if state is poisoned.
1747    pub fn list_moderation(&self, content: Option<&Hash>) -> Result<Vec<ModerationRecord>> {
1748        let state = self
1749            .state
1750            .lock()
1751            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1752        let mut records = state
1753            .moderation
1754            .iter()
1755            .filter(|(hash, _)| content.is_none_or(|requested| requested == *hash))
1756            .flat_map(|(_, records)| records.iter().cloned())
1757            .collect::<Vec<_>>();
1758        drop(state);
1759        records.sort_by(|left, right| {
1760            right
1761                .sequence
1762                .cmp(&left.sequence)
1763                .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1764        });
1765        Ok(records)
1766    }
1767
1768    /// Evaluate the latest applicable moderation decision for a context.
1769    ///
1770    /// More recent decisions win; when sequence numbers tie, the more
1771    /// specific scope wins.
1772    ///
1773    /// # Errors
1774    ///
1775    /// Returns an error if state is poisoned.
1776    pub fn moderation_decision(&self, context: &ModerationContext) -> Result<ModerationAction> {
1777        let selected_record = self
1778            .state
1779            .lock()
1780            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1781            .moderation
1782            .get(&context.content)
1783            .and_then(|records| {
1784                records
1785                    .iter()
1786                    .filter(|record| record.scope.applies_to(context))
1787                    .max_by_key(|record| (record.sequence, record.scope.specificity()))
1788                    .cloned()
1789            });
1790        Ok(selected_record.map_or(ModerationAction::Show, |selected| selected.action))
1791    }
1792
1793    /// Alias for [`Self::moderation_decision`].
1794    ///
1795    /// # Errors
1796    ///
1797    /// Returns an error if state is poisoned.
1798    pub fn evaluate_moderation(&self, context: &ModerationContext) -> Result<ModerationAction> {
1799        self.moderation_decision(context)
1800    }
1801
1802    /// Store an attestation after cryptographic and local trust checks.
1803    ///
1804    /// # Errors
1805    ///
1806    /// Returns an error if the attestation is invalid or its issuer is not
1807    /// trusted.
1808    pub fn append_attestation(&self, attestation: Attestation) -> Result<bool> {
1809        attestation.verify_signature()?;
1810        let now = current_epoch_seconds();
1811        if !self
1812            .policy()?
1813            .is_trusted_for_at(&attestation.issuer, Some(&attestation.content), now)
1814        {
1815            return Err(SyncwebError::InvalidIdentity(
1816                "attestation issuer is not trusted for this content".to_owned(),
1817            ));
1818        }
1819        let mut state = self
1820            .state
1821            .lock()
1822            .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1823        let key = (attestation.content, attestation.issuer.clone(), attestation.sequence);
1824        if state.attestations.contains_key(&key) {
1825            return Ok(false);
1826        }
1827        state.attestations.insert(key, attestation);
1828        drop(state);
1829        Ok(true)
1830    }
1831
1832    /// Verify an attestation's cryptographic signature without applying local
1833    /// trust policy.
1834    ///
1835    /// # Errors
1836    ///
1837    /// Returns an error if the attestation is malformed or tampered with.
1838    pub fn verify_attestation(&self, attestation: &Attestation) -> Result<()> {
1839        attestation.verify_signature()
1840    }
1841}
1842
1843fn signed_bytes<T: Serialize>(context: &[u8], value: &T) -> Result<Vec<u8>> {
1844    let encoded = serde_json::to_vec(value)
1845        .map_err(|error| SyncwebError::operation("failed to serialize signed Web-of-Trust record", error))?;
1846    let mut bytes = Vec::with_capacity(context.len().saturating_add(encoded.len()));
1847    bytes.extend_from_slice(context);
1848    bytes.extend_from_slice(&encoded);
1849    Ok(bytes)
1850}
1851
1852fn sign_text(context: &[u8], unsigned: &[u8], signing_key: &SigningKey) -> String {
1853    let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1854    bytes.extend_from_slice(context);
1855    bytes.extend_from_slice(unsigned);
1856    hex::encode(signing_key.sign(&bytes).to_bytes())
1857}
1858
1859fn verify_text(context: &[u8], unsigned: &[u8], author: &str, signature: Option<&str>) -> Result<()> {
1860    let signature_text = signature
1861        .ok_or_else(|| SyncwebError::InvalidConfig("signed Web-of-Trust record has no signature".to_owned()))?;
1862    let signature_bytes = hex::decode(signature_text)
1863        .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature encoding: {error}")))?;
1864    let parsed_signature = Signature::from_slice(&signature_bytes)
1865        .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature: {error}")))?;
1866    let verifying_key = parse_author(author)?;
1867    let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1868    bytes.extend_from_slice(context);
1869    bytes.extend_from_slice(unsigned);
1870    verifying_key
1871        .verify(&bytes, &parsed_signature)
1872        .map_err(|error| SyncwebError::InvalidConfig(format!("Web-of-Trust signature is invalid: {error}")))
1873}
1874
1875fn ensure_signer(author: &str, signing_key: &VerifyingKey) -> Result<()> {
1876    if author == author_id(signing_key) {
1877        Ok(())
1878    } else {
1879        Err(SyncwebError::InvalidIdentity(
1880            "Web-of-Trust signer does not match the declared author".to_owned(),
1881        ))
1882    }
1883}
1884
1885fn author_id(author: &VerifyingKey) -> String {
1886    hex::encode(author.to_bytes())
1887}
1888
1889fn parse_author(author: &str) -> Result<VerifyingKey> {
1890    let bytes = hex::decode(author)
1891        .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author encoding: {error}")))?;
1892    let key_bytes: [u8; 32] = bytes.try_into().map_err(|error: Vec<u8>| {
1893        SyncwebError::InvalidIdentity(format!("Web-of-Trust author must be 32 bytes, got {}", error.len()))
1894    })?;
1895    VerifyingKey::from_bytes(&key_bytes)
1896        .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author key: {error}")))
1897}
1898
1899impl SignedGossipMessage for Attestation {
1900    fn verify_signature(&self) -> Result<()> {
1901        self.verify_signature()
1902    }
1903}
1904
1905/// Deterministic gossip topic for broadcasting attestations.
1906pub const ATTESTATION_GOSSIP_TOPIC: &[u8] = b"syncweb/attestations/v1";
1907
1908fn current_epoch_seconds() -> u64 {
1909    SystemTime::now()
1910        .duration_since(UNIX_EPOCH)
1911        .map_or(0, |duration| duration.as_secs())
1912}