this repo has no description
1use bspds::plc::{ 2 PlcError, PlcOperation, PlcService, PlcValidationContext, 3 cid_for_cbor, sign_operation, signing_key_to_did_key, 4 validate_plc_operation, validate_plc_operation_for_submission, 5 verify_operation_signature, 6}; 7use k256::ecdsa::SigningKey; 8use serde_json::json; 9use std::collections::HashMap; 10 11fn create_valid_operation() -> serde_json::Value { 12 let key = SigningKey::random(&mut rand::thread_rng()); 13 let did_key = signing_key_to_did_key(&key); 14 let op = json!({ 15 "type": "plc_operation", 16 "rotationKeys": [did_key.clone()], 17 "verificationMethods": { 18 "atproto": did_key.clone() 19 }, 20 "alsoKnownAs": ["at://test.handle"], 21 "services": { 22 "atproto_pds": { 23 "type": "AtprotoPersonalDataServer", 24 "endpoint": "https://pds.example.com" 25 } 26 }, 27 "prev": null 28 }); 29 sign_operation(&op, &key).unwrap() 30} 31 32#[test] 33fn test_validate_plc_operation_valid() { 34 let op = create_valid_operation(); 35 let result = validate_plc_operation(&op); 36 assert!(result.is_ok()); 37} 38 39#[test] 40fn test_validate_plc_operation_missing_type() { 41 let op = json!({ 42 "rotationKeys": [], 43 "verificationMethods": {}, 44 "alsoKnownAs": [], 45 "services": {}, 46 "sig": "test" 47 }); 48 let result = validate_plc_operation(&op); 49 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing type"))); 50} 51 52#[test] 53fn test_validate_plc_operation_invalid_type() { 54 let op = json!({ 55 "type": "invalid_type", 56 "sig": "test" 57 }); 58 let result = validate_plc_operation(&op); 59 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Invalid type"))); 60} 61 62#[test] 63fn test_validate_plc_operation_missing_sig() { 64 let op = json!({ 65 "type": "plc_operation", 66 "rotationKeys": [], 67 "verificationMethods": {}, 68 "alsoKnownAs": [], 69 "services": {} 70 }); 71 let result = validate_plc_operation(&op); 72 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing sig"))); 73} 74 75#[test] 76fn test_validate_plc_operation_missing_rotation_keys() { 77 let op = json!({ 78 "type": "plc_operation", 79 "verificationMethods": {}, 80 "alsoKnownAs": [], 81 "services": {}, 82 "sig": "test" 83 }); 84 let result = validate_plc_operation(&op); 85 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("rotationKeys"))); 86} 87 88#[test] 89fn test_validate_plc_operation_missing_verification_methods() { 90 let op = json!({ 91 "type": "plc_operation", 92 "rotationKeys": [], 93 "alsoKnownAs": [], 94 "services": {}, 95 "sig": "test" 96 }); 97 let result = validate_plc_operation(&op); 98 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods"))); 99} 100 101#[test] 102fn test_validate_plc_operation_missing_also_known_as() { 103 let op = json!({ 104 "type": "plc_operation", 105 "rotationKeys": [], 106 "verificationMethods": {}, 107 "services": {}, 108 "sig": "test" 109 }); 110 let result = validate_plc_operation(&op); 111 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("alsoKnownAs"))); 112} 113 114#[test] 115fn test_validate_plc_operation_missing_services() { 116 let op = json!({ 117 "type": "plc_operation", 118 "rotationKeys": [], 119 "verificationMethods": {}, 120 "alsoKnownAs": [], 121 "sig": "test" 122 }); 123 let result = validate_plc_operation(&op); 124 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("services"))); 125} 126 127#[test] 128fn test_validate_rotation_key_required() { 129 let key = SigningKey::random(&mut rand::thread_rng()); 130 let did_key = signing_key_to_did_key(&key); 131 let server_key = "did:key:zServer123"; 132 let op = json!({ 133 "type": "plc_operation", 134 "rotationKeys": [did_key.clone()], 135 "verificationMethods": {"atproto": did_key.clone()}, 136 "alsoKnownAs": ["at://test.handle"], 137 "services": { 138 "atproto_pds": { 139 "type": "AtprotoPersonalDataServer", 140 "endpoint": "https://pds.example.com" 141 } 142 }, 143 "sig": "test" 144 }); 145 let ctx = PlcValidationContext { 146 server_rotation_key: server_key.to_string(), 147 expected_signing_key: did_key.clone(), 148 expected_handle: "test.handle".to_string(), 149 expected_pds_endpoint: "https://pds.example.com".to_string(), 150 }; 151 let result = validate_plc_operation_for_submission(&op, &ctx); 152 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("rotation key"))); 153} 154 155#[test] 156fn test_validate_signing_key_match() { 157 let key = SigningKey::random(&mut rand::thread_rng()); 158 let did_key = signing_key_to_did_key(&key); 159 let wrong_key = "did:key:zWrongKey456"; 160 let op = json!({ 161 "type": "plc_operation", 162 "rotationKeys": [did_key.clone()], 163 "verificationMethods": {"atproto": wrong_key}, 164 "alsoKnownAs": ["at://test.handle"], 165 "services": { 166 "atproto_pds": { 167 "type": "AtprotoPersonalDataServer", 168 "endpoint": "https://pds.example.com" 169 } 170 }, 171 "sig": "test" 172 }); 173 let ctx = PlcValidationContext { 174 server_rotation_key: did_key.clone(), 175 expected_signing_key: did_key.clone(), 176 expected_handle: "test.handle".to_string(), 177 expected_pds_endpoint: "https://pds.example.com".to_string(), 178 }; 179 let result = validate_plc_operation_for_submission(&op, &ctx); 180 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("signing key"))); 181} 182 183#[test] 184fn test_validate_handle_match() { 185 let key = SigningKey::random(&mut rand::thread_rng()); 186 let did_key = signing_key_to_did_key(&key); 187 let op = json!({ 188 "type": "plc_operation", 189 "rotationKeys": [did_key.clone()], 190 "verificationMethods": {"atproto": did_key.clone()}, 191 "alsoKnownAs": ["at://wrong.handle"], 192 "services": { 193 "atproto_pds": { 194 "type": "AtprotoPersonalDataServer", 195 "endpoint": "https://pds.example.com" 196 } 197 }, 198 "sig": "test" 199 }); 200 let ctx = PlcValidationContext { 201 server_rotation_key: did_key.clone(), 202 expected_signing_key: did_key.clone(), 203 expected_handle: "test.handle".to_string(), 204 expected_pds_endpoint: "https://pds.example.com".to_string(), 205 }; 206 let result = validate_plc_operation_for_submission(&op, &ctx); 207 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("handle"))); 208} 209 210#[test] 211fn test_validate_pds_service_type() { 212 let key = SigningKey::random(&mut rand::thread_rng()); 213 let did_key = signing_key_to_did_key(&key); 214 let op = json!({ 215 "type": "plc_operation", 216 "rotationKeys": [did_key.clone()], 217 "verificationMethods": {"atproto": did_key.clone()}, 218 "alsoKnownAs": ["at://test.handle"], 219 "services": { 220 "atproto_pds": { 221 "type": "WrongServiceType", 222 "endpoint": "https://pds.example.com" 223 } 224 }, 225 "sig": "test" 226 }); 227 let ctx = PlcValidationContext { 228 server_rotation_key: did_key.clone(), 229 expected_signing_key: did_key.clone(), 230 expected_handle: "test.handle".to_string(), 231 expected_pds_endpoint: "https://pds.example.com".to_string(), 232 }; 233 let result = validate_plc_operation_for_submission(&op, &ctx); 234 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("type"))); 235} 236 237#[test] 238fn test_validate_pds_endpoint_match() { 239 let key = SigningKey::random(&mut rand::thread_rng()); 240 let did_key = signing_key_to_did_key(&key); 241 let op = json!({ 242 "type": "plc_operation", 243 "rotationKeys": [did_key.clone()], 244 "verificationMethods": {"atproto": did_key.clone()}, 245 "alsoKnownAs": ["at://test.handle"], 246 "services": { 247 "atproto_pds": { 248 "type": "AtprotoPersonalDataServer", 249 "endpoint": "https://wrong.endpoint.com" 250 } 251 }, 252 "sig": "test" 253 }); 254 let ctx = PlcValidationContext { 255 server_rotation_key: did_key.clone(), 256 expected_signing_key: did_key.clone(), 257 expected_handle: "test.handle".to_string(), 258 expected_pds_endpoint: "https://pds.example.com".to_string(), 259 }; 260 let result = validate_plc_operation_for_submission(&op, &ctx); 261 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("endpoint"))); 262} 263 264#[test] 265fn test_verify_signature_secp256k1() { 266 let key = SigningKey::random(&mut rand::thread_rng()); 267 let did_key = signing_key_to_did_key(&key); 268 let op = json!({ 269 "type": "plc_operation", 270 "rotationKeys": [did_key.clone()], 271 "verificationMethods": {}, 272 "alsoKnownAs": [], 273 "services": {}, 274 "prev": null 275 }); 276 let signed = sign_operation(&op, &key).unwrap(); 277 let rotation_keys = vec![did_key]; 278 let result = verify_operation_signature(&signed, &rotation_keys); 279 assert!(result.is_ok()); 280 assert!(result.unwrap()); 281} 282 283#[test] 284fn test_verify_signature_wrong_key() { 285 let key = SigningKey::random(&mut rand::thread_rng()); 286 let other_key = SigningKey::random(&mut rand::thread_rng()); 287 let other_did_key = signing_key_to_did_key(&other_key); 288 let op = json!({ 289 "type": "plc_operation", 290 "rotationKeys": [], 291 "verificationMethods": {}, 292 "alsoKnownAs": [], 293 "services": {}, 294 "prev": null 295 }); 296 let signed = sign_operation(&op, &key).unwrap(); 297 let wrong_rotation_keys = vec![other_did_key]; 298 let result = verify_operation_signature(&signed, &wrong_rotation_keys); 299 assert!(result.is_ok()); 300 assert!(!result.unwrap()); 301} 302 303#[test] 304fn test_verify_signature_invalid_did_key_format() { 305 let key = SigningKey::random(&mut rand::thread_rng()); 306 let op = json!({ 307 "type": "plc_operation", 308 "rotationKeys": [], 309 "verificationMethods": {}, 310 "alsoKnownAs": [], 311 "services": {}, 312 "prev": null 313 }); 314 let signed = sign_operation(&op, &key).unwrap(); 315 let invalid_keys = vec!["not-a-did-key".to_string()]; 316 let result = verify_operation_signature(&signed, &invalid_keys); 317 assert!(result.is_ok()); 318 assert!(!result.unwrap()); 319} 320 321#[test] 322fn test_tombstone_validation() { 323 let op = json!({ 324 "type": "plc_tombstone", 325 "prev": "bafyreig6xxxxxyyyyyzzzzzz", 326 "sig": "test" 327 }); 328 let result = validate_plc_operation(&op); 329 assert!(result.is_ok()); 330} 331 332#[test] 333fn test_cid_for_cbor_deterministic() { 334 let value = json!({ 335 "alpha": 1, 336 "beta": 2 337 }); 338 let cid1 = cid_for_cbor(&value).unwrap(); 339 let cid2 = cid_for_cbor(&value).unwrap(); 340 assert_eq!(cid1, cid2, "CID generation should be deterministic"); 341 assert!(cid1.starts_with("bafyrei"), "CID should start with bafyrei (dag-cbor + sha256)"); 342} 343 344#[test] 345fn test_cid_different_for_different_data() { 346 let value1 = json!({"data": 1}); 347 let value2 = json!({"data": 2}); 348 let cid1 = cid_for_cbor(&value1).unwrap(); 349 let cid2 = cid_for_cbor(&value2).unwrap(); 350 assert_ne!(cid1, cid2, "Different data should produce different CIDs"); 351} 352 353#[test] 354fn test_signing_key_to_did_key_format() { 355 let key = SigningKey::random(&mut rand::thread_rng()); 356 let did_key = signing_key_to_did_key(&key); 357 assert!(did_key.starts_with("did:key:z"), "Should start with did:key:z"); 358 assert!(did_key.len() > 50, "Did key should be reasonably long"); 359} 360 361#[test] 362fn test_signing_key_to_did_key_unique() { 363 let key1 = SigningKey::random(&mut rand::thread_rng()); 364 let key2 = SigningKey::random(&mut rand::thread_rng()); 365 let did1 = signing_key_to_did_key(&key1); 366 let did2 = signing_key_to_did_key(&key2); 367 assert_ne!(did1, did2, "Different keys should produce different did:keys"); 368} 369 370#[test] 371fn test_signing_key_to_did_key_consistent() { 372 let key = SigningKey::random(&mut rand::thread_rng()); 373 let did1 = signing_key_to_did_key(&key); 374 let did2 = signing_key_to_did_key(&key); 375 assert_eq!(did1, did2, "Same key should produce same did:key"); 376} 377 378#[test] 379fn test_sign_operation_removes_existing_sig() { 380 let key = SigningKey::random(&mut rand::thread_rng()); 381 let op = json!({ 382 "type": "plc_operation", 383 "rotationKeys": [], 384 "verificationMethods": {}, 385 "alsoKnownAs": [], 386 "services": {}, 387 "prev": null, 388 "sig": "old_signature" 389 }); 390 let signed = sign_operation(&op, &key).unwrap(); 391 let new_sig = signed.get("sig").and_then(|v| v.as_str()).unwrap(); 392 assert_ne!(new_sig, "old_signature", "Should replace old signature"); 393} 394 395#[test] 396fn test_validate_plc_operation_not_object() { 397 let result = validate_plc_operation(&json!("not an object")); 398 assert!(matches!(result, Err(PlcError::InvalidResponse(_)))); 399} 400 401#[test] 402fn test_validate_for_submission_tombstone_passes() { 403 let key = SigningKey::random(&mut rand::thread_rng()); 404 let did_key = signing_key_to_did_key(&key); 405 let op = json!({ 406 "type": "plc_tombstone", 407 "prev": "bafyreig6xxxxxyyyyyzzzzzz", 408 "sig": "test" 409 }); 410 let ctx = PlcValidationContext { 411 server_rotation_key: did_key.clone(), 412 expected_signing_key: did_key, 413 expected_handle: "test.handle".to_string(), 414 expected_pds_endpoint: "https://pds.example.com".to_string(), 415 }; 416 let result = validate_plc_operation_for_submission(&op, &ctx); 417 assert!(result.is_ok(), "Tombstone should pass submission validation"); 418} 419 420#[test] 421fn test_verify_signature_missing_sig() { 422 let op = json!({ 423 "type": "plc_operation", 424 "rotationKeys": [], 425 "verificationMethods": {}, 426 "alsoKnownAs": [], 427 "services": {} 428 }); 429 let result = verify_operation_signature(&op, &[]); 430 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("sig"))); 431} 432 433#[test] 434fn test_verify_signature_invalid_base64() { 435 let op = json!({ 436 "type": "plc_operation", 437 "rotationKeys": [], 438 "verificationMethods": {}, 439 "alsoKnownAs": [], 440 "services": {}, 441 "sig": "not-valid-base64!!!" 442 }); 443 let result = verify_operation_signature(&op, &[]); 444 assert!(matches!(result, Err(PlcError::InvalidResponse(_)))); 445} 446 447#[test] 448fn test_plc_operation_struct() { 449 let mut services = HashMap::new(); 450 services.insert("atproto_pds".to_string(), PlcService { 451 service_type: "AtprotoPersonalDataServer".to_string(), 452 endpoint: "https://pds.example.com".to_string(), 453 }); 454 let mut verification_methods = HashMap::new(); 455 verification_methods.insert("atproto".to_string(), "did:key:zTest123".to_string()); 456 let op = PlcOperation { 457 op_type: "plc_operation".to_string(), 458 rotation_keys: vec!["did:key:zTest123".to_string()], 459 verification_methods, 460 also_known_as: vec!["at://test.handle".to_string()], 461 services, 462 prev: None, 463 sig: Some("test".to_string()), 464 }; 465 let json_value = serde_json::to_value(&op).unwrap(); 466 assert_eq!(json_value["type"], "plc_operation"); 467 assert!(json_value["rotationKeys"].is_array()); 468}