A library for ATProtocol identities.
1//! Core attestation creation functions. 2//! 3//! This module provides functions for creating inline and remote attestations 4//! and attaching attestation references. 5 6use crate::cid::{create_attestation_cid, create_dagbor_cid}; 7use crate::errors::AttestationError; 8pub use crate::input::AnyInput; 9use crate::signature::normalize_signature; 10use crate::utils::BASE64; 11use atproto_identity::key::{KeyData, KeyResolver, sign, validate}; 12use atproto_record::lexicon::com::atproto::repo::STRONG_REF_NSID; 13use atproto_record::tid::Tid; 14use base64::Engine; 15use serde::Serialize; 16use serde_json::{Value, json, Map}; 17use std::convert::TryInto; 18 19/// Helper function to extract and validate signatures array from a record 20fn extract_signatures(record_obj: &Map<String, Value>) -> Result<Vec<Value>, AttestationError> { 21 match record_obj.get("signatures") { 22 Some(value) => value 23 .as_array() 24 .ok_or(AttestationError::SignaturesFieldInvalid) 25 .cloned(), 26 None => Ok(vec![]), 27 } 28} 29 30/// Helper function to append a signature to a record and return the modified record 31fn append_signature_to_record( 32 mut record_obj: Map<String, Value>, 33 signature: Value, 34) -> Result<Value, AttestationError> { 35 let mut signatures = extract_signatures(&record_obj)?; 36 signatures.push(signature); 37 38 record_obj.insert( 39 "signatures".to_string(), 40 Value::Array(signatures), 41 ); 42 43 Ok(Value::Object(record_obj)) 44} 45 46/// Creates a cryptographic signature for a record with attestation metadata. 47/// 48/// This is a low-level function that generates just the signature bytes without 49/// embedding them in a record structure. It's useful when you need to create 50/// signatures independently or for custom attestation workflows. 51/// 52/// The signature is created over a content CID that binds together: 53/// - The record content 54/// - The attestation metadata 55/// - The repository DID (to prevent replay attacks) 56/// 57/// # Arguments 58/// 59/// * `record_input` - The record to sign (as AnyInput: String, Json, or TypedLexicon) 60/// * `attestation_input` - The attestation metadata (as AnyInput) 61/// * `repository` - The repository DID where this record will be stored 62/// * `private_key_data` - The private key to use for signing 63/// 64/// # Returns 65/// 66/// A byte vector containing the normalized ECDSA signature that can be verified 67/// against the same content CID. 68/// 69/// # Errors 70/// 71/// Returns an error if: 72/// - CID generation fails 73/// - Signature creation fails 74/// - Signature normalization fails 75/// 76/// # Example 77/// 78/// ```rust 79/// use atproto_attestation::{create_signature, input::AnyInput}; 80/// use atproto_identity::key::{KeyType, generate_key}; 81/// use serde_json::json; 82/// 83/// # fn example() -> Result<(), Box<dyn std::error::Error>> { 84/// let private_key = generate_key(KeyType::K256Private)?; 85/// 86/// let record = json!({"$type": "app.bsky.feed.post", "text": "Hello!"}); 87/// let metadata = json!({"$type": "com.example.signature"}); 88/// 89/// let signature_bytes = create_signature( 90/// AnyInput::Serialize(record), 91/// AnyInput::Serialize(metadata), 92/// "did:plc:repo123", 93/// &private_key 94/// )?; 95/// 96/// // signature_bytes can now be base64-encoded or used as needed 97/// # Ok(()) 98/// # } 99/// ``` 100pub fn create_signature<R, M>( 101 record_input: AnyInput<R>, 102 attestation_input: AnyInput<M>, 103 repository: &str, 104 private_key_data: &KeyData, 105) -> Result<Vec<u8>, AttestationError> 106where 107 R: Serialize + Clone, 108 M: Serialize + Clone, 109{ 110 // Step 1: Create a content CID from record + attestation + repository 111 let content_cid = create_attestation_cid(record_input, attestation_input, repository)?; 112 113 // Step 2: Sign the CID bytes 114 let raw_signature = sign(private_key_data, &content_cid.to_bytes()) 115 .map_err(|error| AttestationError::SignatureCreationFailed { error })?; 116 117 // Step 3: Normalize the signature to ensure consistent format 118 normalize_signature(raw_signature, private_key_data.key_type()) 119} 120 121/// Creates a remote attestation with both the attested record and proof record. 122/// 123/// This is the recommended way to create remote attestations. It generates both: 124/// 1. The attested record with a strongRef in the signatures array 125/// 2. The proof record containing the CID to be stored in the attestation repository 126/// 127/// The CID is generated with the repository DID included in the `$sig` metadata 128/// to bind the attestation to a specific repository and prevent replay attacks. 129/// 130/// # Arguments 131/// 132/// * `record_input` - The record to attest (as AnyInput: String, Json, or TypedLexicon) 133/// * `metadata_input` - The attestation metadata (must include `$type`) 134/// * `repository` - The DID of the repository housing the original record 135/// * `attestation_repository` - The DID of the repository that will store the proof record 136/// 137/// # Returns 138/// 139/// A tuple containing: 140/// * `(attested_record, proof_record)` - Both records needed for remote attestation 141/// 142/// # Errors 143/// 144/// Returns an error if: 145/// - The record or metadata are not valid JSON objects 146/// - The metadata is missing the required `$type` field 147/// - CID generation fails 148/// 149/// # Example 150/// 151/// ```rust 152/// use atproto_attestation::{create_remote_attestation, input::AnyInput}; 153/// use serde_json::json; 154/// 155/// # fn example() -> Result<(), Box<dyn std::error::Error>> { 156/// let record = json!({"$type": "app.bsky.feed.post", "text": "Hello!"}); 157/// let metadata = json!({"$type": "com.example.attestation"}); 158/// 159/// let (attested_record, proof_record) = create_remote_attestation( 160/// AnyInput::Serialize(record), 161/// AnyInput::Serialize(metadata), 162/// "did:plc:repo123", // Source repository 163/// "did:plc:attestor456" // Attestation repository 164/// )?; 165/// # Ok(()) 166/// # } 167/// ``` 168pub fn create_remote_attestation< 169 R: Serialize + Clone, 170 M: Serialize + Clone, 171>( 172 record_input: AnyInput<R>, 173 metadata_input: AnyInput<M>, 174 repository: &str, 175 attestation_repository: &str, 176) -> Result<(Value, Value), AttestationError> { 177 // Step 1: Create a content CID 178 let content_cid = 179 create_attestation_cid(record_input.clone(), metadata_input.clone(), repository)?; 180 181 let record_obj: Map<String, Value> = record_input 182 .try_into() 183 .map_err(|_| AttestationError::RecordMustBeObject)?; 184 185 // Step 2: Create the remote attestation record 186 let (remote_attestation_record, remote_attestation_type) = { 187 let mut metadata_obj: Map<String, Value> = metadata_input 188 .try_into() 189 .map_err(|_| AttestationError::MetadataMustBeObject)?; 190 191 // Extract the type from metadata before modifying it 192 let remote_type = metadata_obj 193 .get("$type") 194 .and_then(Value::as_str) 195 .ok_or(AttestationError::MetadataMissingType)? 196 .to_string(); 197 198 metadata_obj.insert("cid".to_string(), Value::String(content_cid.to_string())); 199 (serde_json::Value::Object(metadata_obj), remote_type) 200 }; 201 202 // Step 3: Create the remote attestation reference (type, AT-URI, and CID) 203 let remote_attestation_record_key = Tid::new(); 204 let remote_attestation_cid = create_dagbor_cid(&remote_attestation_record)?; 205 206 let attestation_reference = json!({ 207 "$type": STRONG_REF_NSID, 208 "uri": format!("at://{attestation_repository}/{remote_attestation_type}/{remote_attestation_record_key}"), 209 "cid": remote_attestation_cid.to_string() 210 }); 211 212 // Step 4: Append the attestation reference to the record "signatures" array 213 let attested_record = append_signature_to_record(record_obj, attestation_reference)?; 214 215 Ok((attested_record, remote_attestation_record)) 216} 217 218/// Creates an inline attestation with signature embedded in the record. 219/// 220/// This is the v2 API that supports flexible input types (String, Json, TypedLexicon) 221/// and provides a more streamlined interface for creating inline attestations. 222/// 223/// The CID is generated with the repository DID included in the `$sig` metadata 224/// to bind the attestation to a specific repository and prevent replay attacks. 225/// 226/// # Arguments 227/// 228/// * `record_input` - The record to sign (as AnyInput: String, Json, or TypedLexicon) 229/// * `metadata_input` - The attestation metadata (must include `$type` and `key`) 230/// * `repository` - The DID of the repository that will house this record 231/// * `private_key_data` - The private key to use for signing 232/// 233/// # Returns 234/// 235/// The record with an inline attestation embedded in the signatures array 236/// 237/// # Errors 238/// 239/// Returns an error if: 240/// - The record or metadata are not valid JSON objects 241/// - The metadata is missing required fields 242/// - Signature creation fails 243/// - CID generation fails 244/// 245/// # Example 246/// 247/// ```rust 248/// use atproto_attestation::{create_inline_attestation, input::AnyInput}; 249/// use atproto_identity::key::{KeyType, generate_key, to_public}; 250/// use serde_json::json; 251/// 252/// # fn example() -> Result<(), Box<dyn std::error::Error>> { 253/// let private_key = generate_key(KeyType::K256Private)?; 254/// let public_key = to_public(&private_key)?; 255/// 256/// let record = json!({"$type": "app.bsky.feed.post", "text": "Hello!"}); 257/// let metadata = json!({ 258/// "$type": "com.example.signature", 259/// "key": format!("{}", public_key) 260/// }); 261/// 262/// let signed_record = create_inline_attestation( 263/// AnyInput::Serialize(record), 264/// AnyInput::Serialize(metadata), 265/// "did:plc:repo123", 266/// &private_key 267/// )?; 268/// # Ok(()) 269/// # } 270/// ``` 271pub fn create_inline_attestation< 272 R: Serialize + Clone, 273 M: Serialize + Clone, 274>( 275 record_input: AnyInput<R>, 276 metadata_input: AnyInput<M>, 277 repository: &str, 278 private_key_data: &KeyData, 279) -> Result<Value, AttestationError> { 280 // Step 1: Create a content CID 281 let content_cid = 282 create_attestation_cid(record_input.clone(), metadata_input.clone(), repository)?; 283 284 let record_obj: Map<String, Value> = record_input 285 .try_into() 286 .map_err(|_| AttestationError::RecordMustBeObject)?; 287 288 // Step 2: Create the inline attestation record 289 let inline_attestation_record = { 290 let mut metadata_obj: Map<String, Value> = metadata_input 291 .try_into() 292 .map_err(|_| AttestationError::MetadataMustBeObject)?; 293 294 metadata_obj.insert("cid".to_string(), Value::String(content_cid.to_string())); 295 296 let raw_signature = sign(private_key_data, &content_cid.to_bytes()) 297 .map_err(|error| AttestationError::SignatureCreationFailed { error })?; 298 let signature_bytes = normalize_signature(raw_signature, private_key_data.key_type())?; 299 300 metadata_obj.insert( 301 "signature".to_string(), 302 json!({"$bytes": BASE64.encode(signature_bytes)}), 303 ); 304 305 serde_json::Value::Object(metadata_obj) 306 }; 307 308 // Step 4: Append the attestation reference to the record "signatures" array 309 append_signature_to_record(record_obj, inline_attestation_record) 310} 311 312/// Validates an existing proof record and appends a strongRef to it in the record's signatures array. 313/// 314/// This function validates that an existing proof record (attestation metadata with CID) 315/// is valid for the given record and repository, then creates and appends a strongRef to it. 316/// 317/// Unlike `create_remote_attestation` which creates a new proof record, this function validates 318/// an existing proof record that was already created and stored in an attestor's repository. 319/// 320/// # Security 321/// 322/// - **Repository binding validation**: Ensures the attestation was created for the specified repository DID 323/// - **CID verification**: Validates the proof record's CID matches the computed CID 324/// - **Content validation**: Ensures the proof record content matches what should be attested 325/// 326/// # Workflow 327/// 328/// 1. Compute the content CID from record + metadata + repository (same as attestation creation) 329/// 2. Extract the claimed CID from the proof record metadata 330/// 3. Verify the claimed CID matches the computed CID 331/// 4. Extract the proof record's storage CID (DAG-CBOR CID of the full proof record) 332/// 5. Create a strongRef with the AT-URI and proof record CID 333/// 6. Append the strongRef to the record's signatures array 334/// 335/// # Arguments 336/// 337/// * `record_input` - The record to append the attestation to (as AnyInput) 338/// * `metadata_input` - The proof record metadata (must include `$type`, `cid`, and attestation fields) 339/// * `repository` - The repository DID where the source record is stored (for replay attack prevention) 340/// * `attestation_uri` - The AT-URI where the proof record is stored (e.g., "at://did:plc:attestor/com.example.attestation/abc123") 341/// 342/// # Returns 343/// 344/// The modified record with the strongRef appended to its `signatures` array 345/// 346/// # Errors 347/// 348/// Returns an error if: 349/// - The record or metadata are not valid JSON objects 350/// - The metadata is missing the `cid` field 351/// - The computed CID doesn't match the claimed CID in the metadata 352/// - The metadata is missing required attestation fields 353/// 354/// # Type Parameters 355/// 356/// * `R` - The record type (must implement Serialize + LexiconType + PartialEq + Clone) 357/// * `A` - The attestation type (must implement Serialize + LexiconType + PartialEq + Clone) 358/// 359/// # Example 360/// 361/// ```ignore 362/// use atproto_attestation::{append_remote_attestation, input::AnyInput}; 363/// use serde_json::json; 364/// 365/// let record = json!({ 366/// "$type": "app.bsky.feed.post", 367/// "text": "Hello world!" 368/// }); 369/// 370/// // This is the proof record that was previously created and stored 371/// let proof_metadata = json!({ 372/// "$type": "com.example.attestation", 373/// "issuer": "did:plc:issuer", 374/// "cid": "bafyrei...", // Content CID computed from record+metadata+repository 375/// // ... other attestation fields 376/// }); 377/// 378/// let repository_did = "did:plc:repo123"; 379/// let attestation_uri = "at://did:plc:attestor456/com.example.attestation/abc123"; 380/// 381/// let signed_record = append_remote_attestation( 382/// AnyInput::Serialize(record), 383/// AnyInput::Serialize(proof_metadata), 384/// repository_did, 385/// attestation_uri 386/// )?; 387/// ``` 388pub fn append_remote_attestation<R, A>( 389 record_input: AnyInput<R>, 390 metadata_input: AnyInput<A>, 391 repository: &str, 392 attestation_uri: &str, 393) -> Result<Value, AttestationError> 394where 395 R: Serialize + Clone, 396 A: Serialize + Clone, 397{ 398 // Step 1: Compute the content CID (same as create_remote_attestation) 399 let content_cid = 400 create_attestation_cid(record_input.clone(), metadata_input.clone(), repository)?; 401 402 // Step 2: Convert metadata to JSON and extract the claimed CID 403 let metadata_obj: Map<String, Value> = metadata_input 404 .try_into() 405 .map_err(|_| AttestationError::MetadataMustBeObject)?; 406 407 let claimed_cid = metadata_obj 408 .get("cid") 409 .and_then(Value::as_str) 410 .filter(|value| !value.is_empty()) 411 .ok_or(AttestationError::SignatureMissingField { 412 field: "cid".to_string(), 413 })?; 414 415 // Step 3: Verify the claimed CID matches the computed content CID 416 if content_cid.to_string() != claimed_cid { 417 return Err(AttestationError::RemoteAttestationCidMismatch { 418 expected: claimed_cid.to_string(), 419 actual: content_cid.to_string(), 420 }); 421 } 422 423 // Step 4: Compute the proof record's DAG-CBOR CID 424 let proof_record_cid = create_dagbor_cid(&metadata_obj)?; 425 426 // Step 5: Create the strongRef 427 let strongref = json!({ 428 "$type": STRONG_REF_NSID, 429 "uri": attestation_uri, 430 "cid": proof_record_cid.to_string() 431 }); 432 433 // Step 6: Convert record to JSON object and append the strongRef 434 let record_obj: Map<String, Value> = record_input 435 .try_into() 436 .map_err(|_| AttestationError::RecordMustBeObject)?; 437 438 append_signature_to_record(record_obj, strongref) 439} 440 441/// Validates an inline attestation and appends it to a record's signatures array. 442/// 443/// Inline attestations contain cryptographic signatures embedded directly in the record. 444/// This function validates the attestation signature against the record and repository, 445/// then appends it if validation succeeds. 446/// 447/// # Security 448/// 449/// - **Repository binding validation**: Ensures the attestation was created for the specified repository DID 450/// - **CID verification**: Validates the CID in the attestation matches the computed CID 451/// - **Signature verification**: Cryptographically verifies the ECDSA signature 452/// - **Key resolution**: Resolves and validates the verification key 453/// 454/// # Arguments 455/// 456/// * `record_input` - The record to append the attestation to (as AnyInput) 457/// * `attestation_input` - The inline attestation to validate and append (as AnyInput) 458/// * `repository` - The repository DID where this record is stored (for replay attack prevention) 459/// * `key_resolver` - Resolver for looking up verification keys from DIDs 460/// 461/// # Returns 462/// 463/// The modified record with the validated attestation appended to its `signatures` array 464/// 465/// # Errors 466/// 467/// Returns an error if: 468/// - The record or attestation are not valid JSON objects 469/// - The attestation is missing required fields (`$type`, `key`, `cid`, `signature`) 470/// - The attestation CID doesn't match the computed CID for the record 471/// - The signature bytes are invalid or not base64-encoded 472/// - Signature verification fails 473/// - Key resolution fails 474/// 475/// # Type Parameters 476/// 477/// * `R` - The record type (must implement Serialize + LexiconType + PartialEq + Clone) 478/// * `A` - The attestation type (must implement Serialize + LexiconType + PartialEq + Clone) 479/// * `KR` - The key resolver type (must implement KeyResolver) 480/// 481/// # Example 482/// 483/// ```ignore 484/// use atproto_attestation::{append_inline_attestation, input::AnyInput}; 485/// use serde_json::json; 486/// 487/// let record = json!({ 488/// "$type": "app.bsky.feed.post", 489/// "text": "Hello world!" 490/// }); 491/// 492/// let attestation = json!({ 493/// "$type": "com.example.inlineSignature", 494/// "key": "did:key:zQ3sh...", 495/// "cid": "bafyrei...", 496/// "signature": {"$bytes": "base64-signature-bytes"} 497/// }); 498/// 499/// let repository_did = "did:plc:repo123"; 500/// let key_resolver = /* your KeyResolver implementation */; 501/// 502/// let signed_record = append_inline_attestation( 503/// AnyInput::Serialize(record), 504/// AnyInput::Serialize(attestation), 505/// repository_did, 506/// key_resolver 507/// ).await?; 508/// ``` 509pub async fn append_inline_attestation<R, A, KR>( 510 record_input: AnyInput<R>, 511 attestation_input: AnyInput<A>, 512 repository: &str, 513 key_resolver: KR, 514) -> Result<Value, AttestationError> 515where 516 R: Serialize + Clone, 517 A: Serialize + Clone, 518 KR: KeyResolver, 519{ 520 // Step 1: Create a content CID 521 let content_cid = 522 create_attestation_cid(record_input.clone(), attestation_input.clone(), repository)?; 523 524 let record_obj: Map<String, Value> = record_input 525 .try_into() 526 .map_err(|_| AttestationError::RecordMustBeObject)?; 527 528 let attestation_obj: Map<String, Value> = attestation_input 529 .try_into() 530 .map_err(|_| AttestationError::MetadataMustBeObject)?; 531 532 let key = attestation_obj 533 .get("key") 534 .and_then(Value::as_str) 535 .filter(|value| !value.is_empty()) 536 .ok_or(AttestationError::SignatureMissingField { 537 field: "key".to_string(), 538 })?; 539 let key_data = 540 key_resolver 541 .resolve(key) 542 .await 543 .map_err(|error| AttestationError::KeyResolutionFailed { 544 key: key.to_string(), 545 error, 546 })?; 547 548 let signature_bytes = attestation_obj 549 .get("signature") 550 .and_then(Value::as_object) 551 .and_then(|object| object.get("$bytes")) 552 .and_then(Value::as_str) 553 .ok_or(AttestationError::SignatureBytesFormatInvalid)?; 554 555 let signature_bytes = BASE64 556 .decode(signature_bytes) 557 .map_err(|error| AttestationError::SignatureDecodingFailed { error })?; 558 559 let computed_cid_bytes = content_cid.to_bytes(); 560 561 validate(&key_data, &signature_bytes, &computed_cid_bytes) 562 .map_err(|error| AttestationError::SignatureValidationFailed { error })?; 563 564 // Step 6: Append the validated attestation to the signatures array 565 append_signature_to_record(record_obj, json!(attestation_obj)) 566} 567 568#[cfg(test)] 569mod tests { 570 use super::*; 571 use atproto_identity::key::{KeyType, generate_key, to_public}; 572 use serde_json::json; 573 574 #[test] 575 fn create_remote_attestation_produces_both_records() -> Result<(), Box<dyn std::error::Error>> { 576 577 let record = json!({ 578 "$type": "app.example.record", 579 "body": "remote attestation" 580 }); 581 582 let metadata = json!({ 583 "$type": "com.example.attestation" 584 }); 585 586 let source_repository = "did:plc:test"; 587 let attestation_repository = "did:plc:attestor"; 588 589 let (attested_record, proof_record) = 590 create_remote_attestation( 591 AnyInput::Serialize(record.clone()), 592 AnyInput::Serialize(metadata), 593 source_repository, 594 attestation_repository, 595 )?; 596 597 // Verify proof record structure 598 let proof_object = proof_record.as_object().expect("proof should be an object"); 599 assert_eq!( 600 proof_object.get("$type").and_then(Value::as_str), 601 Some("com.example.attestation") 602 ); 603 assert!( 604 proof_object.get("cid").and_then(Value::as_str).is_some(), 605 "proof must contain a cid" 606 ); 607 assert!( 608 proof_object.get("repository").is_none(), 609 "repository should not be in final proof record" 610 ); 611 612 // Verify attested record has strongRef 613 let attested_object = attested_record 614 .as_object() 615 .expect("attested record should be an object"); 616 let signatures = attested_object 617 .get("signatures") 618 .and_then(Value::as_array) 619 .expect("attested record should have signatures array"); 620 assert_eq!(signatures.len(), 1, "should have one signature"); 621 622 let signature = &signatures[0]; 623 assert_eq!( 624 signature.get("$type").and_then(Value::as_str), 625 Some("com.atproto.repo.strongRef"), 626 "signature should be a strongRef" 627 ); 628 assert!( 629 signature.get("uri").and_then(Value::as_str).is_some(), 630 "strongRef must contain a uri" 631 ); 632 assert!( 633 signature.get("cid").and_then(Value::as_str).is_some(), 634 "strongRef must contain a cid" 635 ); 636 637 Ok(()) 638 } 639 640 #[tokio::test] 641 async fn create_inline_attestation_full_workflow() -> Result<(), Box<dyn std::error::Error>> { 642 let private_key = generate_key(KeyType::K256Private)?; 643 let public_key = to_public(&private_key)?; 644 let key_reference = format!("{}", &public_key); 645 let repository_did = "did:plc:testrepository123"; 646 647 let base_record = json!({ 648 "$type": "app.example.record", 649 "body": "Sign me" 650 }); 651 652 let sig_metadata = json!({ 653 "$type": "com.example.inlineSignature", 654 "key": key_reference, 655 "purpose": "unit-test" 656 }); 657 658 let signed = create_inline_attestation( 659 AnyInput::Serialize(base_record), 660 AnyInput::Serialize(sig_metadata), 661 repository_did, 662 &private_key, 663 )?; 664 665 // Verify structure 666 let signatures = signed 667 .get("signatures") 668 .and_then(Value::as_array) 669 .expect("should have signatures array"); 670 assert_eq!(signatures.len(), 1); 671 672 let sig = &signatures[0]; 673 assert_eq!( 674 sig.get("$type").and_then(Value::as_str), 675 Some("com.example.inlineSignature") 676 ); 677 assert!(sig.get("signature").is_some()); 678 assert!(sig.get("key").is_some()); 679 assert!(sig.get("repository").is_none()); // Should not be in final signature 680 681 Ok(()) 682 } 683 684 #[test] 685 fn create_signature_returns_valid_bytes() -> Result<(), Box<dyn std::error::Error>> { 686 let private_key = generate_key(KeyType::K256Private)?; 687 let public_key = to_public(&private_key)?; 688 689 let record = json!({ 690 "$type": "app.example.record", 691 "body": "Test signature creation" 692 }); 693 694 let metadata = json!({ 695 "$type": "com.example.signature", 696 "key": format!("{}", public_key) 697 }); 698 699 let repository = "did:plc:test123"; 700 701 // Create signature 702 let signature_bytes = create_signature( 703 AnyInput::Serialize(record.clone()), 704 AnyInput::Serialize(metadata.clone()), 705 repository, 706 &private_key, 707 )?; 708 709 // Verify signature is not empty 710 assert!(!signature_bytes.is_empty(), "Signature bytes should not be empty"); 711 712 // Verify signature length is reasonable for ECDSA (typically 64-72 bytes for secp256k1) 713 assert!( 714 signature_bytes.len() >= 64 && signature_bytes.len() <= 73, 715 "Signature length should be between 64 and 73 bytes, got {}", 716 signature_bytes.len() 717 ); 718 719 // Verify we can validate the signature 720 let content_cid = create_attestation_cid( 721 AnyInput::Serialize(record), 722 AnyInput::Serialize(metadata), 723 repository, 724 )?; 725 726 validate(&public_key, &signature_bytes, &content_cid.to_bytes())?; 727 728 Ok(()) 729 } 730 731 #[test] 732 fn create_signature_different_inputs_produce_different_signatures() -> Result<(), Box<dyn std::error::Error>> { 733 let private_key = generate_key(KeyType::K256Private)?; 734 735 let record1 = json!({"$type": "app.example.record", "body": "First message"}); 736 let record2 = json!({"$type": "app.example.record", "body": "Second message"}); 737 let metadata = json!({"$type": "com.example.signature"}); 738 let repository = "did:plc:test123"; 739 740 let sig1 = create_signature( 741 AnyInput::Serialize(record1), 742 AnyInput::Serialize(metadata.clone()), 743 repository, 744 &private_key, 745 )?; 746 747 let sig2 = create_signature( 748 AnyInput::Serialize(record2), 749 AnyInput::Serialize(metadata), 750 repository, 751 &private_key, 752 )?; 753 754 assert_ne!(sig1, sig2, "Different records should produce different signatures"); 755 756 Ok(()) 757 } 758 759 #[test] 760 fn create_signature_different_repositories_produce_different_signatures() -> Result<(), Box<dyn std::error::Error>> { 761 let private_key = generate_key(KeyType::K256Private)?; 762 763 let record = json!({"$type": "app.example.record", "body": "Message"}); 764 let metadata = json!({"$type": "com.example.signature"}); 765 766 let sig1 = create_signature( 767 AnyInput::Serialize(record.clone()), 768 AnyInput::Serialize(metadata.clone()), 769 "did:plc:repo1", 770 &private_key, 771 )?; 772 773 let sig2 = create_signature( 774 AnyInput::Serialize(record), 775 AnyInput::Serialize(metadata), 776 "did:plc:repo2", 777 &private_key, 778 )?; 779 780 assert_ne!( 781 sig1, sig2, 782 "Different repository DIDs should produce different signatures" 783 ); 784 785 Ok(()) 786 } 787}