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