+179
crates/atproto-attestation/src/attestation.rs
+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
+1
-1
crates/atproto-attestation/src/lib.rs