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 }