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 let mut state = WotState::default();
1317 if let Ok(records) = database.load_provider_trust_records() {
1318 let now = current_epoch_seconds();
1319 for record in records {
1320 if record.expires_at.is_some_and(|e| e <= now) {
1321 continue;
1322 }
1323 if record.verify_signature().is_err() {
1324 tracing::warn!("skipping provider trust record with invalid signature on load");
1325 continue;
1326 }
1327 state.provider_trust.entry(record.provider).or_default().push(record);
1328 }
1329 }
1330 Self {
1331 database,
1332 policy: Arc::new(Mutex::new(policy)),
1333 state: Arc::new(Mutex::new(state)),
1334 }
1335 }
1336
1337 pub fn in_memory(policy: TrustPolicy) -> Result<Self> {
1343 Ok(Self::with_database(IndexingDatabase::in_memory()?, policy))
1344 }
1345
1346 #[must_use]
1347 pub const fn database(&self) -> &IndexingDatabase {
1348 &self.database
1349 }
1350
1351 pub fn policy(&self) -> Result<TrustPolicy> {
1357 Ok(self
1358 .policy
1359 .lock()
1360 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1361 .clone())
1362 }
1363
1364 pub fn trust_author(&self, author: &VerifyingKey) -> Result<()> {
1370 self.policy
1371 .lock()
1372 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1373 .trust_author(author);
1374 Ok(())
1375 }
1376
1377 pub fn add_delegation(&self, delegation: TrustDelegation) -> Result<bool> {
1384 self.add_delegation_at(delegation, current_epoch_seconds())
1385 }
1386
1387 pub fn add_delegation_at(&self, delegation: TrustDelegation, now: u64) -> Result<bool> {
1394 self.policy
1395 .lock()
1396 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1397 .add_delegation_at(delegation, now)
1398 }
1399
1400 pub fn revoke_delegation(&self, delegator: &str, delegate: &str, scope: Option<&Hash>) -> Result<bool> {
1408 self.revoke_delegation_at(delegator, delegate, scope, current_epoch_seconds())
1409 }
1410
1411 pub fn revoke_delegation_at(&self, delegator: &str, delegate: &str, scope: Option<&Hash>, at: u64) -> Result<bool> {
1419 self.policy
1420 .lock()
1421 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?
1422 .revoke_delegation_at(delegator, delegate, scope, at)
1423 }
1424
1425 pub fn apply_provider_trust(&self, record: ProviderTrustRecord) -> Result<bool> {
1434 let now = current_epoch_seconds();
1435 record.verify_at(now)?;
1436 let issuer_key = parse_author(&record.issuer)?;
1437 let is_self_distrust =
1438 issuer_key.as_bytes() == record.provider.as_bytes() && record.action == ProviderTrustAction::Distrust;
1439 if !is_self_distrust
1440 && !self
1441 .policy()?
1442 .is_trusted_for_at(&record.issuer, record.scope.as_ref(), now)
1443 {
1444 return Err(SyncwebError::InvalidIdentity(
1445 "provider trust record issuer is not trusted for its scope".to_owned(),
1446 ));
1447 }
1448 let all_records = {
1449 let mut state = self
1450 .state
1451 .lock()
1452 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1453 let records = state.provider_trust.entry(record.provider).or_default();
1454 if records.iter().any(|existing| {
1455 existing.issuer == record.issuer
1456 && existing.scope == record.scope
1457 && existing.sequence >= record.sequence
1458 }) {
1459 return Ok(false);
1460 }
1461 records.retain(|existing| {
1462 !(existing.issuer == record.issuer
1463 && existing.scope == record.scope
1464 && existing.sequence < record.sequence)
1465 });
1466 records.push(record);
1467 state
1468 .provider_trust
1469 .values()
1470 .flat_map(|v| v.iter().cloned())
1471 .collect::<Vec<_>>()
1472 };
1473 self.database.save_provider_trust_records(&all_records)?;
1474 Ok(true)
1475 }
1476
1477 pub fn evaluate_provider_trust(
1482 &self,
1483 provider: PublicKey,
1484 hash: Option<&Hash>,
1485 now: u64,
1486 ) -> Result<ProviderTrustDecision> {
1487 let records = self
1488 .state
1489 .lock()
1490 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1491 .provider_trust
1492 .get(&provider)
1493 .cloned()
1494 .unwrap_or_default();
1495 let maybe_latest_sequence = records
1496 .iter()
1497 .filter(|record| {
1498 record.expires_at.is_none_or(|expires_at| expires_at > now)
1499 && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1500 })
1501 .map(|record| record.sequence)
1502 .max();
1503 let Some(latest_sequence) = maybe_latest_sequence else {
1504 return Ok(ProviderTrustDecision::Unknown);
1505 };
1506 let latest = records
1507 .iter()
1508 .filter(|record| {
1509 record.sequence == latest_sequence
1510 && record.expires_at.is_none_or(|expires_at| expires_at > now)
1511 && record.scope.as_ref().is_none_or(|scope| Some(scope) == hash)
1512 })
1513 .collect::<Vec<_>>();
1514 let trusted = latest
1515 .iter()
1516 .any(|record| matches!(record.action, ProviderTrustAction::Trust | ProviderTrustAction::Vouch));
1517 let distrusted = latest
1518 .iter()
1519 .any(|record| matches!(record.action, ProviderTrustAction::Distrust));
1520 Ok(match (trusted, distrusted) {
1521 (true, true) => ProviderTrustDecision::Conflicting,
1522 (true, false) => ProviderTrustDecision::Trusted,
1523 (false, true) => ProviderTrustDecision::Distrusted,
1524 (false, false) => ProviderTrustDecision::Unknown,
1525 })
1526 }
1527
1528 pub fn evaluate_provider_trust_for_hash(
1534 &self,
1535 provider: PublicKey,
1536 hash: Hash,
1537 now: u64,
1538 ) -> Result<ProviderTrustDecision> {
1539 self.evaluate_provider_trust(provider, Some(&hash), now)
1540 }
1541
1542 pub fn provider_trust_records(&self, provider: PublicKey) -> Result<Vec<ProviderTrustRecord>> {
1548 let mut records = self
1549 .state
1550 .lock()
1551 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1552 .provider_trust
1553 .get(&provider)
1554 .cloned()
1555 .unwrap_or_default();
1556 records.sort_by(|left, right| {
1557 right
1558 .sequence
1559 .cmp(&left.sequence)
1560 .then_with(|| right.issued_at.cmp(&left.issued_at))
1561 });
1562 Ok(records)
1563 }
1564
1565 pub fn append_metadata(&self, entry: &MetadataEntry) -> Result<bool> {
1572 entry.verify_signature()?;
1573 let now = current_epoch_seconds();
1574 let policy = self
1575 .policy
1576 .lock()
1577 .map_err(|error| SyncwebError::operation("Web-of-Trust policy lock poisoned", error))?;
1578 if !policy.is_trusted_for_at(&entry.author, Some(&entry.content), now) {
1579 return Err(SyncwebError::InvalidIdentity(
1580 "metadata author is not trusted for this content".to_owned(),
1581 ));
1582 }
1583 drop(policy);
1584 if self.is_revoked(&entry.content)? {
1585 return Err(SyncwebError::InvalidConfig(
1586 "metadata cannot be appended to self-revoked content".to_owned(),
1587 ));
1588 }
1589 self.database.append_wot_metadata(entry)
1590 }
1591
1592 pub fn search_metadata(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1598 let records = self.database.search_wot_metadata(query, limit)?;
1599 let state = self
1600 .state
1601 .lock()
1602 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1603 Ok(records
1604 .into_iter()
1605 .filter(|entry| !state.revocations.contains_key(&entry.content))
1606 .filter(|entry| {
1607 state.moderation.get(&entry.content).is_none_or(|moderation_records| {
1608 moderation_records
1609 .iter()
1610 .filter(|record| matches!(&record.scope, ModerationScope::Global))
1611 .max_by_key(|record| record.sequence)
1612 .is_none_or(|record| !record.action.hides_content())
1613 })
1614 })
1615 .collect())
1616 }
1617
1618 pub fn search(&self, query: &str, limit: usize) -> Result<Vec<MetadataEntry>> {
1624 self.search_metadata(query, limit)
1625 }
1626
1627 pub fn revoke_self(&self, record: RevocationRecord) -> Result<bool> {
1634 record.verify_signature()?;
1635 let mut state = self
1636 .state
1637 .lock()
1638 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1639 if state
1640 .revocations
1641 .get(&record.content)
1642 .is_some_and(|existing| existing.sequence >= record.sequence)
1643 {
1644 return Ok(false);
1645 }
1646 state.revocations.insert(record.content, record);
1647 drop(state);
1648 Ok(true)
1649 }
1650
1651 pub fn is_revoked(&self, content: &Hash) -> Result<bool> {
1657 Ok(self
1658 .state
1659 .lock()
1660 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1661 .revocations
1662 .contains_key(content))
1663 }
1664
1665 pub fn apply_moderation(&self, record: ModerationRecord) -> Result<bool> {
1672 record.verify_signature()?;
1673 let now = current_epoch_seconds();
1674 if !self
1675 .policy()?
1676 .is_trusted_for_at(&record.moderator, Some(&record.content), now)
1677 {
1678 return Err(SyncwebError::InvalidIdentity(
1679 "moderator is not trusted for this content".to_owned(),
1680 ));
1681 }
1682 let mut state = self
1683 .state
1684 .lock()
1685 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1686 let records = state.moderation.entry(record.content).or_default();
1687 if records
1688 .iter()
1689 .filter(|existing| existing.scope == record.scope)
1690 .any(|existing| existing.sequence >= record.sequence)
1691 {
1692 return Ok(false);
1693 }
1694 records.push(record);
1695 drop(state);
1696 Ok(true)
1697 }
1698
1699 pub fn moderation(&self, content: &Hash) -> Result<Option<ModerationRecord>> {
1705 Ok(self
1706 .state
1707 .lock()
1708 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1709 .moderation
1710 .get(content)
1711 .and_then(|records| {
1712 records
1713 .iter()
1714 .max_by_key(|record| (record.sequence, record.scope.specificity()))
1715 .cloned()
1716 }))
1717 }
1718
1719 pub fn moderation_records(&self, content: &Hash) -> Result<Vec<ModerationRecord>> {
1725 let mut records = self
1726 .state
1727 .lock()
1728 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1729 .moderation
1730 .get(content)
1731 .cloned()
1732 .unwrap_or_default();
1733 records.sort_by(|left, right| {
1734 right
1735 .sequence
1736 .cmp(&left.sequence)
1737 .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1738 });
1739 Ok(records)
1740 }
1741
1742 pub fn list_moderation(&self, content: Option<&Hash>) -> Result<Vec<ModerationRecord>> {
1748 let state = self
1749 .state
1750 .lock()
1751 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1752 let mut records = state
1753 .moderation
1754 .iter()
1755 .filter(|(hash, _)| content.is_none_or(|requested| requested == *hash))
1756 .flat_map(|(_, records)| records.iter().cloned())
1757 .collect::<Vec<_>>();
1758 drop(state);
1759 records.sort_by(|left, right| {
1760 right
1761 .sequence
1762 .cmp(&left.sequence)
1763 .then_with(|| right.scope.specificity().cmp(&left.scope.specificity()))
1764 });
1765 Ok(records)
1766 }
1767
1768 pub fn moderation_decision(&self, context: &ModerationContext) -> Result<ModerationAction> {
1777 let selected_record = self
1778 .state
1779 .lock()
1780 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?
1781 .moderation
1782 .get(&context.content)
1783 .and_then(|records| {
1784 records
1785 .iter()
1786 .filter(|record| record.scope.applies_to(context))
1787 .max_by_key(|record| (record.sequence, record.scope.specificity()))
1788 .cloned()
1789 });
1790 Ok(selected_record.map_or(ModerationAction::Show, |selected| selected.action))
1791 }
1792
1793 pub fn evaluate_moderation(&self, context: &ModerationContext) -> Result<ModerationAction> {
1799 self.moderation_decision(context)
1800 }
1801
1802 pub fn append_attestation(&self, attestation: Attestation) -> Result<bool> {
1809 attestation.verify_signature()?;
1810 let now = current_epoch_seconds();
1811 if !self
1812 .policy()?
1813 .is_trusted_for_at(&attestation.issuer, Some(&attestation.content), now)
1814 {
1815 return Err(SyncwebError::InvalidIdentity(
1816 "attestation issuer is not trusted for this content".to_owned(),
1817 ));
1818 }
1819 let mut state = self
1820 .state
1821 .lock()
1822 .map_err(|error| SyncwebError::operation("Web-of-Trust state lock poisoned", error))?;
1823 let key = (attestation.content, attestation.issuer.clone(), attestation.sequence);
1824 if state.attestations.contains_key(&key) {
1825 return Ok(false);
1826 }
1827 state.attestations.insert(key, attestation);
1828 drop(state);
1829 Ok(true)
1830 }
1831
1832 pub fn verify_attestation(&self, attestation: &Attestation) -> Result<()> {
1839 attestation.verify_signature()
1840 }
1841}
1842
1843fn signed_bytes<T: Serialize>(context: &[u8], value: &T) -> Result<Vec<u8>> {
1844 let encoded = serde_json::to_vec(value)
1845 .map_err(|error| SyncwebError::operation("failed to serialize signed Web-of-Trust record", error))?;
1846 let mut bytes = Vec::with_capacity(context.len().saturating_add(encoded.len()));
1847 bytes.extend_from_slice(context);
1848 bytes.extend_from_slice(&encoded);
1849 Ok(bytes)
1850}
1851
1852fn sign_text(context: &[u8], unsigned: &[u8], signing_key: &SigningKey) -> String {
1853 let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1854 bytes.extend_from_slice(context);
1855 bytes.extend_from_slice(unsigned);
1856 hex::encode(signing_key.sign(&bytes).to_bytes())
1857}
1858
1859fn verify_text(context: &[u8], unsigned: &[u8], author: &str, signature: Option<&str>) -> Result<()> {
1860 let signature_text = signature
1861 .ok_or_else(|| SyncwebError::InvalidConfig("signed Web-of-Trust record has no signature".to_owned()))?;
1862 let signature_bytes = hex::decode(signature_text)
1863 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature encoding: {error}")))?;
1864 let parsed_signature = Signature::from_slice(&signature_bytes)
1865 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid Web-of-Trust signature: {error}")))?;
1866 let verifying_key = parse_author(author)?;
1867 let mut bytes = Vec::with_capacity(context.len().saturating_add(unsigned.len()));
1868 bytes.extend_from_slice(context);
1869 bytes.extend_from_slice(unsigned);
1870 verifying_key
1871 .verify(&bytes, &parsed_signature)
1872 .map_err(|error| SyncwebError::InvalidConfig(format!("Web-of-Trust signature is invalid: {error}")))
1873}
1874
1875fn ensure_signer(author: &str, signing_key: &VerifyingKey) -> Result<()> {
1876 if author == author_id(signing_key) {
1877 Ok(())
1878 } else {
1879 Err(SyncwebError::InvalidIdentity(
1880 "Web-of-Trust signer does not match the declared author".to_owned(),
1881 ))
1882 }
1883}
1884
1885fn author_id(author: &VerifyingKey) -> String {
1886 hex::encode(author.to_bytes())
1887}
1888
1889fn parse_author(author: &str) -> Result<VerifyingKey> {
1890 let bytes = hex::decode(author)
1891 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author encoding: {error}")))?;
1892 let key_bytes: [u8; 32] = bytes.try_into().map_err(|error: Vec<u8>| {
1893 SyncwebError::InvalidIdentity(format!("Web-of-Trust author must be 32 bytes, got {}", error.len()))
1894 })?;
1895 VerifyingKey::from_bytes(&key_bytes)
1896 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid Web-of-Trust author key: {error}")))
1897}
1898
1899impl SignedGossipMessage for Attestation {
1900 fn verify_signature(&self) -> Result<()> {
1901 self.verify_signature()
1902 }
1903}
1904
1905pub const ATTESTATION_GOSSIP_TOPIC: &[u8] = b"syncweb/attestations/v1";
1907
1908fn current_epoch_seconds() -> u64 {
1909 SystemTime::now()
1910 .duration_since(UNIX_EPOCH)
1911 .map_or(0, |duration| duration.as_secs())
1912}