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