A library for ATProtocol identities.

feature: Created create_signature function in atproto-attestation

Changed files
+180 -1
crates
atproto-attestation
+179
crates/atproto-attestation/src/attestation.rs
··· 43 43 Ok(Value::Object(record_obj)) 44 44 } 45 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 + /// ``` 100 + pub 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> 106 + where 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 + 46 121 /// Creates a remote attestation with both the attested record and proof record. 47 122 /// 48 123 /// This is the recommended way to create remote attestations. It generates both: ··· 602 677 assert!(sig.get("signature").is_some()); 603 678 assert!(sig.get("key").is_some()); 604 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 + ); 605 784 606 785 Ok(()) 607 786 }
+1 -1
crates/atproto-attestation/src/lib.rs
··· 59 59 // Re-export attestation functions 60 60 pub use attestation::{ 61 61 append_inline_attestation, append_remote_attestation, create_inline_attestation, 62 - create_remote_attestation, 62 + create_remote_attestation, create_signature, 63 63 }; 64 64 65 65 // Re-export input types