plan_signing.rs (5272B)
1 //! Exact Nostr unsigned-event construction and completion for authored plans. 2 3 #![forbid(unsafe_code)] 4 5 use crate::{ 6 error::Error, 7 types::{ 8 RadrootsNostrEvent, RadrootsNostrEventId, RadrootsNostrKind, RadrootsNostrPublicKey, 9 RadrootsNostrTag, RadrootsNostrTimestamp, 10 }, 11 }; 12 use radroots_event_codec::authoring::AuthoredEventPlan; 13 #[cfg(feature = "signing")] 14 use radroots_signing::SignRequest; 15 16 #[cfg(feature = "signing")] 17 use nostr::JsonUtil; 18 #[cfg(feature = "signing")] 19 use radroots_event::{SignedEvent, wire::Nip01EventWire}; 20 21 pub(crate) fn unsigned_event_from_plan( 22 plan: &AuthoredEventPlan, 23 ) -> Result<nostr::UnsignedEvent, Error> { 24 let kind = u16::try_from(plan.body().kind()).map_err(|_| Error::KindOutOfRange { 25 kind: plan.body().kind(), 26 max: u16::MAX, 27 })?; 28 let tags = plan 29 .body() 30 .tags() 31 .iter() 32 .cloned() 33 .map(RadrootsNostrTag::parse) 34 .collect::<Result<alloc::vec::Vec<_>, _>>() 35 .map_err(|_| Error::TagConversion)?; 36 let expected_event_id = RadrootsNostrEventId::from_slice(plan.expected_event_id().as_bytes()) 37 .map_err(|_| Error::EventConversion { 38 field: "expected_event_id", 39 })?; 40 let expected_public_key = RadrootsNostrPublicKey::from_slice(plan.author().as_bytes()) 41 .map_err(|_| Error::EventConversion { field: "author" })?; 42 43 let unsigned = nostr::UnsignedEvent { 44 id: Some(expected_event_id), 45 pubkey: expected_public_key, 46 created_at: RadrootsNostrTimestamp::from_secs(plan.created_at()), 47 kind: RadrootsNostrKind::Custom(kind), 48 tags: nostr::Tags::from_list(tags), 49 content: plan.body().content().into(), 50 }; 51 validate_unsigned_event_matches_plan(&unsigned, plan)?; 52 Ok(unsigned) 53 } 54 55 #[cfg(feature = "signing")] 56 fn unsigned_event_from_request(request: &SignRequest) -> Result<nostr::UnsignedEvent, Error> { 57 let kind = u16::try_from(request.kind()).map_err(|_| Error::KindOutOfRange { 58 kind: request.kind(), 59 max: u16::MAX, 60 })?; 61 let tags = request 62 .tags() 63 .iter() 64 .cloned() 65 .map(RadrootsNostrTag::parse) 66 .collect::<Result<alloc::vec::Vec<_>, _>>() 67 .map_err(|_| Error::TagConversion)?; 68 let expected_event_id = 69 RadrootsNostrEventId::from_slice(request.expected_event_id().as_bytes()).map_err(|_| { 70 Error::EventConversion { 71 field: "expected_event_id", 72 } 73 })?; 74 let expected_public_key = 75 RadrootsNostrPublicKey::from_slice(request.expected_author().as_bytes()) 76 .map_err(|_| Error::EventConversion { field: "author" })?; 77 Ok(nostr::UnsignedEvent { 78 id: Some(expected_event_id), 79 pubkey: expected_public_key, 80 created_at: RadrootsNostrTimestamp::from_secs(request.created_at()), 81 kind: RadrootsNostrKind::Custom(kind), 82 tags: nostr::Tags::from_list(tags), 83 content: request.content().into(), 84 }) 85 } 86 87 #[cfg(feature = "signing")] 88 pub(crate) fn sign_request( 89 keys: &nostr::Keys, 90 request: &SignRequest, 91 ) -> Result<SignedEvent, Error> { 92 let event = unsigned_event_from_request(request)?.sign_with_keys(keys)?; 93 let raw_json = event.as_json(); 94 let wire = Nip01EventWire::parse_json(&raw_json)?; 95 SignedEvent::from_wire_verified_id(wire, raw_json).map_err(Into::into) 96 } 97 98 pub(crate) fn validate_signed_event_matches_plan( 99 event: &RadrootsNostrEvent, 100 plan: &AuthoredEventPlan, 101 ) -> Result<(), Error> { 102 validate_exact_fields( 103 event.pubkey, 104 event.created_at, 105 event.kind, 106 &event.tags, 107 &event.content, 108 plan, 109 ) 110 } 111 112 fn validate_unsigned_event_matches_plan( 113 event: &nostr::UnsignedEvent, 114 plan: &AuthoredEventPlan, 115 ) -> Result<(), Error> { 116 if event.id.map(|id| id.to_bytes()) != Some(*plan.expected_event_id().as_bytes()) { 117 return Err(Error::ExternalSigningPlanMismatch { 118 field: "expected_event_id", 119 }); 120 } 121 validate_exact_fields( 122 event.pubkey, 123 event.created_at, 124 event.kind, 125 &event.tags, 126 &event.content, 127 plan, 128 ) 129 } 130 131 fn validate_exact_fields( 132 author: RadrootsNostrPublicKey, 133 created_at: RadrootsNostrTimestamp, 134 kind: RadrootsNostrKind, 135 tags: &nostr::Tags, 136 content: &str, 137 plan: &AuthoredEventPlan, 138 ) -> Result<(), Error> { 139 if author.to_bytes() != *plan.author().as_bytes() { 140 return Err(Error::ExternalSigningPlanMismatch { field: "author" }); 141 } 142 if created_at.as_secs() != plan.created_at() { 143 return Err(Error::ExternalSigningPlanMismatch { 144 field: "created_at", 145 }); 146 } 147 if u32::from(kind.as_u16()) != plan.body().kind() { 148 return Err(Error::ExternalSigningPlanMismatch { field: "kind" }); 149 } 150 if tags.len() != plan.body().tags().len() 151 || tags 152 .iter() 153 .zip(plan.body().tags()) 154 .any(|(actual, expected)| actual.as_slice() != expected) 155 { 156 return Err(Error::ExternalSigningPlanMismatch { field: "tags" }); 157 } 158 if content != plan.body().content() { 159 return Err(Error::ExternalSigningPlanMismatch { field: "content" }); 160 } 161 Ok(()) 162 }