A library for ATProtocol identities.

bug: verify signature referenced invalid field type

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

Changed files
+584 -13
crates
+1 -1
crates/atproto-jetstream/src/consumer.rs
··· 153 pub(crate) enum SubscriberSourcedMessage { 154 #[serde(rename = "options_update")] 155 Update { 156 - #[serde(rename = "wantedCollections")] 157 wanted_collections: Vec<String>, 158 159 #[serde(rename = "wantedDids", skip_serializing_if = "Vec::is_empty", default)]
··· 153 pub(crate) enum SubscriberSourcedMessage { 154 #[serde(rename = "options_update")] 155 Update { 156 + #[serde(rename = "wantedCollections", skip_serializing_if = "Vec::is_empty", default)] 157 wanted_collections: Vec<String>, 158 159 #[serde(rename = "wantedDids", skip_serializing_if = "Vec::is_empty", default)]
+12 -4
crates/atproto-record/src/bytes.rs
··· 21 //! 22 //! ## Base64 Encoding 23 //! 24 - //! The serialization uses standard base64 encoding (RFC 4648) with padding, 25 - //! compatible with the `$bytes` field format used throughout AT Protocol. 26 27 /// Base64 serialization format for byte arrays. 28 /// ··· 34 use serde::{Deserialize, Serialize}; 35 use serde::{Deserializer, Serializer}; 36 37 - use base64::{Engine, engine::general_purpose::STANDARD}; 38 39 /// Serializes a byte vector to a base64 encoded string. 40 pub fn serialize<S: Serializer>(value: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error> { ··· 43 } 44 45 /// Deserializes a base64 encoded string to a byte vector. 46 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> { 47 let encoded_value = String::deserialize(d)?; 48 STANDARD 49 - .decode(encoded_value) 50 .map_err(serde::de::Error::custom) 51 } 52 }
··· 21 //! 22 //! ## Base64 Encoding 23 //! 24 + //! The serialization uses standard base64 encoding (RFC 4648) with padding. 25 + //! The deserialization accepts both padded and unpadded base64 strings for 26 + //! compatibility with various AT Protocol implementations. 27 28 /// Base64 serialization format for byte arrays. 29 /// ··· 35 use serde::{Deserialize, Serialize}; 36 use serde::{Deserializer, Serializer}; 37 38 + use base64::{Engine, engine::general_purpose::{STANDARD, STANDARD_NO_PAD}}; 39 40 /// Serializes a byte vector to a base64 encoded string. 41 pub fn serialize<S: Serializer>(value: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error> { ··· 44 } 45 46 /// Deserializes a base64 encoded string to a byte vector. 47 + /// Handles both padded and unpadded base64 strings for compatibility. 48 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> { 49 let encoded_value = String::deserialize(d)?; 50 + 51 + // Try standard base64 with padding first 52 STANDARD 53 + .decode(&encoded_value) 54 + .or_else(|_| { 55 + // If that fails, try without padding requirement 56 + STANDARD_NO_PAD.decode(&encoded_value) 57 + }) 58 .map_err(serde::de::Error::custom) 59 } 60 }
+70
crates/atproto-record/src/lexicon/community_lexicon_attestation.rs
··· 145 use serde_json::json; 146 147 #[test] 148 fn test_signature_deserialization() { 149 let json_str = r#"{ 150 "$type": "community.lexicon.attestation.signature",
··· 145 use serde_json::json; 146 147 #[test] 148 + fn test_real_signature_or_ref_deserialization() { 149 + // Test with the exact signature structure from the user's RSVP 150 + let json_str = r#"{ 151 + "$type": "community.lexicon.attestation.signature", 152 + "issuedAt": "2025-08-19T20:17:17.133Z", 153 + "issuer": "did:web:acudo-dev.smokesignal.tools", 154 + "signature": { 155 + "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 156 + } 157 + }"#; 158 + 159 + // First, try to deserialize as TypedSignature 160 + let typed_sig_result: Result<TypedSignature, _> = serde_json::from_str(json_str); 161 + match &typed_sig_result { 162 + Ok(sig) => { 163 + println!("TypedSignature OK: issuer={}", sig.inner.issuer); 164 + assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools"); 165 + } 166 + Err(e) => { 167 + eprintln!("TypedSignature deserialization error: {}", e); 168 + } 169 + } 170 + 171 + // Then try as SignatureOrRef 172 + let sig_or_ref_result: Result<SignatureOrRef, _> = serde_json::from_str(json_str); 173 + match &sig_or_ref_result { 174 + Ok(SignatureOrRef::Inline(sig)) => { 175 + println!("SignatureOrRef OK (Inline): issuer={}", sig.inner.issuer); 176 + assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools"); 177 + } 178 + Ok(SignatureOrRef::Reference(_)) => { 179 + panic!("Expected Inline signature, got Reference"); 180 + } 181 + Err(e) => { 182 + eprintln!("SignatureOrRef deserialization error: {}", e); 183 + } 184 + } 185 + 186 + // Try without $type field 187 + let json_no_type = r#"{ 188 + "issuedAt": "2025-08-19T20:17:17.133Z", 189 + "issuer": "did:web:acudo-dev.smokesignal.tools", 190 + "signature": { 191 + "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 192 + } 193 + }"#; 194 + 195 + let no_type_result: Result<Signature, _> = serde_json::from_str(json_no_type); 196 + match &no_type_result { 197 + Ok(sig) => { 198 + println!("Signature (no type) OK: issuer={}", sig.issuer); 199 + assert_eq!(sig.issuer, "did:web:acudo-dev.smokesignal.tools"); 200 + assert_eq!(sig.signature.bytes.len(), 64); 201 + 202 + // Now wrap it in TypedLexicon and try as SignatureOrRef 203 + let typed = TypedLexicon::new(sig.clone()); 204 + let _as_sig_or_ref = SignatureOrRef::Inline(typed); 205 + println!("Successfully created SignatureOrRef from Signature"); 206 + } 207 + Err(e) => { 208 + eprintln!("Signature (no type) deserialization error: {}", e); 209 + } 210 + } 211 + 212 + // Check that at least one worked 213 + assert!(typed_sig_result.is_ok() || sig_or_ref_result.is_ok() || no_type_result.is_ok(), 214 + "Failed to deserialize signature in any form"); 215 + } 216 + 217 + #[test] 218 fn test_signature_deserialization() { 219 let json_str = r#"{ 220 "$type": "community.lexicon.attestation.signature",
+78
crates/atproto-record/src/lexicon/community_lexicon_calendar_rsvp.rs
··· 302 303 Ok(()) 304 } 305 }
··· 302 303 Ok(()) 304 } 305 + 306 + #[test] 307 + fn test_deserialize_real_rsvp_with_signature() -> Result<()> { 308 + use crate::lexicon::community_lexicon_attestation::SignatureOrRef; 309 + use chrono::Timelike; 310 + use serde_json::Value; 311 + 312 + // Real RSVP JSON with actual signature from the user 313 + let json_str = r#"{ 314 + "$type": "community.lexicon.calendar.rsvp", 315 + "createdAt": "2025-08-19T20:17:17.133Z", 316 + "signatures": [ 317 + { 318 + "$type": "community.lexicon.attestation.signature", 319 + "issuedAt": "2025-08-19T20:17:17.133Z", 320 + "issuer": "did:web:acudo-dev.smokesignal.tools", 321 + "signature": { 322 + "$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA" 323 + } 324 + } 325 + ], 326 + "status": "community.lexicon.calendar.rsvp#going", 327 + "subject": { 328 + "cid": "bafyreidtr72nriwlscae3gfwhckxiy427b6ni5jmetzkvaafimeuelnlxa", 329 + "uri": "at://did:web:acudo-dev.smokesignal.tools/community.lexicon.calendar.event/3lwrfkzy36k2s" 330 + } 331 + }"#; 332 + 333 + // First parse as generic JSON to verify structure 334 + let json_value: Value = serde_json::from_str(json_str)?; 335 + assert_eq!(json_value["$type"], "community.lexicon.calendar.rsvp"); 336 + assert_eq!(json_value["status"], "community.lexicon.calendar.rsvp#going"); 337 + 338 + // Deserialize the JSON 339 + let typed_rsvp: TypedRsvp = serde_json::from_str(json_str)?; 340 + 341 + // Verify the basic fields 342 + assert_eq!(typed_rsvp.inner.status, RsvpStatus::Going); 343 + assert_eq!( 344 + typed_rsvp.inner.subject.uri, 345 + "at://did:web:acudo-dev.smokesignal.tools/community.lexicon.calendar.event/3lwrfkzy36k2s" 346 + ); 347 + assert_eq!( 348 + typed_rsvp.inner.subject.cid, 349 + "bafyreidtr72nriwlscae3gfwhckxiy427b6ni5jmetzkvaafimeuelnlxa" 350 + ); 351 + 352 + // Verify the timestamp 353 + let expected_time = Utc.with_ymd_and_hms(2025, 8, 19, 20, 17, 17) 354 + .unwrap() 355 + .with_nanosecond(133_000_000) 356 + .unwrap(); 357 + assert_eq!(typed_rsvp.inner.created_at, expected_time); 358 + 359 + // Verify the signature 360 + assert_eq!(typed_rsvp.inner.signatures.len(), 1); 361 + match &typed_rsvp.inner.signatures[0] { 362 + SignatureOrRef::Inline(sig) => { 363 + assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools"); 364 + 365 + // Verify the issuedAt field if present 366 + if let Some(issued_at_value) = sig.inner.extra.get("issuedAt") { 367 + assert_eq!(issued_at_value, "2025-08-19T20:17:17.133Z"); 368 + } 369 + 370 + // Verify the signature is base64 encoded data 371 + // The signature should be 64 bytes for P-256 ECDSA (32 bytes for r, 32 bytes for s) 372 + assert_eq!(sig.inner.signature.bytes.len(), 64); 373 + } 374 + _ => panic!("Expected inline signature"), 375 + } 376 + 377 + // Verify the type field is present 378 + assert!(typed_rsvp.has_type_field()); 379 + assert!(typed_rsvp.validate().is_ok()); 380 + 381 + Ok(()) 382 + } 383 }
+423 -8
crates/atproto-record/src/signature.rs
··· 39 //! }); 40 //! 41 //! let signed = create(&key, &record, "did:plc:repo", 42 - //! "app.bsky.feed.post", sig_obj).await?; 43 //! 44 //! // Verify the signature 45 //! verify("did:plc:issuer", &key, signed, 46 - //! "did:plc:repo", "app.bsky.feed.post").await?; 47 //! ``` 48 49 use atproto_identity::key::{KeyData, sign, validate}; 50 - use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; 51 use serde_json::json; 52 53 use crate::errors::VerificationError; ··· 83 /// - IPLD DAG-CBOR serialization fails 84 /// - Cryptographic signing operation fails 85 /// - JSON structure manipulation fails 86 - pub async fn create( 87 key_data: &KeyData, 88 record: &serde_json::Value, 89 repository: &str, ··· 123 let serialized_signing_record = serde_ipld_dagcbor::to_vec(&signing_record)?; 124 125 let signature: Vec<u8> = sign(key_data, &serialized_signing_record)?; 126 - let encoded_signature = URL_SAFE_NO_PAD.encode(&signature); 127 128 // Compose the proof object 129 let mut proof = signature_object.clone(); ··· 187 /// - No `signatures` or `sigs` field exists in the record 188 /// - No signature from the specified issuer is found 189 /// - The issuer's signature is malformed or missing required fields 190 /// - Base64 decoding of the signature fails 191 /// - IPLD DAG-CBOR serialization of reconstructed content fails 192 /// - Cryptographic verification fails (invalid signature) ··· 195 /// 196 /// This function supports both `signatures` and `sigs` field names for 197 /// backward compatibility with different AT Protocol implementations. 198 - pub async fn verify( 199 issuer: &str, 200 key_data: &KeyData, 201 record: serde_json::Value, ··· 217 218 let signature_value = sig_obj 219 .get("signature") 220 - .and_then(|v| v.as_str()) 221 .ok_or(VerificationError::MissingSignatureField)?; 222 223 if issuer != signature_issuer { ··· 235 let mut signed_record = record.clone(); 236 if let Some(record_map) = signed_record.as_object_mut() { 237 record_map.remove("signatures"); 238 record_map.insert("$sig".to_string(), sig_variable); 239 } 240 241 let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record) 242 .map_err(|error| VerificationError::RecordSerializationFailed { error })?; 243 244 - let signature_bytes = URL_SAFE_NO_PAD 245 .decode(signature_value) 246 .map_err(|error| VerificationError::SignatureDecodingFailed { error })?; 247 ··· 255 issuer: issuer.to_string(), 256 }) 257 }
··· 39 //! }); 40 //! 41 //! let signed = create(&key, &record, "did:plc:repo", 42 + //! "app.bsky.feed.post", sig_obj)?; 43 //! 44 //! // Verify the signature 45 //! verify("did:plc:issuer", &key, signed, 46 + //! "did:plc:repo", "app.bsky.feed.post")?; 47 //! ``` 48 49 use atproto_identity::key::{KeyData, sign, validate}; 50 + use base64::{Engine, engine::general_purpose::STANDARD}; 51 use serde_json::json; 52 53 use crate::errors::VerificationError; ··· 83 /// - IPLD DAG-CBOR serialization fails 84 /// - Cryptographic signing operation fails 85 /// - JSON structure manipulation fails 86 + pub fn create( 87 key_data: &KeyData, 88 record: &serde_json::Value, 89 repository: &str, ··· 123 let serialized_signing_record = serde_ipld_dagcbor::to_vec(&signing_record)?; 124 125 let signature: Vec<u8> = sign(key_data, &serialized_signing_record)?; 126 + let encoded_signature = STANDARD.encode(&signature); 127 128 // Compose the proof object 129 let mut proof = signature_object.clone(); ··· 187 /// - No `signatures` or `sigs` field exists in the record 188 /// - No signature from the specified issuer is found 189 /// - The issuer's signature is malformed or missing required fields 190 + /// - The signature is not in the expected `{"$bytes": "..."}` format 191 /// - Base64 decoding of the signature fails 192 /// - IPLD DAG-CBOR serialization of reconstructed content fails 193 /// - Cryptographic verification fails (invalid signature) ··· 196 /// 197 /// This function supports both `signatures` and `sigs` field names for 198 /// backward compatibility with different AT Protocol implementations. 199 + pub fn verify( 200 issuer: &str, 201 key_data: &KeyData, 202 record: serde_json::Value, ··· 218 219 let signature_value = sig_obj 220 .get("signature") 221 + .and_then(|v| v.as_object()) 222 + .and_then(|obj| obj.get("$bytes")) 223 + .and_then(|b| b.as_str()) 224 .ok_or(VerificationError::MissingSignatureField)?; 225 226 if issuer != signature_issuer { ··· 238 let mut signed_record = record.clone(); 239 if let Some(record_map) = signed_record.as_object_mut() { 240 record_map.remove("signatures"); 241 + record_map.remove("sigs"); 242 record_map.insert("$sig".to_string(), sig_variable); 243 } 244 245 let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record) 246 .map_err(|error| VerificationError::RecordSerializationFailed { error })?; 247 248 + let signature_bytes = STANDARD 249 .decode(signature_value) 250 .map_err(|error| VerificationError::SignatureDecodingFailed { error })?; 251 ··· 259 issuer: issuer.to_string(), 260 }) 261 } 262 + 263 + #[cfg(test)] 264 + mod tests { 265 + use super::*; 266 + use atproto_identity::key::{KeyType, generate_key, to_public}; 267 + use serde_json::json; 268 + 269 + #[test] 270 + fn test_create_sign_and_verify_record_p256() -> Result<(), Box<dyn std::error::Error>> { 271 + // Step 1: Generate a P-256 key pair 272 + let private_key = generate_key(KeyType::P256Private)?; 273 + let public_key = to_public(&private_key)?; 274 + 275 + // Step 2: Create a sample record 276 + let record = json!({ 277 + "text": "Hello AT Protocol!", 278 + "createdAt": "2025-01-19T10:00:00Z", 279 + "langs": ["en"] 280 + }); 281 + 282 + // Step 3: Define signature metadata 283 + let issuer_did = "did:plc:test123"; 284 + let repository = "did:plc:repo456"; 285 + let collection = "app.bsky.feed.post"; 286 + 287 + let signature_object = json!({ 288 + "issuer": issuer_did, 289 + "issuedAt": "2025-01-19T10:00:00Z", 290 + "purpose": "attestation" 291 + }); 292 + 293 + // Step 4: Sign the record 294 + let signed_record = create( 295 + &private_key, 296 + &record, 297 + repository, 298 + collection, 299 + signature_object.clone(), 300 + )?; 301 + 302 + // Verify that the signed record contains signatures array 303 + assert!(signed_record.get("signatures").is_some()); 304 + let signatures = signed_record 305 + .get("signatures") 306 + .and_then(|v| v.as_array()) 307 + .expect("signatures should be an array"); 308 + assert_eq!(signatures.len(), 1); 309 + 310 + // Verify signature object structure 311 + let sig = &signatures[0]; 312 + assert_eq!(sig.get("issuer").and_then(|v| v.as_str()), Some(issuer_did)); 313 + assert!(sig.get("signature").is_some()); 314 + assert_eq!( 315 + sig.get("$type").and_then(|v| v.as_str()), 316 + Some("community.lexicon.attestation.signature") 317 + ); 318 + 319 + // Step 5: Verify the signature 320 + verify( 321 + issuer_did, 322 + &public_key, 323 + signed_record.clone(), 324 + repository, 325 + collection, 326 + )?; 327 + 328 + Ok(()) 329 + } 330 + 331 + #[test] 332 + fn test_create_sign_and_verify_record_k256() -> Result<(), Box<dyn std::error::Error>> { 333 + // Test with K-256 curve 334 + let private_key = generate_key(KeyType::K256Private)?; 335 + let public_key = to_public(&private_key)?; 336 + 337 + let record = json!({ 338 + "subject": "at://did:plc:example/app.bsky.feed.post/123", 339 + "likedAt": "2025-01-19T10:00:00Z" 340 + }); 341 + 342 + let issuer_did = "did:plc:issuer789"; 343 + let repository = "did:plc:repo789"; 344 + let collection = "app.bsky.feed.like"; 345 + 346 + let signature_object = json!({ 347 + "issuer": issuer_did, 348 + "issuedAt": "2025-01-19T10:00:00Z" 349 + }); 350 + 351 + let signed_record = create( 352 + &private_key, 353 + &record, 354 + repository, 355 + collection, 356 + signature_object, 357 + )?; 358 + 359 + verify( 360 + issuer_did, 361 + &public_key, 362 + signed_record, 363 + repository, 364 + collection, 365 + )?; 366 + 367 + Ok(()) 368 + } 369 + 370 + #[test] 371 + fn test_create_sign_and_verify_record_p384() -> Result<(), Box<dyn std::error::Error>> { 372 + // Test with P-384 curve 373 + let private_key = generate_key(KeyType::P384Private)?; 374 + let public_key = to_public(&private_key)?; 375 + 376 + let record = json!({ 377 + "displayName": "Test User", 378 + "description": "Testing P-384 signatures" 379 + }); 380 + 381 + let issuer_did = "did:web:example.com"; 382 + let repository = "did:plc:profile123"; 383 + let collection = "app.bsky.actor.profile"; 384 + 385 + let signature_object = json!({ 386 + "issuer": issuer_did, 387 + "issuedAt": "2025-01-19T10:00:00Z", 388 + "expiresAt": "2025-01-20T10:00:00Z", 389 + "customField": "custom value" 390 + }); 391 + 392 + let signed_record = create( 393 + &private_key, 394 + &record, 395 + repository, 396 + collection, 397 + signature_object.clone(), 398 + )?; 399 + 400 + // Verify custom fields are preserved in signature 401 + let signatures = signed_record 402 + .get("signatures") 403 + .and_then(|v| v.as_array()) 404 + .expect("signatures should exist"); 405 + let sig = &signatures[0]; 406 + assert_eq!( 407 + sig.get("customField").and_then(|v| v.as_str()), 408 + Some("custom value") 409 + ); 410 + 411 + verify( 412 + issuer_did, 413 + &public_key, 414 + signed_record, 415 + repository, 416 + collection, 417 + )?; 418 + 419 + Ok(()) 420 + } 421 + 422 + #[test] 423 + fn test_multiple_signatures() -> Result<(), Box<dyn std::error::Error>> { 424 + // Create a record with multiple signatures from different issuers 425 + let private_key1 = generate_key(KeyType::P256Private)?; 426 + let public_key1 = to_public(&private_key1)?; 427 + 428 + let private_key2 = generate_key(KeyType::K256Private)?; 429 + let public_key2 = to_public(&private_key2)?; 430 + 431 + let record = json!({ 432 + "text": "Multi-signed content", 433 + "important": true 434 + }); 435 + 436 + let repository = "did:plc:repo_multi"; 437 + let collection = "app.example.document"; 438 + 439 + // First signature 440 + let issuer1 = "did:plc:issuer1"; 441 + let sig_obj1 = json!({ 442 + "issuer": issuer1, 443 + "issuedAt": "2025-01-19T09:00:00Z", 444 + "role": "author" 445 + }); 446 + 447 + let signed_once = create(&private_key1, &record, repository, collection, sig_obj1)?; 448 + 449 + // Second signature on already signed record 450 + let issuer2 = "did:plc:issuer2"; 451 + let sig_obj2 = json!({ 452 + "issuer": issuer2, 453 + "issuedAt": "2025-01-19T10:00:00Z", 454 + "role": "reviewer" 455 + }); 456 + 457 + let signed_twice = create( 458 + &private_key2, 459 + &signed_once, 460 + repository, 461 + collection, 462 + sig_obj2, 463 + )?; 464 + 465 + // Verify we have two signatures 466 + let signatures = signed_twice 467 + .get("signatures") 468 + .and_then(|v| v.as_array()) 469 + .expect("signatures should exist"); 470 + assert_eq!(signatures.len(), 2); 471 + 472 + // Verify both signatures independently 473 + verify( 474 + issuer1, 475 + &public_key1, 476 + signed_twice.clone(), 477 + repository, 478 + collection, 479 + )?; 480 + verify( 481 + issuer2, 482 + &public_key2, 483 + signed_twice.clone(), 484 + repository, 485 + collection, 486 + )?; 487 + 488 + Ok(()) 489 + } 490 + 491 + #[test] 492 + fn test_verify_wrong_issuer_fails() -> Result<(), Box<dyn std::error::Error>> { 493 + let private_key = generate_key(KeyType::P256Private)?; 494 + let public_key = to_public(&private_key)?; 495 + 496 + let record = json!({"test": "data"}); 497 + let repository = "did:plc:repo"; 498 + let collection = "app.test"; 499 + 500 + let sig_obj = json!({ 501 + "issuer": "did:plc:correct_issuer" 502 + }); 503 + 504 + let signed = create(&private_key, &record, repository, collection, sig_obj)?; 505 + 506 + // Try to verify with wrong issuer 507 + let result = verify( 508 + "did:plc:wrong_issuer", 509 + &public_key, 510 + signed, 511 + repository, 512 + collection, 513 + ); 514 + 515 + assert!(result.is_err()); 516 + assert!(matches!( 517 + result.unwrap_err(), 518 + VerificationError::NoValidSignatureForIssuer { .. } 519 + )); 520 + 521 + Ok(()) 522 + } 523 + 524 + #[test] 525 + fn test_verify_wrong_key_fails() -> Result<(), Box<dyn std::error::Error>> { 526 + let private_key = generate_key(KeyType::P256Private)?; 527 + let wrong_private_key = generate_key(KeyType::P256Private)?; 528 + let wrong_public_key = to_public(&wrong_private_key)?; 529 + 530 + let record = json!({"test": "data"}); 531 + let repository = "did:plc:repo"; 532 + let collection = "app.test"; 533 + let issuer = "did:plc:issuer"; 534 + 535 + let sig_obj = json!({ "issuer": issuer }); 536 + 537 + let signed = create(&private_key, &record, repository, collection, sig_obj)?; 538 + 539 + // Try to verify with wrong key 540 + let result = verify(issuer, &wrong_public_key, signed, repository, collection); 541 + 542 + assert!(result.is_err()); 543 + assert!(matches!( 544 + result.unwrap_err(), 545 + VerificationError::CryptographicValidationFailed { .. } 546 + )); 547 + 548 + Ok(()) 549 + } 550 + 551 + #[test] 552 + fn test_verify_tampered_record_fails() -> Result<(), Box<dyn std::error::Error>> { 553 + let private_key = generate_key(KeyType::P256Private)?; 554 + let public_key = to_public(&private_key)?; 555 + 556 + let record = json!({"text": "original"}); 557 + let repository = "did:plc:repo"; 558 + let collection = "app.test"; 559 + let issuer = "did:plc:issuer"; 560 + 561 + let sig_obj = json!({ "issuer": issuer }); 562 + 563 + let mut signed = create(&private_key, &record, repository, collection, sig_obj)?; 564 + 565 + // Tamper with the record content 566 + if let Some(obj) = signed.as_object_mut() { 567 + obj.insert("text".to_string(), json!("tampered")); 568 + } 569 + 570 + // Verification should fail 571 + let result = verify(issuer, &public_key, signed, repository, collection); 572 + 573 + assert!(result.is_err()); 574 + assert!(matches!( 575 + result.unwrap_err(), 576 + VerificationError::CryptographicValidationFailed { .. } 577 + )); 578 + 579 + Ok(()) 580 + } 581 + 582 + #[test] 583 + fn test_create_missing_issuer_fails() -> Result<(), Box<dyn std::error::Error>> { 584 + let private_key = generate_key(KeyType::P256Private)?; 585 + 586 + let record = json!({"test": "data"}); 587 + let repository = "did:plc:repo"; 588 + let collection = "app.test"; 589 + 590 + // Signature object without issuer field 591 + let sig_obj = json!({ 592 + "issuedAt": "2025-01-19T10:00:00Z" 593 + }); 594 + 595 + let result = create(&private_key, &record, repository, collection, sig_obj); 596 + 597 + assert!(result.is_err()); 598 + assert!(matches!( 599 + result.unwrap_err(), 600 + VerificationError::SignatureObjectMissingField { field } if field == "issuer" 601 + )); 602 + 603 + Ok(()) 604 + } 605 + 606 + #[test] 607 + fn test_verify_supports_sigs_field() -> Result<(), Box<dyn std::error::Error>> { 608 + // Test backward compatibility with "sigs" field name 609 + let private_key = generate_key(KeyType::P256Private)?; 610 + let public_key = to_public(&private_key)?; 611 + 612 + let record = json!({"test": "data"}); 613 + let repository = "did:plc:repo"; 614 + let collection = "app.test"; 615 + let issuer = "did:plc:issuer"; 616 + 617 + let sig_obj = json!({ "issuer": issuer }); 618 + 619 + let mut signed = create(&private_key, &record, repository, collection, sig_obj)?; 620 + 621 + // Rename "signatures" to "sigs" 622 + if let Some(obj) = signed.as_object_mut() { 623 + if let Some(signatures) = obj.remove("signatures") { 624 + obj.insert("sigs".to_string(), signatures); 625 + } 626 + } 627 + 628 + // Should still verify successfully 629 + verify(issuer, &public_key, signed, repository, collection)?; 630 + 631 + Ok(()) 632 + } 633 + 634 + #[test] 635 + fn test_signature_preserves_original_record() -> Result<(), Box<dyn std::error::Error>> { 636 + let private_key = generate_key(KeyType::P256Private)?; 637 + 638 + let original_record = json!({ 639 + "text": "Original content", 640 + "metadata": { 641 + "author": "Test", 642 + "version": 1 643 + }, 644 + "tags": ["test", "sample"] 645 + }); 646 + 647 + let repository = "did:plc:repo"; 648 + let collection = "app.test"; 649 + 650 + let sig_obj = json!({ 651 + "issuer": "did:plc:issuer" 652 + }); 653 + 654 + let signed = create( 655 + &private_key, 656 + &original_record, 657 + repository, 658 + collection, 659 + sig_obj, 660 + )?; 661 + 662 + // All original fields should be preserved 663 + assert_eq!(signed.get("text"), original_record.get("text")); 664 + assert_eq!(signed.get("metadata"), original_record.get("metadata")); 665 + assert_eq!(signed.get("tags"), original_record.get("tags")); 666 + 667 + // Plus the new signatures field 668 + assert!(signed.get("signatures").is_some()); 669 + 670 + Ok(()) 671 + } 672 + }