1use 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#[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
46pub type WotMetadata = MetadataEntry;
48
49impl MetadataEntry {
50 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 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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#[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#[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 #[must_use]
380 pub fn new() -> Self {
381 Self::default()
382 }
383
384 #[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 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 pub fn revoke_author(&mut self, author: &VerifyingKey) {
400 self.revoked_authors.insert(author_id(author));
401 }
402
403 pub fn add_delegation(&mut self, delegation: TrustDelegation) -> Result<bool> {
410 self.add_delegation_at(delegation, current_epoch_seconds())
411 }
412
413 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 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 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 #[must_use]
486 pub fn evaluate(&self, author: &str) -> TrustDecision {
487 self.evaluate_at(author, current_epoch_seconds())
488 }
489
490 #[must_use]
492 pub fn evaluate_at(&self, author: &str, now: u64) -> TrustDecision {
493 self.evaluate_for_at(author, None, now)
494 }
495
496 #[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 #[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 #[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 #[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#[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 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 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 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 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#[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
663pub 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#[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#[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#[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 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 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 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 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 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 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 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 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#[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#[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 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 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 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 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#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1062#[non_exhaustive]
1063pub enum ProviderTrustAction {
1064 Trust,
1065 Distrust,
1066 Vouch,
1067 Warn,
1068}
1069
1070#[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 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 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 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 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 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 pub fn verify(&self) -> Result<()> {
1187 self.verify_at(current_epoch_seconds())
1188 }
1189
1190 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 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 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 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#[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#[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 #[must_use]
1309 pub fn new(indexing: &IndexingService, policy: TrustPolicy) -> Self {
1310 Self::with_database(indexing.database().clone(), policy)
1311 }
1312
1313 #[must_use]
1315 pub fn with_database(database: IndexingDatabase, policy: TrustPolicy) -> Self {
1316 Self {
1317 database,
1318 policy: Arc::new(Mutex::new(policy)),
1319 state: Arc::new(Mutex::new(WotState::default())),
1320 }
1321 }
1322
1323 pub fn in_memory(policy: TrustPolicy) -> Result<Self> {
1329 Ok(Self::with_database(IndexingDatabase::in_memory()?, policy))
1330 }
1331
1332 #[must_use]
1333 pub const fn database(&self) -> &IndexingDatabase {
1334 &self.database
1335 }
1336
1337 pub fn policy(&self) -> Result<TrustPolicy> {
1343 Ok(self
1344 .policy
1345 .lock()
1346 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1347 .clone())
1348 }
1349
1350 pub fn trust_author(&self, author: &VerifyingKey) -> Result<()> {
1356 self.policy
1357 .lock()
1358 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1359 .trust_author(author);
1360 Ok(())
1361 }
1362
1363 pub fn add_delegation(&self, delegation: TrustDelegation) -> Result<bool> {
1370 self.add_delegation_at(delegation, current_epoch_seconds())
1371 }
1372
1373 pub fn add_delegation_at(&self, delegation: TrustDelegation, now: u64) -> Result<bool> {
1380 self.policy
1381 .lock()
1382 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1383 .add_delegation_at(delegation, now)
1384 }
1385
1386 pub fn revoke_delegation(&self, delegator: &str, delegate: &str, scope: Option<&Hash>) -> Result<bool> {
1394 self.revoke_delegation_at(delegator, delegate, scope, current_epoch_seconds())
1395 }
1396
1397 pub fn revoke_delegation_at(&self, delegator: &str, delegate: &str, scope: Option<&Hash>, at: u64) -> Result<bool> {
1405 self.policy
1406 .lock()
1407 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1408 .revoke_delegation_at(delegator, delegate, scope, at)
1409 }
1410
1411 pub fn apply_provider_trust(&self, record: ProviderTrustRecord) -> Result<bool> {
1420 let now = current_epoch_seconds();
1421 record.verify_at(now)?;
1422 let issuer_key = parse_author(&record.issuer)?;
1423 let is_self_distrust =
1424 issuer_key.as_bytes() == record.provider.as_bytes() && record.action == ProviderTrustAction::Distrust;
1425 if !is_self_distrust
1426 && !self
1427 .policy()?
1428 .is_trusted_for_at(&record.issuer, record.scope.as_ref(), now)
1429 {
1430 return Err(SyncwebError::InvalidIdentity(
1431 "provider trust record issuer is not trusted for its scope".to_owned(),
1432 ));
1433 }
1434 let mut state = self
1435 .state
1436 .lock()
1437 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1438 let records = state.provider_trust.entry(record.provider).or_default();
1439 if records.iter().any(|existing| {
1440 existing.issuer == record.issuer && existing.scope == record.scope && existing.sequence >= record.sequence
1441 }) {
1442 return Ok(false);
1443 }
1444 records.retain(|existing| {
1445 !(existing.issuer == record.issuer && existing.scope == record.scope && existing.sequence < record.sequence)
1446 });
1447 records.push(record);
1448 drop(state);
1449 Ok(true)
1450 }
1451
1452 pub fn evaluate_provider_trust(
1457 &self,
1458 provider: PublicKey,
1459 hash: Option<&Hash>,
1460 now: u64,
1461 ) -> Result<ProviderTrustDecision> {
1462 let records = self
1463 .state
1464 .lock()
1465 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1466 .provider_trust
1467 .get(&provider)
1468 .cloned()
1469 .unwrap_or_default();
1470 let maybe_latest_sequence = records
1471 .iter()
1472 .filter(|record| {
1473 record.expires_at.is_none_or(|expires_at| expires_at > now)
1474 && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1475 })
1476 .map(|record| record.sequence)
1477 .max();
1478 let Some(latest_sequence) = maybe_latest_sequence else {
1479 return Ok(ProviderTrustDecision::Unknown);
1480 };
1481 let latest = records
1482 .iter()
1483 .filter(|record| {
1484 record.sequence == latest_sequence
1485 && record.expires_at.is_none_or(|expires_at| expires_at > now)
1486 && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1487 })
1488 .collect::<Vec<_>>();
1489 let trusted = latest
1490 .iter()
1491 .any(|record| matches!(record.action, ProviderTrustAction::Trust | ProviderTrustAction::Vouch));
1492 let distrusted = latest
1493 .iter()
1494 .any(|record| matches!(record.action, ProviderTrustAction::Distrust));
1495 Ok(match (trusted, distrusted) {
1496 (true, true) => ProviderTrustDecision::Conflicting,
1497 (true, false) => ProviderTrustDecision::Trusted,
1498 (false, true) => ProviderTrustDecision::Distrusted,
1499 (false, false) => ProviderTrustDecision::Unknown,
1500 })
1501 }
1502
1503 pub fn evaluate_provider_trust_for_hash(
1509 &self,
1510 provider: PublicKey,
1511 hash: Hash,
1512 now: u64,
1513 ) -> Result<ProviderTrustDecision> {
1514 self.evaluate_provider_trust(provider, Some(&hash), now)
1515 }
1516
1517 pub fn provider_trust_records(&self, provider: PublicKey) -> Result<Vec<ProviderTrustRecord>> {
1523 let mut records = self
1524 .state
1525 .lock()
1526 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1527 .provider_trust
1528 .get(&provider)
1529 .cloned()
1530 .unwrap_or_default();
1531 records.sort_by(|left, right| {
1532 right
1533 .sequence
1534 .cmp(&left.sequence)
1535 .then_with(|| right.issued_at.cmp(&left.issued_at))
1536 });
1537 Ok(records)
1538 }
1539
1540 pub fn append_metadata(&self, entry: &MetadataEntry) -> Result<bool> {
1547 entry.verify_signature()?;
1548 let now = current_epoch_seconds();
1549 let policy = self
1550 .policy
1551 .lock()
1552 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?;
1553 if !policy.is_trusted_for_at(&entry.author, Some(&entry.content), now) {
1554 return Err(SyncwebError::InvalidIdentity(
1555 "metadata author is not trusted for this content".to_owned(),
1556 ));
1557 }
1558 drop(policy);
1559 if self.is_revoked(&entry.content)? {
1560 return Err(SyncwebError::InvalidConfig(
1561 "metadata cannot be appended to self-revoked content".to_owned(),
1562 ));
1563 }
1564 self.database.append_wot_metadata(entry)
1565 }
1566
1567 pub fn search_metadata(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1573 let records = self.database.search_wot_metadata(query, limit)?;
1574 let state = self
1575 .state
1576 .lock()
1577 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1578 Ok(records
1579 .into_iter()
1580 .filter(|entry| !state.revocations.contains_key(&entry.content))
1581 .filter(|entry| {
1582 state.moderation.get(&entry.content).is_none_or(|moderation_records| {
1583 moderation_records
1584 .iter()
1585 .filter(|record| matches!(&record.scope, ModerationScope::Global))
1586 .max_by_key(|record| record.sequence)
1587 .is_none_or(|record| !record.action.hides_content())
1588 })
1589 })
1590 .collect())
1591 }
1592
1593 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1599 self.search_metadata(query, limit)
1600 }
1601
1602 pub fn revoke_self(&self, record: RevocationRecord) -> Result<bool> {
1609 record.verify_signature()?;
1610 let mut state = self
1611 .state
1612 .lock()
1613 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1614 if state
1615 .revocations
1616 .get(&record.content)
1617 .is_some_and(|existing| existing.sequence >= record.sequence)
1618 {
1619 return Ok(false);
1620 }
1621 state.revocations.insert(record.content, record);
1622 drop(state);
1623 Ok(true)
1624 }
1625
1626 pub fn is_revoked(&self, content: &Hash) -> Result<bool> {
1632 Ok(self
1633 .state
1634 .lock()
1635 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1636 .revocations
1637 .contains_key(content))
1638 }
1639
1640 pub fn apply_moderation(&self, record: ModerationRecord) -> Result<bool> {
1647 record.verify_signature()?;
1648 let now = current_epoch_seconds();
1649 if !self
1650 .policy()?
1651 .is_trusted_for_at(&record.moderator, Some(&record.content), now)
1652 {
1653 return Err(SyncwebError::InvalidIdentity(
1654 "moderator is not trusted for this content".to_owned(),
1655 ));
1656 }
1657 let mut state = self
1658 .state
1659 .lock()
1660 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1661 let records = state.moderation.entry(record.content).or_default();
1662 if records
1663 .iter()
1664 .filter(|existing| existing.scope == record.scope)
1665 .any(|existing| existing.sequence >= record.sequence)
1666 {
1667 return Ok(false);
1668 }
1669 records.push(record);
1670 drop(state);
1671 Ok(true)
1672 }
1673
1674 pub fn moderation(&self, content: &Hash) -> Result<Option<ModerationRecord>> {
1680 Ok(self
1681 .state
1682 .lock()
1683 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1684 .moderation
1685 .get(content)
1686 .and_then(|records| {
1687 records
1688 .iter()
1689 .max_by_key(|record| (record.sequence, record.scope.specificity()))
1690 .cloned()
1691 }))
1692 }
1693
1694 pub fn moderation_records(&self, content: &Hash) -> Result<Vec<ModerationRecord>> {
1700 let mut records = self
1701 .state
1702 .lock()
1703 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1704 .moderation
1705 .get(content)
1706 .cloned()
1707 .unwrap_or_default();
1708 records.sort_by(|left, right| {
1709 right
1710 .sequence
1711 .cmp(&left.sequence)
1712 .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1713 });
1714 Ok(records)
1715 }
1716
1717 pub fn list_moderation(&self, content: Option<&Hash>) -> Result<Vec<ModerationRecord>> {
1723 let state = self
1724 .state
1725 .lock()
1726 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1727 let mut records = state
1728 .moderation
1729 .iter()
1730 .filter(|(hash, _)| content.is_none_or(|requested| requested == *hash))
1731 .flat_map(|(_, records)| records.iter().cloned())
1732 .collect::<Vec<_>>();
1733 drop(state);
1734 records.sort_by(|left, right| {
1735 right
1736 .sequence
1737 .cmp(&left.sequence)
1738 .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1739 });
1740 Ok(records)
1741 }
1742
1743 pub fn moderation_decision(&self, context: &ModerationContext) -> Result<ModerationAction> {
1752 let selected_record = self
1753 .state
1754 .lock()
1755 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1756 .moderation
1757 .get(&context.content)
1758 .and_then(|records| {
1759 records
1760 .iter()
1761 .filter(|record| record.scope.applies_to(context))
1762 .max_by_key(|record| (record.sequence, record.scope.specificity()))
1763 .cloned()
1764 });
1765 Ok(selected_record.map_or(ModerationAction::Show, |selected| selected.action))
1766 }
1767
1768 pub fn evaluate_moderation(&self, context: &ModerationContext) -> Result<ModerationAction> {
1774 self.moderation_decision(context)
1775 }
1776
1777 pub fn append_attestation(&self, attestation: Attestation) -> Result<bool> {
1784 attestation.verify_signature()?;
1785 let now = current_epoch_seconds();
1786 if !self
1787 .policy()?
1788 .is_trusted_for_at(&attestation.issuer, Some(&attestation.content), now)
1789 {
1790 return Err(SyncwebError::InvalidIdentity(
1791 "attestation issuer is not trusted for this content".to_owned(),
1792 ));
1793 }
1794 let mut state = self
1795 .state
1796 .lock()
1797 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1798 let key = (attestation.content, attestation.issuer.clone(), attestation.sequence);
1799 if state.attestations.contains_key(&key) {
1800 return Ok(false);
1801 }
1802 state.attestations.insert(key, attestation);
1803 drop(state);
1804 Ok(true)
1805 }
1806
1807 pub fn verify_attestation(&self, attestation: &Attestation) -> Result<()> {
1814 attestation.verify_signature()
1815 }
1816}
1817
1818fn signed_bytes<T: Serialize>(context: &[u8], value: &T) -> Result<Vec<u8>> {
1819 let encoded = serde_json::to_vec(value)
1820 .map_err(|error| SyncwebError::operation("failed to serialize signed Web-of-Trust record", error))?;
1821 let mut bytes = Vec::with_capacity(context.len().saturating_add(encoded.len()));
1822 bytes.extend_from_slice(context);
1823 bytes.extend_from_slice(&encoded);
1824 Ok(bytes)
1825}
1826
1827fn sign_text(context: &[u8], unsigned: &[u8], signing_key: &SigningKey) -> String {
1828 let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1829 bytes.extend_from_slice(context);
1830 bytes.extend_from_slice(unsigned);
1831 hex::encode(signing_key.sign(&bytes).to_bytes())
1832}
1833
1834fn verify_text(context: &[u8], unsigned: &[u8], author: &str, signature: Option<&str>) -> Result<()> {
1835 let signature_text = signature
1836 .ok_or_else(|| SyncwebError::InvalidConfig("signed Web-of-Trust record has no signature".to_owned()))?;
1837 let signature_bytes = hex::decode(signature_text)
1838 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature encoding: {error}")))?;
1839 let parsed_signature = Signature::from_slice(&signature_bytes)
1840 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature: {error}")))?;
1841 let verifying_key = parse_author(author)?;
1842 let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1843 bytes.extend_from_slice(context);
1844 bytes.extend_from_slice(unsigned);
1845 verifying_key
1846 .verify(&bytes, &parsed_signature)
1847 .map_err(|error| SyncwebError::InvalidConfig(format!("Web-of-Trust signature is invalid: {error}")))
1848}
1849
1850fn ensure_signer(author: &str, signing_key: &VerifyingKey) -> Result<()> {
1851 if author == author_id(signing_key) {
1852 Ok(())
1853 } else {
1854 Err(SyncwebError::InvalidIdentity(
1855 "Web-of-Trust signer does not match the declared author".to_owned(),
1856 ))
1857 }
1858}
1859
1860fn author_id(author: &VerifyingKey) -> String {
1861 hex::encode(author.to_bytes())
1862}
1863
1864fn parse_author(author: &str) -> Result<VerifyingKey> {
1865 let bytes = hex::decode(author)
1866 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author encoding: {error}")))?;
1867 let key_bytes: [u8; 32] = bytes.try_into().map_err(|error: Vec<u8>| {
1868 SyncwebError::InvalidIdentity(format!("Web-of-Trust author must be 32 bytes, got {}", error.len()))
1869 })?;
1870 VerifyingKey::from_bytes(&key_bytes)
1871 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author key: {error}")))
1872}
1873
1874impl SignedGossipMessage for Attestation {
1875 fn verify_signature(&self) -> Result<()> {
1876 self.verify_signature()
1877 }
1878}
1879
1880pub const ATTESTATION_GOSSIP_TOPIC: &[u8] = b"syncweb/attestations/v1";
1882
1883fn current_epoch_seconds() -> u64 {
1884 SystemTime::now()
1885 .duration_since(UNIX_EPOCH)
1886 .map_or(0, |duration| duration.as_secs())
1887}