services_hardening_nip46_verification.rs (16412B)
1 #![forbid(unsafe_code)] 2 3 use std::error::Error; 4 5 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; 6 use myc::{ 7 MycConfigProfile, MycNip46AdmissionLimits, MycNip46AuthoredTimePolicy, 8 MycNip46EncryptionContext, MycNip46ObservedAtUnixSeconds, MycNip46VerificationErrorKind, 9 MycProviderBinding, MycProviderRole, admit_myc_nip46_event, admit_myc_nip46_request, 10 parse_myc_config_v1, verify_myc_nip46_event, verify_myc_nip46_request, 11 }; 12 use nostr::{ 13 Event, EventBuilder, EventId, Keys, Kind, Tag, Timestamp, 14 nips::{nip04, nip44}, 15 }; 16 use serde_json::{Value, json}; 17 18 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 19 const CONTRACT: &str = include_str!("../contracts/services_hardening/nip46_verification.v1.json"); 20 const OBSERVED_AT: u64 = 1_725_000_000; 21 22 fn limits() -> MycNip46AdmissionLimits { 23 let config = 24 parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::RepoLocal).expect("test config"); 25 MycNip46AdmissionLimits::from_config(&config).expect("limits") 26 } 27 28 fn keys(seed: u8) -> Keys { 29 Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys") 30 } 31 32 fn provider_binding(role: MycProviderRole, keys: &Keys) -> MycProviderBinding { 33 let (section, original) = match role { 34 MycProviderRole::Transport => ( 35 "[identity.transport]", 36 "4444444444444444444444444444444444444444444444444444444444444444", 37 ), 38 MycProviderRole::User => ( 39 "[identity.user]", 40 "2222222222222222222222222222222222222222222222222222222222222222", 41 ), 42 MycProviderRole::Discovery => ( 43 "[identity.discovery.binding]", 44 "3333333333333333333333333333333333333333333333333333333333333333", 45 ), 46 }; 47 let section_start = CONFIG.find(section).expect("identity section"); 48 let mut source = CONFIG.to_owned(); 49 let relative = source[section_start..] 50 .find(original) 51 .expect("configured identity"); 52 let identity_start = section_start + relative; 53 source.replace_range( 54 identity_start..identity_start + original.len(), 55 &keys.public_key().to_hex(), 56 ); 57 parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal) 58 .expect("test config") 59 .provider_contract() 60 .binding(role) 61 .expect("provider binding") 62 .clone() 63 } 64 65 fn signed_event(sender: &Keys, receiver: &Keys, content: String, created_at: u64) -> Event { 66 EventBuilder::new(Kind::Custom(24_133), content) 67 .tag(Tag::public_key(receiver.public_key())) 68 .custom_created_at(Timestamp::from_secs(created_at)) 69 .sign_with_keys(sender) 70 .expect("signed event") 71 } 72 73 fn verify_event( 74 event: &Event, 75 receiver_keys: &Keys, 76 policy: MycNip46AuthoredTimePolicy, 77 ) -> Result<myc::MycVerifiedNip46Event, myc::MycNip46VerificationError> { 78 let wire = serde_json::to_vec(event).expect("event JSON"); 79 let bounded = admit_myc_nip46_event(limits(), &wire).expect("bounded event"); 80 verify_myc_nip46_event( 81 bounded, 82 &provider_binding(MycProviderRole::Transport, receiver_keys), 83 MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observation"), 84 policy, 85 ) 86 } 87 88 fn request(id: &str, method: &str, params: Value) -> Vec<u8> { 89 serde_json::to_vec(&json!({"id": id, "method": method, "params": params})) 90 .expect("request JSON") 91 } 92 93 #[test] 94 fn machine_contract_freezes_exact_step_boundary() { 95 let contract: Value = serde_json::from_str(CONTRACT).expect("verification contract"); 96 assert_eq!(contract["schema"], "radroots.myc.nip46-verification.v1"); 97 assert_eq!(contract["contract_version"], 1); 98 assert_eq!(contract["event"]["kind"], 24_133); 99 assert_eq!(contract["authored_time"]["default"], "none"); 100 assert_eq!( 101 contract["encryption"]["contexts"], 102 json!(["nip04", "nip44_v2"]) 103 ); 104 assert_eq!(contract["encryption"]["decryption"], "deferred_to_step_145"); 105 assert_eq!( 106 contract["request"]["replay_and_conflicting_reuse"], 107 "deferred_to_step_143" 108 ); 109 } 110 111 #[test] 112 fn signed_nip44_and_nip04_events_verify_without_decrypting() { 113 let sender = keys(1); 114 let target = keys(2); 115 let policy = MycNip46AuthoredTimePolicy::new(60, 10).expect("policy"); 116 let plaintext = br#"{"id":"request-secret-marker","method":"ping","params":[]}"#; 117 118 let nip44_content = nip44::encrypt( 119 sender.secret_key(), 120 &target.public_key(), 121 plaintext, 122 nip44::Version::V2, 123 ) 124 .expect("NIP-44 encrypt"); 125 let event = signed_event(&sender, &target, nip44_content.clone(), OBSERVED_AT); 126 let wire = serde_json::to_vec(&event).expect("event wire"); 127 let verified = verify_event(&event, &target, policy).expect("verified NIP-44 event"); 128 assert_eq!(verified.original_bytes(), wire); 129 assert_eq!(verified.event_id().as_bytes(), &event.id.to_bytes()); 130 assert_eq!( 131 verified.client_public_key().as_hex(), 132 sender.public_key().to_hex() 133 ); 134 assert_eq!(verified.authored_at_unix_seconds(), OBSERVED_AT); 135 assert_eq!( 136 verified.encryption_context(), 137 MycNip46EncryptionContext::Nip44V2 138 ); 139 assert_eq!(verified.encrypted_content(), nip44_content); 140 141 let nip04_content = nip04::encrypt(sender.secret_key(), &target.public_key(), plaintext) 142 .expect("NIP-04 encrypt"); 143 let event = signed_event(&sender, &target, nip04_content, OBSERVED_AT); 144 assert_eq!( 145 verify_event(&event, &target, policy) 146 .expect("verified NIP-04 event") 147 .encryption_context(), 148 MycNip46EncryptionContext::Nip04 149 ); 150 } 151 152 #[test] 153 fn canonical_kind_sender_and_exact_receiver_shape_fail_closed() { 154 let sender = keys(3); 155 let target = keys(4); 156 let policy = MycNip46AuthoredTimePolicy::new(60, 10).expect("policy"); 157 let content = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat()); 158 159 let wrong_kind = EventBuilder::new(Kind::Custom(24_132), content.clone()) 160 .tag(Tag::public_key(target.public_key())) 161 .custom_created_at(Timestamp::from_secs(OBSERVED_AT)) 162 .sign_with_keys(&sender) 163 .expect("wrong-kind event"); 164 assert_eq!( 165 verify_event(&wrong_kind, &target, policy) 166 .unwrap_err() 167 .kind(), 168 MycNip46VerificationErrorKind::InvalidKind 169 ); 170 171 let self_sent = signed_event(&target, &target, content.clone(), OBSERVED_AT); 172 assert_eq!( 173 verify_event(&self_sent, &target, policy) 174 .unwrap_err() 175 .kind(), 176 MycNip46VerificationErrorKind::InvalidSender 177 ); 178 179 let wrong_target = keys(5); 180 let wrong_recipient = signed_event(&sender, &wrong_target, content.clone(), OBSERVED_AT); 181 assert_eq!( 182 verify_event(&wrong_recipient, &target, policy) 183 .unwrap_err() 184 .kind(), 185 MycNip46VerificationErrorKind::InvalidReceiver 186 ); 187 188 let extra_tag = EventBuilder::new(Kind::Custom(24_133), content) 189 .tags([ 190 Tag::public_key(target.public_key()), 191 Tag::parse(["client", "untrusted-metadata"]).expect("extra tag"), 192 ]) 193 .custom_created_at(Timestamp::from_secs(OBSERVED_AT)) 194 .sign_with_keys(&sender) 195 .expect("extra-tag event"); 196 assert_eq!( 197 verify_event(&extra_tag, &target, policy) 198 .unwrap_err() 199 .kind(), 200 MycNip46VerificationErrorKind::InvalidReceiver 201 ); 202 } 203 204 #[test] 205 fn identifiers_event_id_and_signature_are_independently_verified() { 206 let sender = keys(6); 207 let target = keys(7); 208 let policy = MycNip46AuthoredTimePolicy::new(60, 10).expect("policy"); 209 let payload = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat()); 210 let event = signed_event(&sender, &target, payload.clone(), OBSERVED_AT); 211 212 let wire = String::from_utf8(serde_json::to_vec(&event).expect("wire")).expect("UTF-8"); 213 let uppercase = wire.replacen(&event.id.to_hex(), &event.id.to_hex().to_uppercase(), 1); 214 let bounded = admit_myc_nip46_event(limits(), uppercase.as_bytes()).expect("bounded uppercase"); 215 assert_eq!( 216 verify_myc_nip46_event( 217 bounded, 218 &provider_binding(MycProviderRole::Transport, &target), 219 MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observed"), 220 policy, 221 ) 222 .unwrap_err() 223 .kind(), 224 MycNip46VerificationErrorKind::NonCanonicalEvent 225 ); 226 227 let mut wrong_id = event.clone(); 228 wrong_id.id = EventId::all_zeros(); 229 assert_eq!( 230 verify_event(&wrong_id, &target, policy).unwrap_err().kind(), 231 MycNip46VerificationErrorKind::InvalidEventId 232 ); 233 234 let other = signed_event(&keys(8), &target, payload, OBSERVED_AT); 235 let mut wrong_signature = event; 236 wrong_signature.sig = other.sig; 237 assert_eq!( 238 verify_event(&wrong_signature, &target, policy) 239 .unwrap_err() 240 .kind(), 241 MycNip46VerificationErrorKind::InvalidSignature 242 ); 243 } 244 245 #[test] 246 fn authored_time_policy_is_explicit_inclusive_and_overflow_safe() { 247 assert!(MycNip46ObservedAtUnixSeconds::new(0).is_err()); 248 assert!(MycNip46ObservedAtUnixSeconds::new(i64::MAX as u64).is_ok()); 249 assert!(MycNip46ObservedAtUnixSeconds::new(i64::MAX as u64 + 1).is_err()); 250 assert!(MycNip46AuthoredTimePolicy::new(i64::MAX as u64, 0).is_ok()); 251 assert!(MycNip46AuthoredTimePolicy::new(i64::MAX as u64 + 1, 0).is_err()); 252 253 let sender = keys(9); 254 let target = keys(10); 255 let payload = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat()); 256 let policy = MycNip46AuthoredTimePolicy::new(10, 2).expect("policy"); 257 for accepted in [OBSERVED_AT - 10, OBSERVED_AT + 2] { 258 verify_event( 259 &signed_event(&sender, &target, payload.clone(), accepted), 260 &target, 261 policy, 262 ) 263 .expect("inclusive boundary"); 264 } 265 for rejected in [OBSERVED_AT - 11, OBSERVED_AT + 3] { 266 assert_eq!( 267 verify_event( 268 &signed_event(&sender, &target, payload.clone(), rejected), 269 &target, 270 policy, 271 ) 272 .unwrap_err() 273 .kind(), 274 MycNip46VerificationErrorKind::AuthoredTimeRejected 275 ); 276 } 277 } 278 279 #[test] 280 fn only_the_configured_transport_role_can_supply_receiver_authority() { 281 let sender = keys(15); 282 let target = keys(16); 283 let payload = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat()); 284 let event = signed_event(&sender, &target, payload, OBSERVED_AT); 285 let wire = serde_json::to_vec(&event).expect("event wire"); 286 let bounded = admit_myc_nip46_event(limits(), &wire).expect("bounded event"); 287 let error = verify_myc_nip46_event( 288 bounded, 289 &provider_binding(MycProviderRole::User, &target), 290 MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observation"), 291 MycNip46AuthoredTimePolicy::new(0, 0).expect("policy"), 292 ) 293 .expect_err("user identity cannot become transport receiver authority"); 294 assert_eq!( 295 error.kind(), 296 MycNip46VerificationErrorKind::InvalidTransportBinding 297 ); 298 } 299 300 #[test] 301 fn nip04_and_nip44_envelope_shapes_are_canonical_and_versioned() { 302 let sender = keys(11); 303 let target = keys(12); 304 let policy = MycNip46AuthoredTimePolicy::new(1, 1).expect("policy"); 305 306 let invalid_nip04 = signed_event(&sender, &target, "AAAA?iv=AAAA".to_owned(), OBSERVED_AT); 307 assert_eq!( 308 verify_event(&invalid_nip04, &target, policy) 309 .unwrap_err() 310 .kind(), 311 MycNip46VerificationErrorKind::InvalidNip04Envelope 312 ); 313 314 let mut wrong_version = vec![0; 99]; 315 wrong_version[0] = 3; 316 let event = signed_event( 317 &sender, 318 &target, 319 BASE64_STANDARD.encode(wrong_version), 320 OBSERVED_AT, 321 ); 322 assert_eq!( 323 verify_event(&event, &target, policy).unwrap_err().kind(), 324 MycNip46VerificationErrorKind::InvalidNip44Envelope 325 ); 326 327 let mut invalid_padding = vec![0; 100]; 328 invalid_padding[0] = 2; 329 let event = signed_event( 330 &sender, 331 &target, 332 BASE64_STANDARD.encode(invalid_padding), 333 OBSERVED_AT, 334 ); 335 assert_eq!( 336 verify_event(&event, &target, policy).unwrap_err().kind(), 337 MycNip46VerificationErrorKind::InvalidNip44Envelope 338 ); 339 } 340 341 #[test] 342 fn exact_typed_request_decoding_accepts_governed_and_custom_methods_only_when_semantic() { 343 let ping = request("request-01", "ping", json!([])); 344 let bounded = admit_myc_nip46_request(limits(), &ping).expect("bounded ping"); 345 let verified = verify_myc_nip46_request(bounded).expect("typed ping"); 346 assert_eq!(verified.request_id().as_str(), "request-01"); 347 assert_eq!(verified.method(), "ping"); 348 assert_eq!(verified.parameter_count(), 0); 349 assert_eq!(verified.parameter_bytes(), 0); 350 351 let custom = request("custom-01", "vendor_method", json!(["a", "bc"])); 352 let bounded = admit_myc_nip46_request(limits(), &custom).expect("bounded custom"); 353 let verified = verify_myc_nip46_request(bounded).expect("typed custom"); 354 assert_eq!(verified.method(), "vendor_method"); 355 assert_eq!(verified.parameter_count(), 2); 356 assert_eq!(verified.parameter_bytes(), 3); 357 358 for invalid in [ 359 request("", "ping", json!([])), 360 request(" request ", "ping", json!([])), 361 request("request-02", "ping", json!(["unexpected"])), 362 request("request-03", "nip44_encrypt", json!(["not-a-key", "x"])), 363 ] { 364 let bounded = admit_myc_nip46_request(limits(), &invalid).expect("structurally bounded"); 365 assert_eq!( 366 verify_myc_nip46_request(bounded).unwrap_err().kind(), 367 MycNip46VerificationErrorKind::InvalidTypedRequest 368 ); 369 } 370 } 371 372 #[test] 373 fn verified_capabilities_and_errors_redact_protected_content() { 374 let sender = keys(13); 375 let target = keys(14); 376 let marker = br#"{"id":"protected-request-marker","method":"ping","params":[]}"#; 377 let ciphertext = nip44::encrypt( 378 sender.secret_key(), 379 &target.public_key(), 380 marker, 381 nip44::Version::V2, 382 ) 383 .expect("ciphertext"); 384 let event = signed_event(&sender, &target, ciphertext.clone(), OBSERVED_AT); 385 let verified_event = verify_event( 386 &event, 387 &target, 388 MycNip46AuthoredTimePolicy::new(0, 0).expect("policy"), 389 ) 390 .expect("verified event"); 391 let rendered = format!("{verified_event:?}"); 392 assert!(!rendered.contains(&ciphertext)); 393 assert!(!rendered.contains(&sender.public_key().to_hex())); 394 395 let bounded = admit_myc_nip46_request(limits(), marker).expect("bounded request"); 396 let verified_request = verify_myc_nip46_request(bounded).expect("verified request"); 397 let rendered = format!("{verified_request:?}"); 398 assert!(!rendered.contains("protected-request-marker")); 399 assert!(!rendered.contains("ping")); 400 401 for kind in [ 402 MycNip46VerificationErrorKind::InvalidObservationTime, 403 MycNip46VerificationErrorKind::InvalidTimePolicy, 404 MycNip46VerificationErrorKind::InvalidTransportBinding, 405 MycNip46VerificationErrorKind::MalformedEvent, 406 MycNip46VerificationErrorKind::NonCanonicalEvent, 407 MycNip46VerificationErrorKind::InvalidKind, 408 MycNip46VerificationErrorKind::InvalidSender, 409 MycNip46VerificationErrorKind::InvalidReceiver, 410 MycNip46VerificationErrorKind::InvalidEventId, 411 MycNip46VerificationErrorKind::InvalidSignature, 412 MycNip46VerificationErrorKind::AuthoredTimeRejected, 413 MycNip46VerificationErrorKind::InvalidNip04Envelope, 414 MycNip46VerificationErrorKind::InvalidNip44Envelope, 415 MycNip46VerificationErrorKind::InvalidTypedRequest, 416 ] { 417 let error = if kind == MycNip46VerificationErrorKind::InvalidObservationTime { 418 MycNip46ObservedAtUnixSeconds::new(0).unwrap_err() 419 } else if kind == MycNip46VerificationErrorKind::InvalidTimePolicy { 420 MycNip46AuthoredTimePolicy::new(u64::MAX, 0).unwrap_err() 421 } else { 422 continue; 423 }; 424 assert_eq!(error.kind(), kind); 425 assert!(!error.to_string().is_empty()); 426 assert!(error.source().is_none()); 427 } 428 }