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}