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