authored_signing.rs (17051B)
1 use nostr::{EventBuilder, JsonUtil, Keys, Kind as NostrKind, SecretKey, Timestamp}; 2 use radroots_blossom::{ 3 Sha256 as BlossomSha256, 4 authorization::{AuthoredUploadClaim, AuthorizationContent, ServerDomain}, 5 }; 6 use radroots_event::{ 7 GenericEventDraft, 8 contract::AuthorRole, 9 food::availability::{ 10 FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityStatus, FoodContent, 11 FoodCurrency, FoodIdentifier, FoodPrice, FoodPublishedAt, FoodText, FoodUnit, 12 }, 13 wire::Nip01EventWire, 14 }; 15 use radroots_event_codec::authoring::{AuthoredEventPlan, BlossomAuthorizationPlan}; 16 use radroots_identity::{AccountId, PublicKey}; 17 use radroots_protocol::runtime::v1::OperationId; 18 use radroots_signing::{ 19 Actor, AuthoredArtifactId, CurrentAuthoringAuthority, CurrentAuthoringDecision, Error, 20 SignReceipt, SignRequest, SigningIntentId, SigningOperationId, SigningPurpose, 21 actor::ActorSource, 22 authorization::ManagedSigningPolicy, 23 error::Kind, 24 recovery::{RecoveryDisposition, RemoteEffect, ReplayCapability, recovery_disposition}, 25 request::{CancellationPolicy, CancellationSignal, ProgressObserver, SignPolicy}, 26 status::{SignProgress, SignProgressStage}, 27 }; 28 29 const SECRET: &str = "7e0112ad58b2d2d13fb80532625195dc169b86d72b0e1db48347837a785cae90"; 30 const CREATED_AT: u64 = 1_700_000_000; 31 const DEADLINE_MS: u64 = 1_700_000_100_000; 32 33 #[derive(Clone, Copy)] 34 struct FixedAuthority(CurrentAuthoringDecision); 35 36 impl CurrentAuthoringAuthority for FixedAuthority { 37 fn evaluate(&self, _plan: &AuthoredEventPlan) -> CurrentAuthoringDecision { 38 self.0 39 } 40 } 41 42 fn keys() -> Keys { 43 Keys::new(SecretKey::from_hex(SECRET).expect("secret fixture")) 44 } 45 46 fn public_key() -> PublicKey { 47 PublicKey::from_hex(&keys().public_key().to_hex()).expect("public key") 48 } 49 50 fn plan() -> AuthoredEventPlan { 51 AuthoredEventPlan::from_generic( 52 GenericEventDraft::new( 53 "radroots.social.geochat.v1", 54 20_000, 55 CREATED_AT, 56 Vec::new(), 57 "exact signing plan", 58 public_key().to_hex(), 59 ) 60 .expect("generic draft"), 61 ) 62 .expect("authored plan") 63 } 64 65 fn actor(source: ActorSource, roles: impl IntoIterator<Item = AuthorRole>) -> Actor { 66 Actor::new(public_key(), source, roles).expect("actor") 67 } 68 69 fn intent(operation: u8, artifact: u8) -> SigningIntentId { 70 SigningIntentId::new( 71 SigningOperationId::new([operation; 16]).expect("operation ID"), 72 AuthoredArtifactId::new([artifact; 16]).expect("artifact ID"), 73 ) 74 } 75 76 fn policy() -> SignPolicy { 77 SignPolicy::new(DEADLINE_MS, CancellationPolicy::LocalCooperative).expect("policy") 78 } 79 80 fn request() -> SignRequest { 81 SignRequest::new( 82 OperationId::SyncPush, 83 intent(1, 2), 84 actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]), 85 plan(), 86 policy(), 87 ) 88 .expect("request") 89 } 90 91 fn blossom_plan() -> BlossomAuthorizationPlan { 92 let claim = AuthoredUploadClaim::new( 93 AuthorizationContent::parse("Upload exact image").expect("content"), 94 ServerDomain::parse("media.example").expect("server"), 95 BlossomSha256::digest(b"exact-image"), 96 CREATED_AT, 97 60, 98 ) 99 .expect("upload claim"); 100 BlossomAuthorizationPlan::for_upload(&claim, public_key()).expect("authorization plan") 101 } 102 103 fn blossom_request() -> SignRequest { 104 SignRequest::blossom_upload( 105 OperationId::SyncPush, 106 intent(9, 10), 107 actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]), 108 blossom_plan(), 109 policy(), 110 ) 111 .expect("Blossom request") 112 } 113 114 fn signed_event() -> radroots_event::SignedEvent { 115 signed_event_with( 116 &keys(), 117 20_000, 118 "exact signing plan", 119 CREATED_AT, 120 Vec::new(), 121 ) 122 } 123 124 fn signed_event_with( 125 signer: &Keys, 126 kind: u16, 127 content: &str, 128 created_at: u64, 129 tags: Vec<nostr::Tag>, 130 ) -> radroots_event::SignedEvent { 131 let event = EventBuilder::new(NostrKind::Custom(kind), content) 132 .tags(tags) 133 .custom_created_at(Timestamp::from_secs(created_at)) 134 .sign_with_keys(signer) 135 .expect("signed fixture"); 136 let raw = event.as_json(); 137 let wire = Nip01EventWire::parse_json(&raw).expect("wire"); 138 radroots_event::SignedEvent::from_wire_verified_id(wire, raw).expect("signed event") 139 } 140 141 #[test] 142 fn authorization_decisions_are_current_and_explicit() { 143 let base_actor = actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]); 144 let base_plan = plan(); 145 for decision in [ 146 CurrentAuthoringDecision::Blocked { code: "blocked" }, 147 CurrentAuthoringDecision::Revoked { code: "revoked" }, 148 ] { 149 let error = SignRequest::new_with_authority( 150 OperationId::SyncPush, 151 intent(1, 2), 152 base_actor.clone(), 153 base_plan.clone(), 154 policy(), 155 &FixedAuthority(decision), 156 ) 157 .expect_err("denied decision"); 158 assert_eq!(error.kind(), Kind::AuthorizationDenied); 159 } 160 161 let deprecated = FixedAuthority(CurrentAuthoringDecision::AllowedDeprecated { 162 warning_code: "deprecated", 163 }); 164 assert_eq!( 165 SignRequest::new_with_authority( 166 OperationId::SyncPush, 167 intent(1, 2), 168 base_actor.clone(), 169 base_plan.clone(), 170 policy(), 171 &deprecated, 172 ) 173 .expect_err("deprecated requires opt in") 174 .kind(), 175 Kind::AuthorizationDenied 176 ); 177 let allowed = SignRequest::new_with_authority( 178 OperationId::SyncPush, 179 intent(1, 2), 180 base_actor, 181 base_plan, 182 policy().allowing_deprecated(), 183 &deprecated, 184 ) 185 .expect("explicitly allowed deprecated plan"); 186 assert!(matches!( 187 allowed.authorization(), 188 Some(CurrentAuthoringDecision::AllowedDeprecated { .. }) 189 )); 190 } 191 192 #[test] 193 fn authorization_enforces_key_role_and_host_provenance() { 194 let wrong_key = 195 PublicKey::from_hex("e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af") 196 .expect("other public key"); 197 let wrong_actor = 198 Actor::new(wrong_key, ActorSource::ExplicitPublicKey, [AuthorRole::Any]).expect("actor"); 199 assert_eq!( 200 SignRequest::new( 201 OperationId::SyncPush, 202 intent(1, 2), 203 wrong_actor, 204 plan(), 205 policy(), 206 ) 207 .expect_err("key drift") 208 .kind(), 209 Kind::AuthorizationDenied 210 ); 211 212 let account = AccountId::from_hex(&public_key().to_hex()).expect("account ID"); 213 let local_actor = actor(ActorSource::LocalAccount(account), [AuthorRole::Any]); 214 SignRequest::new( 215 OperationId::SyncPush, 216 intent(1, 2), 217 local_actor, 218 plan(), 219 policy().with_managed_signing_policy(ManagedSigningPolicy::LocalAccountOnly), 220 ) 221 .expect("local account is allowed"); 222 assert_eq!( 223 SignRequest::new( 224 OperationId::SyncPush, 225 intent(1, 2), 226 actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]), 227 plan(), 228 policy().with_managed_signing_policy(ManagedSigningPolicy::AccountBackedOnly), 229 ) 230 .expect_err("unmanaged explicit key") 231 .kind(), 232 Kind::AuthorizationDenied 233 ); 234 235 let food = FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts { 236 content: FoodContent::new("Carrots available.").unwrap(), 237 identifier: FoodIdentifier::parse("carrots").unwrap(), 238 title: FoodText::new("Carrots").unwrap(), 239 summary: FoodText::new("Fresh bunches").unwrap(), 240 published_at: FoodPublishedAt::new(CREATED_AT).unwrap(), 241 location: FoodText::new("Saanich").unwrap(), 242 price: FoodPrice::new("3", FoodCurrency::parse("CAD").unwrap(), FoodUnit::Pound).unwrap(), 243 quantity: None, 244 status: FoodAvailabilityStatus::Active, 245 images: Vec::new(), 246 }) 247 .unwrap(); 248 let seller_plan = 249 AuthoredEventPlan::from_food_availability(&food, CREATED_AT, public_key().to_hex()) 250 .unwrap(); 251 assert_eq!( 252 SignRequest::new( 253 OperationId::SyncPush, 254 intent(1, 3), 255 actor(ActorSource::ExplicitPublicKey, [AuthorRole::Buyer]), 256 seller_plan, 257 policy(), 258 ) 259 .expect_err("role drift") 260 .kind(), 261 Kind::AuthorizationDenied 262 ); 263 } 264 265 #[test] 266 fn blossom_request_is_http_only_domain_separated_and_author_bound() { 267 let request = blossom_request(); 268 assert_eq!( 269 request.purpose(), 270 SigningPurpose::BlossomUploadAuthorization 271 ); 272 assert!(request.authored_plan().is_none()); 273 assert_eq!(request.blossom_authorization_plan(), Some(&blossom_plan())); 274 assert_eq!(request.authorization(), None); 275 assert_eq!(request.kind(), 24_242); 276 277 let wrong_key = 278 PublicKey::from_hex("e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af") 279 .expect("other public key"); 280 let wrong_actor = 281 Actor::new(wrong_key, ActorSource::ExplicitPublicKey, [AuthorRole::Any]).expect("actor"); 282 assert_eq!( 283 SignRequest::blossom_upload( 284 OperationId::SyncPush, 285 intent(9, 10), 286 wrong_actor, 287 blossom_plan(), 288 policy(), 289 ) 290 .expect_err("author mismatch") 291 .kind(), 292 Kind::AuthorizationDenied 293 ); 294 } 295 296 #[test] 297 fn blossom_receipt_rejects_mismatch_lateness_and_cancellation() { 298 let request = blossom_request(); 299 let tags = request 300 .tags() 301 .iter() 302 .cloned() 303 .map(nostr::Tag::parse) 304 .collect::<Result<Vec<_>, _>>() 305 .expect("BUD-11 tags"); 306 let valid = signed_event_with( 307 &keys(), 308 24_242, 309 request.content(), 310 request.created_at(), 311 tags.clone(), 312 ); 313 SignReceipt::from_signed_event(&request, valid, DEADLINE_MS - 1) 314 .expect("verified BUD-11 receipt"); 315 316 let mismatched = signed_event_with( 317 &keys(), 318 24_242, 319 "Upload a different image", 320 request.created_at(), 321 tags, 322 ); 323 assert_eq!( 324 SignReceipt::from_signed_event(&request, mismatched, DEADLINE_MS - 1) 325 .expect_err("mismatched output") 326 .kind(), 327 Kind::SignerOutputInvalid 328 ); 329 assert_eq!( 330 SignReceipt::from_signed_event( 331 &request, 332 signed_event_with( 333 &keys(), 334 24_242, 335 request.content(), 336 request.created_at(), 337 request 338 .tags() 339 .iter() 340 .cloned() 341 .map(nostr::Tag::parse) 342 .collect::<Result<Vec<_>, _>>() 343 .expect("BUD-11 tags"), 344 ), 345 DEADLINE_MS, 346 ) 347 .expect_err("late output") 348 .kind(), 349 Kind::DeadlineExceeded 350 ); 351 352 let signal = CancellationSignal::new(); 353 let cancelled = request.with_cancellation_signal(signal.clone()); 354 signal.cancel(); 355 assert_eq!( 356 SignReceipt::from_signed_event( 357 &cancelled, 358 signed_event_with( 359 &keys(), 360 24_242, 361 cancelled.content(), 362 cancelled.created_at(), 363 cancelled 364 .tags() 365 .iter() 366 .cloned() 367 .map(nostr::Tag::parse) 368 .collect::<Result<Vec<_>, _>>() 369 .expect("BUD-11 tags"), 370 ), 371 DEADLINE_MS - 1, 372 ) 373 .expect_err("cancelled output") 374 .kind(), 375 Kind::SignerCancelled 376 ); 377 } 378 379 #[test] 380 fn request_identity_deadline_and_cancellation_are_exact() { 381 let request = request(); 382 let replay = request.clone(); 383 assert_eq!(request.operation_kind(), OperationId::SyncPush); 384 assert_eq!(request.intent_id(), intent(1, 2)); 385 assert_eq!(request.actor().public_key(), public_key()); 386 assert_eq!(request.plan_digest(), plan().digest()); 387 assert_eq!(request.policy(), policy()); 388 assert!(!request.cancellation_signal().is_cancelled()); 389 assert_eq!(request.signer_request_id(), replay.signer_request_id()); 390 let other_artifact = SignRequest::new( 391 OperationId::SyncPush, 392 intent(1, 3), 393 actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]), 394 plan(), 395 policy(), 396 ) 397 .unwrap(); 398 assert_ne!( 399 request.signer_request_id(), 400 other_artifact.signer_request_id() 401 ); 402 assert!(request.ensure_active(DEADLINE_MS - 1).is_ok()); 403 assert_eq!( 404 request.ensure_active(DEADLINE_MS).unwrap_err().kind(), 405 Kind::DeadlineExceeded 406 ); 407 408 let signal = CancellationSignal::new(); 409 let cancelled = request.clone().with_cancellation_signal(signal.clone()); 410 signal.cancel(); 411 assert_eq!( 412 cancelled.ensure_active(DEADLINE_MS - 1).unwrap_err().kind(), 413 Kind::SignerCancelled 414 ); 415 416 struct Counter(std::sync::atomic::AtomicUsize); 417 impl ProgressObserver for Counter { 418 fn on_progress(&self, _progress: &SignProgress) { 419 self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 420 } 421 } 422 let observer = std::sync::Arc::new(Counter(std::sync::atomic::AtomicUsize::new(0))); 423 let observed = request.with_progress_observer(observer.clone()); 424 observed.report_progress(&SignProgress::stage(SignProgressStage::Validating).unwrap()); 425 assert_eq!(observer.0.load(std::sync::atomic::Ordering::Relaxed), 1); 426 } 427 428 #[test] 429 fn receipt_requires_exact_fields_and_a_valid_schnorr_signature() { 430 let request = request(); 431 let receipt = SignReceipt::from_signed_event(&request, signed_event(), DEADLINE_MS - 1) 432 .expect("verified receipt"); 433 assert_eq!(receipt.intent_id(), request.intent_id()); 434 assert_eq!(receipt.signer_request_id(), request.signer_request_id()); 435 assert_eq!(receipt.operation_kind(), OperationId::SyncPush); 436 assert_eq!(receipt.completed_at_unix_ms(), DEADLINE_MS - 1); 437 assert_eq!(receipt.signed_event().id(), signed_event().id()); 438 439 let other_keys = Keys::generate(); 440 let mismatches = [ 441 signed_event_with( 442 &other_keys, 443 20_000, 444 "exact signing plan", 445 CREATED_AT, 446 Vec::new(), 447 ), 448 signed_event_with( 449 &keys(), 450 20_000, 451 "exact signing plan", 452 CREATED_AT + 1, 453 Vec::new(), 454 ), 455 signed_event_with( 456 &keys(), 457 20_001, 458 "exact signing plan", 459 CREATED_AT, 460 Vec::new(), 461 ), 462 signed_event_with( 463 &keys(), 464 20_000, 465 "exact signing plan", 466 CREATED_AT, 467 vec![nostr::Tag::parse(["t", "mismatch"]).expect("tag")], 468 ), 469 signed_event_with(&keys(), 20_000, "different content", CREATED_AT, Vec::new()), 470 ]; 471 for mismatch in mismatches { 472 assert_eq!( 473 SignReceipt::from_signed_event(&request, mismatch, DEADLINE_MS - 1) 474 .expect_err("mismatched exact plan must fail") 475 .kind(), 476 Kind::SignerOutputInvalid 477 ); 478 } 479 480 let invalid = invalid_signature_event(); 481 assert_eq!( 482 SignReceipt::from_signed_event(&request, invalid, DEADLINE_MS - 1) 483 .expect_err("invalid signature") 484 .kind(), 485 Kind::SignerOutputInvalid 486 ); 487 } 488 489 #[test] 490 fn uncertain_remote_effects_never_become_unsafe_automatic_retries() { 491 let uncertain = Error::new(Kind::SignerTimeout).with_possible_remote_effect(); 492 assert_eq!(uncertain.remote_effect(), RemoteEffect::MayHaveOccurred); 493 assert_eq!( 494 recovery_disposition( 495 ReplayCapability::ExactReplayByRequestId, 496 uncertain.remote_effect(), 497 uncertain.retryable(), 498 ), 499 RecoveryDisposition::RetryExactRequest 500 ); 501 assert_eq!( 502 recovery_disposition( 503 ReplayCapability::NonReplayable, 504 uncertain.remote_effect(), 505 uncertain.retryable(), 506 ), 507 RecoveryDisposition::Indeterminate 508 ); 509 assert_eq!( 510 recovery_disposition(ReplayCapability::LocalReplaySafe, RemoteEffect::None, true,), 511 RecoveryDisposition::RetryLocal 512 ); 513 } 514 515 #[path = "authored_signing/evidence.rs"] 516 mod evidence; 517 518 fn invalid_signature_event() -> radroots_event::SignedEvent { 519 let valid = signed_event(); 520 let mut wire = valid.wire().clone(); 521 wire.sig = "f".repeat(128); 522 let mut raw: serde_json::Value = serde_json::from_str(valid.raw_json()).expect("raw event"); 523 raw["sig"] = serde_json::Value::String(wire.sig.clone()); 524 radroots_event::SignedEvent::from_wire_verified_id( 525 wire, 526 serde_json::to_string(&raw).expect("hostile raw event"), 527 ) 528 .expect("ID-valid event with hostile signature") 529 }