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