lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }