lib

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

verification.rs (13277B)


      1 //! Native event verification typestates.
      2 //!
      3 //! The types in this module make the ordered NIP-01 validation stages explicit.
      4 //! Their representations are private so a later stage can only be obtained by
      5 //! consuming the immediately preceding stage and successfully enforcing its
      6 //! invariant.
      7 
      8 use core::fmt;
      9 
     10 use crate::{
     11     contract::registry_v7::{
     12         ContractValidationError, EventContract, validate_event_contract_for_admission,
     13         validate_event_contract_registry_v7,
     14     },
     15     envelope::EventEnvelope,
     16     id::EventId,
     17     wire::compute_canonical_nip01_event_id,
     18 };
     19 
     20 /// A structurally valid event whose canonical identifier is not yet trusted.
     21 #[derive(Clone, Debug, PartialEq, Eq)]
     22 pub struct RawEvent(EventEnvelope);
     23 
     24 impl RawEvent {
     25     #[must_use]
     26     pub const fn new(event: EventEnvelope) -> Self {
     27         Self(event)
     28     }
     29 
     30     #[must_use]
     31     pub const fn event(&self) -> &EventEnvelope {
     32         &self.0
     33     }
     34 
     35     #[must_use]
     36     pub fn into_event(self) -> EventEnvelope {
     37         self.0
     38     }
     39 
     40     /// Recomputes and verifies the canonical NIP-01 event identifier.
     41     pub fn verify_id(self) -> Result<IdVerifiedEvent, Error> {
     42         let expected = compute_canonical_nip01_event_id(
     43             &self.0.author().to_hex(),
     44             self.0.created_at_u64(),
     45             self.0.kind_u32(),
     46             &self.0.tags_as_vec(),
     47             self.0.content(),
     48         )
     49         .map_err(|_| Error::MalformedEnvelope)?;
     50         let actual = *self.0.id();
     51         if actual != expected {
     52             return Err(Error::IdMismatch { expected, actual });
     53         }
     54         Ok(IdVerifiedEvent(self.0))
     55     }
     56 }
     57 
     58 /// An event whose declared identifier matches its canonical NIP-01 digest.
     59 ///
     60 /// Its private representation rejects forged transitions:
     61 ///
     62 /// ```compile_fail
     63 /// use radroots_event::admission::{IdVerifiedEvent, RawEvent};
     64 ///
     65 /// fn forge(raw: RawEvent) -> IdVerifiedEvent {
     66 ///     IdVerifiedEvent(raw.into_event())
     67 /// }
     68 /// ```
     69 #[derive(Clone, Debug, PartialEq, Eq)]
     70 pub struct IdVerifiedEvent(EventEnvelope);
     71 
     72 impl IdVerifiedEvent {
     73     #[must_use]
     74     pub const fn event(&self) -> &EventEnvelope {
     75         &self.0
     76     }
     77 
     78     #[must_use]
     79     pub fn into_event(self) -> EventEnvelope {
     80         self.0
     81     }
     82 
     83     /// Uses an explicit host capability to verify the signature over the
     84     /// already verified event ID.
     85     pub fn verify_signature<V>(self, verifier: &V) -> Result<SignatureVerifiedEvent, Error>
     86     where
     87         V: SignatureVerifier + ?Sized,
     88     {
     89         verifier.verify_signature(&self.0)?;
     90         Ok(SignatureVerifiedEvent(self.0))
     91     }
     92 }
     93 
     94 /// **Host SPI:** verifies an event signature.
     95 ///
     96 /// The event crate owns the verification state transition, while concrete
     97 /// cryptographic implementations belong to codec or signing crates. Downstream
     98 /// implementations are supported and must normalize rejection to
     99 /// [`Error::SignatureInvalid`]. The trait is dyn-compatible and requires
    100 /// `Send + Sync`; verification is a synchronous, deterministic, side-effect-free
    101 /// operation with no cancellation or deadline boundary and must not durably
    102 /// commit state.
    103 pub trait SignatureVerifier: Send + Sync {
    104     /// Returns success only when the envelope's signature is valid for its
    105     /// already verified canonical event identifier.
    106     fn verify_signature(&self, event: &EventEnvelope) -> Result<(), Error>;
    107 }
    108 
    109 /// An event whose canonical identifier and BIP-340 signature are verified.
    110 ///
    111 /// ```compile_fail
    112 /// use radroots_event::admission::{IdVerifiedEvent, SignatureVerifiedEvent};
    113 ///
    114 /// fn bypass_signature(event: IdVerifiedEvent) -> SignatureVerifiedEvent {
    115 ///     SignatureVerifiedEvent::new(event.into_event())
    116 /// }
    117 /// ```
    118 #[derive(Clone, Debug, PartialEq, Eq)]
    119 pub struct SignatureVerifiedEvent(EventEnvelope);
    120 
    121 impl SignatureVerifiedEvent {
    122     #[must_use]
    123     pub const fn event(&self) -> &EventEnvelope {
    124         &self.0
    125     }
    126 
    127     #[must_use]
    128     pub fn into_event(self) -> EventEnvelope {
    129         self.0
    130     }
    131 
    132     /// Validates the event against the immutable registry-v7 contract set.
    133     pub fn validate_contract(self) -> Result<ContractValidatedEvent, Error> {
    134         let contract =
    135             validate_event_contract_registry_v7(&self.0).map_err(Error::ContractValidation)?;
    136         Ok(ContractValidatedEvent {
    137             event: self,
    138             contract,
    139         })
    140     }
    141 
    142     /// Validates a contract selected explicitly by the admission boundary.
    143     ///
    144     /// This transition supports profiles such as root updates, replies,
    145     /// comments, and deletion requests that intentionally cannot be selected
    146     /// solely from their public Nostr wire shape.
    147     pub fn validate_contract_for_admission(
    148         self,
    149         contract_id: &str,
    150     ) -> Result<ContractValidatedEvent, Error> {
    151         let contract = validate_event_contract_for_admission(&self.0, contract_id)
    152             .map_err(Error::ContractValidation)?;
    153         Ok(ContractValidatedEvent {
    154             event: self,
    155             contract,
    156         })
    157     }
    158 }
    159 
    160 /// A signature-verified event whose registry-selected contract is valid.
    161 ///
    162 /// ```compile_fail
    163 /// use radroots_event::admission::{ContractValidatedEvent, SignatureVerifiedEvent};
    164 ///
    165 /// fn bypass_contract(event: SignatureVerifiedEvent) -> ContractValidatedEvent {
    166 ///     ContractValidatedEvent::from(event)
    167 /// }
    168 /// ```
    169 #[derive(Clone, Debug, PartialEq, Eq)]
    170 pub struct ContractValidatedEvent {
    171     event: SignatureVerifiedEvent,
    172     contract: &'static EventContract,
    173 }
    174 
    175 impl ContractValidatedEvent {
    176     #[must_use]
    177     pub const fn verified_event(&self) -> &SignatureVerifiedEvent {
    178         &self.event
    179     }
    180 
    181     #[must_use]
    182     pub const fn event(&self) -> &EventEnvelope {
    183         self.event.event()
    184     }
    185 
    186     #[must_use]
    187     pub const fn contract(&self) -> &'static EventContract {
    188         self.contract
    189     }
    190 
    191     #[must_use]
    192     pub const fn contract_id(&self) -> &'static str {
    193         self.contract.id
    194     }
    195 
    196     #[must_use]
    197     pub fn into_verified_event(self) -> SignatureVerifiedEvent {
    198         self.event
    199     }
    200 
    201     #[must_use]
    202     pub fn into_event(self) -> EventEnvelope {
    203         self.event.into_event()
    204     }
    205 }
    206 
    207 /// A failure while advancing an event verification typestate.
    208 #[non_exhaustive]
    209 #[derive(Clone, Debug, PartialEq, Eq)]
    210 pub enum Error {
    211     MalformedEnvelope,
    212     IdMismatch { expected: EventId, actual: EventId },
    213     SignatureInvalid,
    214     ContractValidation(ContractValidationError),
    215 }
    216 
    217 impl Error {
    218     #[must_use]
    219     pub const fn code(&self) -> &'static str {
    220         match self {
    221             Self::MalformedEnvelope => "malformed_envelope",
    222             Self::IdMismatch { .. } => "id_mismatch",
    223             Self::SignatureInvalid => "signature_invalid",
    224             Self::ContractValidation(_) => "contract_validation",
    225         }
    226     }
    227 }
    228 
    229 impl fmt::Display for Error {
    230     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    231         match self {
    232             Self::MalformedEnvelope => formatter.write_str("malformed NIP-01 event envelope"),
    233             Self::IdMismatch { expected, actual } => write!(
    234                 formatter,
    235                 "NIP-01 event id mismatch: expected {}, got {}",
    236                 expected.to_hex(),
    237                 actual.to_hex()
    238             ),
    239             Self::SignatureInvalid => formatter.write_str("invalid NIP-01 event signature"),
    240             Self::ContractValidation(error) => {
    241                 write!(
    242                     formatter,
    243                     "event contract validation failed: {}",
    244                     error.code()
    245                 )
    246             }
    247         }
    248     }
    249 }
    250 
    251 #[cfg(feature = "std")]
    252 impl std::error::Error for Error {}
    253 
    254 #[cfg(test)]
    255 #[cfg_attr(coverage_nightly, coverage(off))]
    256 mod tests {
    257     use super::*;
    258     use crate::envelope::EventEnvelopeParts;
    259 
    260     struct Accept;
    261 
    262     impl SignatureVerifier for Accept {
    263         fn verify_signature(&self, _event: &EventEnvelope) -> Result<(), Error> {
    264             Ok(())
    265         }
    266     }
    267 
    268     struct Reject;
    269 
    270     impl SignatureVerifier for Reject {
    271         fn verify_signature(&self, _event: &EventEnvelope) -> Result<(), Error> {
    272             Err(Error::SignatureInvalid)
    273         }
    274     }
    275 
    276     fn valid_profile_event() -> EventEnvelope {
    277         EventEnvelope::new(EventEnvelopeParts {
    278             id: "762bee187e9e645b81ec26ade05a69b5e8398caf527be8de0d9a45311ed0c7a0"
    279                 .to_owned(),
    280             author: "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
    281                 .to_owned(),
    282             created_at: 1_800_000_100,
    283             kind: 0,
    284             tags: vec![],
    285             content: "{\"display_name\":\"Moss Street Farm\",\"bot\":false,\"website\":\"https://mossstreet.example\",\"picture\":42}".to_owned(),
    286             sig: "4290da0bb6422986647bc8cd5f63bd52d49f41e7b665d3b47105b8109183e8d596f322c531d4061df53e1d2b70fda12d5d1c14f3720d7a56d9d0a03746af5109".to_owned(),
    287         })
    288         .expect("valid profile event")
    289     }
    290 
    291     #[test]
    292     fn raw_event_advances_only_after_id_verification() {
    293         let verified = RawEvent::new(valid_profile_event())
    294             .verify_id()
    295             .expect("verified id");
    296         assert_eq!(
    297             verified.event().id().to_hex(),
    298             "762bee187e9e645b81ec26ade05a69b5e8398caf527be8de0d9a45311ed0c7a0"
    299         );
    300     }
    301 
    302     #[test]
    303     fn positive_vector_reaches_contract_validated_state() {
    304         let verifier: &dyn SignatureVerifier = &Accept;
    305         let validated = RawEvent::new(valid_profile_event())
    306             .verify_id()
    307             .expect("verified id")
    308             .verify_signature(verifier)
    309             .expect("verified signature")
    310             .validate_contract()
    311             .expect("validated contract");
    312         assert_eq!(validated.contract_id(), "radroots.profile.metadata.v1");
    313     }
    314 
    315     #[test]
    316     fn signature_transition_fails_closed_when_verifier_rejects() {
    317         let error = RawEvent::new(valid_profile_event())
    318             .verify_id()
    319             .expect("verified id")
    320             .verify_signature(&Reject)
    321             .expect_err("signature must be rejected");
    322         assert_eq!(error, Error::SignatureInvalid);
    323     }
    324 
    325     #[test]
    326     fn typestate_accessors_and_consuming_transitions_preserve_the_envelope() {
    327         let envelope = valid_profile_event();
    328         let raw = RawEvent::new(envelope.clone());
    329         assert_eq!(raw.event(), &envelope);
    330         assert_eq!(raw.clone().into_event(), envelope);
    331 
    332         let id_verified = raw.verify_id().unwrap();
    333         assert_eq!(id_verified.event().kind_u32(), 0);
    334         assert_eq!(id_verified.clone().into_event().kind_u32(), 0);
    335         let signature_verified = id_verified.verify_signature(&Accept).unwrap();
    336         assert_eq!(signature_verified.event().kind_u32(), 0);
    337         assert_eq!(signature_verified.clone().into_event().kind_u32(), 0);
    338 
    339         let validated = signature_verified.validate_contract().unwrap();
    340         assert_eq!(validated.verified_event().event(), validated.event());
    341         assert_eq!(validated.contract().id, validated.contract_id());
    342         assert_eq!(
    343             validated.clone().into_verified_event().event().kind_u32(),
    344             0
    345         );
    346         assert_eq!(validated.into_event().kind_u32(), 0);
    347     }
    348 
    349     #[test]
    350     fn id_and_contract_failures_are_typed_and_diagnostic() {
    351         let wrong_id = EventEnvelope::new(EventEnvelopeParts {
    352             id: "0".repeat(64),
    353             author: "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df".to_owned(),
    354             created_at: 1_800_000_100,
    355             kind: 0,
    356             tags: vec![],
    357             content: "{}".to_owned(),
    358             sig: "0".repeat(128),
    359         })
    360         .unwrap();
    361         assert!(matches!(
    362             RawEvent::new(wrong_id).verify_id(),
    363             Err(Error::IdMismatch { .. })
    364         ));
    365 
    366         let unknown = envelope_with_computed_id(65_535, vec![], "{}");
    367         let error = RawEvent::new(unknown)
    368             .verify_id()
    369             .unwrap()
    370             .verify_signature(&Accept)
    371             .unwrap()
    372             .validate_contract()
    373             .unwrap_err();
    374         assert!(matches!(error, Error::ContractValidation(_)));
    375 
    376         let errors = [
    377             Error::MalformedEnvelope,
    378             Error::IdMismatch {
    379                 expected: EventId::parse("1".repeat(64)).unwrap(),
    380                 actual: EventId::parse("2".repeat(64)).unwrap(),
    381             },
    382             Error::SignatureInvalid,
    383             error,
    384         ];
    385         for error in errors {
    386             assert!(!error.code().is_empty());
    387             assert!(!error.to_string().is_empty());
    388         }
    389     }
    390 
    391     fn envelope_with_computed_id(
    392         kind: u32,
    393         tags: Vec<Vec<String>>,
    394         content: &str,
    395     ) -> EventEnvelope {
    396         let author = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
    397         let created_at = 1_800_000_100;
    398         let id =
    399             compute_canonical_nip01_event_id(author, created_at, kind, &tags, content).unwrap();
    400         EventEnvelope::new(EventEnvelopeParts {
    401             id: id.to_hex(),
    402             author: author.to_owned(),
    403             created_at,
    404             kind,
    405             tags,
    406             content: content.to_owned(),
    407             sig: "0".repeat(128),
    408         })
    409         .unwrap()
    410     }
    411 }