services_hardening_nip46_replay.rs (8841B)
1 #![forbid(unsafe_code)] 2 3 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; 4 use myc::{ 5 MycConfigProfile, MycNip46AdmissionLimits, MycNip46AuthoredTimePolicy, 6 MycNip46ObservedAtUnixSeconds, MycNip46ReplayDisposition, MycProviderBinding, MycProviderRole, 7 admit_myc_nip46_event, admit_myc_nip46_request, bind_myc_nip46_replay, parse_myc_config_v1, 8 verify_myc_nip46_event, verify_myc_nip46_request, 9 }; 10 use nostr::{Event, EventBuilder, Keys, Kind, Tag, Timestamp}; 11 use serde_json::{Value, json}; 12 13 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 14 const CONTRACT: &str = include_str!("../contracts/services_hardening/nip46_replay.v1.json"); 15 const OBSERVED_AT: u64 = 1_725_000_000; 16 17 fn limits() -> MycNip46AdmissionLimits { 18 let config = 19 parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::RepoLocal).expect("test config"); 20 MycNip46AdmissionLimits::from_config(&config).expect("limits") 21 } 22 23 fn keys(seed: u8) -> Keys { 24 Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys") 25 } 26 27 fn provider_binding(target: &Keys) -> MycProviderBinding { 28 let section_start = CONFIG 29 .find("[identity.transport]") 30 .expect("transport section"); 31 let original = "4444444444444444444444444444444444444444444444444444444444444444"; 32 let mut source = CONFIG.to_owned(); 33 let relative = source[section_start..] 34 .find(original) 35 .expect("configured identity"); 36 let identity_start = section_start + relative; 37 source.replace_range( 38 identity_start..identity_start + original.len(), 39 &target.public_key().to_hex(), 40 ); 41 parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal) 42 .expect("test config") 43 .provider_contract() 44 .binding(MycProviderRole::Transport) 45 .expect("transport binding") 46 .clone() 47 } 48 49 fn event(sender: &Keys, target: &Keys, created_at: u64) -> Event { 50 let content = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat()); 51 EventBuilder::new(Kind::Custom(24_133), content) 52 .tag(Tag::public_key(target.public_key())) 53 .custom_created_at(Timestamp::from_secs(created_at)) 54 .sign_with_keys(sender) 55 .expect("signed event") 56 } 57 58 fn candidate( 59 sender: &Keys, 60 target: &Keys, 61 created_at: u64, 62 request_id: &str, 63 method: &str, 64 params: Value, 65 ) -> myc::MycReplayBoundNip46Request { 66 let plaintext = serde_json::to_vec(&json!({ 67 "id": request_id, 68 "method": method, 69 "params": params 70 })) 71 .expect("request wire"); 72 candidate_from_plaintext(sender, target, created_at, &plaintext) 73 } 74 75 fn candidate_from_plaintext( 76 sender: &Keys, 77 target: &Keys, 78 created_at: u64, 79 plaintext: &[u8], 80 ) -> myc::MycReplayBoundNip46Request { 81 let event = event(sender, target, created_at); 82 let wire = serde_json::to_vec(&event).expect("event wire"); 83 let event = verify_myc_nip46_event( 84 admit_myc_nip46_event(limits(), &wire).expect("bounded event"), 85 &provider_binding(target), 86 MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observed"), 87 MycNip46AuthoredTimePolicy::new(60, 60).expect("policy"), 88 ) 89 .expect("verified event"); 90 let request = verify_myc_nip46_request( 91 admit_myc_nip46_request(limits(), plaintext).expect("bounded request"), 92 ) 93 .expect("verified request"); 94 bind_myc_nip46_replay(event, request).expect("replay binding") 95 } 96 97 fn hex(bytes: &[u8]) -> String { 98 bytes.iter().map(|byte| format!("{byte:02x}")).collect() 99 } 100 101 #[test] 102 fn machine_contract_freezes_identity_framing_and_authority_split() { 103 let contract: Value = serde_json::from_str(CONTRACT).expect("replay contract"); 104 assert_eq!(contract["schema"], "radroots.myc.nip46-replay.v1"); 105 assert_eq!(contract["contract_version"], 1); 106 assert_eq!(contract["prerequisite"], "nip46_verification.v1"); 107 assert_eq!( 108 contract["connection_identity"]["domain"], 109 "radroots.myc.nip46.connection_identity.v1\\0" 110 ); 111 assert_eq!( 112 contract["request_identity"]["domain"], 113 "radroots.myc.nip46.request_identity.v1\\0" 114 ); 115 assert_eq!( 116 contract["request_digest"]["domain"], 117 "radroots.myc.nip46.request.v1\\0" 118 ); 119 assert_eq!(contract["connection_identity"]["digest"], "sha256"); 120 assert_eq!(contract["request_identity"]["digest"], "sha256"); 121 assert_eq!(contract["request_digest"]["digest"], "sha256"); 122 assert_eq!( 123 contract["durable_authority"]["new_replay_store"], 124 "forbidden" 125 ); 126 assert_eq!( 127 contract["nonclaims"], 128 json!([ 129 "ciphertext_decryption", 130 "plaintext_event_cryptographic_binding", 131 "supported_method_admission", 132 "authorization", 133 "provider_execution", 134 "database_mutation", 135 "relay_publication" 136 ]) 137 ); 138 } 139 140 #[test] 141 fn stable_connection_request_and_content_vectors_are_exact() { 142 let bound = candidate( 143 &keys(1), 144 &keys(2), 145 OBSERVED_AT, 146 "request-01", 147 "ping", 148 json!([]), 149 ); 150 assert_eq!( 151 hex(bound.connection_identity().as_bytes()), 152 "93f2c20f3c8043fdae2ec14f08e398ef5012fd813868285d3ed5c2c98ac8e1c1" 153 ); 154 assert_eq!( 155 hex(bound.request_identity().as_bytes()), 156 "68efc9cd254207260f097997205ee3e34228b290315c53c92cba054dce0452d9" 157 ); 158 assert_eq!( 159 hex(bound.request_digest().as_bytes()), 160 "378d4f7906aed41e5af96c7000dfc57d9c4c4c9ad3d94b84e985621c25e727f2" 161 ); 162 } 163 164 #[test] 165 fn duplicate_replay_conflict_and_distinct_relations_are_total() { 166 let sender = keys(3); 167 let target = keys(4); 168 let retained = candidate( 169 &sender, 170 &target, 171 OBSERVED_AT, 172 "request-01", 173 "ping", 174 json!([]), 175 ); 176 let retained_key = retained.replay_key(); 177 assert_eq!(format!("{retained_key:?}"), "MycNip46ReplayKey([redacted])"); 178 drop(retained); 179 let duplicate = candidate( 180 &sender, 181 &target, 182 OBSERVED_AT, 183 "request-01", 184 "ping", 185 json!([]), 186 ); 187 assert_eq!( 188 duplicate.classify_against(retained_key), 189 MycNip46ReplayDisposition::DuplicateEvent 190 ); 191 192 let replay = candidate( 193 &sender, 194 &target, 195 OBSERVED_AT + 1, 196 "request-01", 197 "ping", 198 json!([]), 199 ); 200 assert_eq!( 201 replay.classify_against(retained_key), 202 MycNip46ReplayDisposition::ExactRequestReplay 203 ); 204 205 let normalized_replay = candidate_from_plaintext( 206 &sender, 207 &target, 208 OBSERVED_AT + 1, 209 br#"{ "params" : [ ], "method" : "ping", "id" : "request-01" }"#, 210 ); 211 assert_eq!( 212 normalized_replay.classify_against(retained_key), 213 MycNip46ReplayDisposition::ExactRequestReplay 214 ); 215 216 let request_conflict = candidate( 217 &sender, 218 &target, 219 OBSERVED_AT + 2, 220 "request-01", 221 "vendor_method", 222 json!(["changed"]), 223 ); 224 assert_eq!( 225 request_conflict.classify_against(retained_key), 226 MycNip46ReplayDisposition::ConflictingRequestReuse 227 ); 228 229 let event_conflict = candidate( 230 &sender, 231 &target, 232 OBSERVED_AT, 233 "request-02", 234 "ping", 235 json!([]), 236 ); 237 assert_eq!( 238 event_conflict.classify_against(retained_key), 239 MycNip46ReplayDisposition::ConflictingEventReuse 240 ); 241 242 let distinct = candidate( 243 &keys(5), 244 &target, 245 OBSERVED_AT + 3, 246 "request-01", 247 "ping", 248 json!([]), 249 ); 250 assert_eq!( 251 distinct.classify_against(retained_key), 252 MycNip46ReplayDisposition::DistinctRequest 253 ); 254 255 let other_receiver = candidate( 256 &sender, 257 &keys(8), 258 OBSERVED_AT + 4, 259 "request-01", 260 "ping", 261 json!([]), 262 ); 263 assert_eq!( 264 other_receiver.classify_against(retained_key), 265 MycNip46ReplayDisposition::DistinctRequest 266 ); 267 } 268 269 #[test] 270 fn binding_is_sealed_redacted_and_not_execution_authority() { 271 let sender = keys(6); 272 let target = keys(7); 273 let bound = candidate( 274 &sender, 275 &target, 276 OBSERVED_AT, 277 "protected-request-marker", 278 "vendor_secret_method", 279 json!(["protected-parameter-marker"]), 280 ); 281 let rendered = format!("{bound:?}"); 282 for secret in [ 283 "protected-request-marker", 284 "vendor_secret_method", 285 "protected-parameter-marker", 286 &sender.public_key().to_hex(), 287 ] { 288 assert!(!rendered.contains(secret)); 289 } 290 assert_eq!( 291 rendered, 292 "MycReplayBoundNip46Request { identity: \"[redacted]\" }" 293 ); 294 }