atproto-record 0.11.3

AT Protocol record signature operations - cryptographic signing and verification for AT Protocol records
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
//! AT Protocol record signature creation and verification.
//!
//! This module provides comprehensive functionality for creating and verifying
//! cryptographic signatures on AT Protocol records following the
//! community.lexicon.attestation.signature specification.
//!
//! ## Signature Process
//!
//! 1. **Signing**: Records are augmented with a `$sig` object containing issuer,
//!    timestamp, and context information, then serialized using IPLD DAG-CBOR
//!    for deterministic encoding before signing with ECDSA.
//!
//! 2. **Storage**: Signatures are stored in a `signatures` array within the record,
//!    allowing multiple signatures from different issuers.
//!
//! 3. **Verification**: The original signed content is reconstructed by replacing
//!    the signatures array with the appropriate `$sig` object, then verified
//!    using the issuer's public key.
//!
//! ## Supported Curves
//!
//! - P-256 (NIST P-256 / secp256r1)
//! - P-384 (NIST P-384 / secp384r1)  
//! - K-256 (secp256k1)
//!
//! ## Example
//!
//! ```ignore
//! use atproto_record::signature::{create, verify};
//! use atproto_identity::key::identify_key;
//! use serde_json::json;
//!
//! // Create a signature
//! let key = identify_key("did:key:...")?;
//! let record = json!({"text": "Hello!"});
//! let sig_obj = json!({
//!     "issuer": "did:plc:issuer"
//!     // Optional: any additional fields like "issuedAt", "purpose", etc.
//! });
//!
//! let signed = create(&key, &record, "did:plc:repo",
//!                     "app.bsky.feed.post", sig_obj)?;
//!
//! // Verify the signature
//! verify("did:plc:issuer", &key, signed,
//!        "did:plc:repo", "app.bsky.feed.post")?;
//! ```

use atproto_identity::key::{KeyData, sign, validate};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde_json::json;

use crate::errors::VerificationError;

/// Creates a cryptographic signature for an AT Protocol record.
///
/// This function generates a signature following the community.lexicon.attestation.signature
/// specification. The record is augmented with a `$sig` object containing context information,
/// serialized using IPLD DAG-CBOR, signed with the provided key, and the signature is added
/// to a `signatures` array in the returned record.
///
/// # Parameters
///
/// * `key_data` - The signing key (private key) wrapped in KeyData
/// * `record` - The JSON record to be signed (will not be modified)
/// * `repository` - The repository DID where this record will be stored
/// * `collection` - The collection type (NSID) for this record
/// * `signature_object` - Metadata for the signature, must include:
///   - `issuer`: The DID of the entity creating the signature (required)
///   - Additional custom fields are preserved in the signature (optional)
///
/// # Returns
///
/// Returns a new record containing:
/// - All original record fields
/// - A `signatures` array with the new signature appended
/// - No `$sig` field (only used during signing)
///
/// # Errors
///
/// Returns [`VerificationError`] if:
/// - Required field `issuer` is missing from signature_object
/// - IPLD DAG-CBOR serialization fails
/// - Cryptographic signing operation fails
/// - JSON structure manipulation fails
pub fn create(
    key_data: &KeyData,
    record: &serde_json::Value,
    repository: &str,
    collection: &str,
    signature_object: serde_json::Value,
) -> Result<serde_json::Value, VerificationError> {
    if let Some(record_map) = signature_object.as_object() {
        if !record_map.contains_key("issuer") {
            return Err(VerificationError::SignatureObjectMissingField {
                field: "issuer".to_string(),
            });
        }
    } else {
        return Err(VerificationError::InvalidSignatureObjectType);
    };

    // Prepare the $sig object.
    let mut sig = signature_object.clone();
    if let Some(record_map) = sig.as_object_mut() {
        record_map.insert("repository".to_string(), json!(repository));
        record_map.insert("collection".to_string(), json!(collection));
        record_map.insert(
            "$type".to_string(),
            json!("community.lexicon.attestation.signature"),
        );
    }

    // Create a copy of the record with the $sig object for signing.
    let mut signing_record = record.clone();
    if let Some(record_map) = signing_record.as_object_mut() {
        record_map.remove("signatures");
        record_map.remove("$sig");
        record_map.insert("$sig".to_string(), sig);
    }

    // Create a signature.
    let serialized_signing_record = serde_ipld_dagcbor::to_vec(&signing_record)?;

    let signature: Vec<u8> = sign(key_data, &serialized_signing_record)?;
    let encoded_signature = STANDARD.encode(&signature);

    // Compose the proof object
    let mut proof = signature_object.clone();
    if let Some(record_map) = proof.as_object_mut() {
        record_map.remove("repository");
        record_map.remove("collection");
        record_map.insert(
            "signature".to_string(),
            json!({"$bytes": json!(encoded_signature)}),
        );
        record_map.insert(
            "$type".to_string(),
            json!("community.lexicon.attestation.signature"),
        );
    }

    // Add the signature to the original record
    let mut signed_record = record.clone();

    if let Some(record_map) = signed_record.as_object_mut() {
        let mut signatures: Vec<serde_json::Value> = record
            .get("signatures")
            .and_then(|v| v.as_array().cloned())
            .unwrap_or_default();

        signatures.push(proof);

        record_map.remove("$sig");
        record_map.remove("signatures");

        // Add the $sig field
        record_map.insert("signatures".to_string(), json!(signatures));
    }

    Ok(signed_record)
}

/// Verifies a cryptographic signature on an AT Protocol record.
///
/// This function validates signatures by reconstructing the original signed content
/// (record with `$sig` object) and verifying the ECDSA signature against it.
/// It searches through all signatures in the record to find one matching the
/// specified issuer, then verifies it with the provided public key.
///
/// # Parameters
///
/// * `issuer` - The DID of the expected signature issuer to verify
/// * `key_data` - The public key for signature verification
/// * `record` - The signed record containing a `signatures` or `sigs` array
/// * `repository` - The repository DID used during signing (must match)
/// * `collection` - The collection type used during signing (must match)
///
/// # Returns
///
/// Returns `Ok(())` if a valid signature from the specified issuer is found
/// and successfully verified against the reconstructed signed content.
///
/// # Errors
///
/// Returns [`VerificationError`] if:
/// - No `signatures` or `sigs` field exists in the record
/// - No signature from the specified issuer is found
/// - The issuer's signature is malformed or missing required fields
/// - The signature is not in the expected `{"$bytes": "..."}` format
/// - Base64 decoding of the signature fails
/// - IPLD DAG-CBOR serialization of reconstructed content fails
/// - Cryptographic verification fails (invalid signature)
///
/// # Note
///
/// This function supports both `signatures` and `sigs` field names for
/// backward compatibility with different AT Protocol implementations.
pub fn verify(
    issuer: &str,
    key_data: &KeyData,
    record: serde_json::Value,
    repository: &str,
    collection: &str,
) -> Result<(), VerificationError> {
    let signatures = record
        .get("sigs")
        .or_else(|| record.get("signatures"))
        .and_then(|v| v.as_array())
        .ok_or(VerificationError::NoSignaturesField)?;

    for sig_obj in signatures {
        // Extract the issuer from the signature object
        let signature_issuer = sig_obj
            .get("issuer")
            .and_then(|v| v.as_str())
            .ok_or(VerificationError::MissingIssuerField)?;

        let signature_value = sig_obj
            .get("signature")
            .and_then(|v| v.as_object())
            .and_then(|obj| obj.get("$bytes"))
            .and_then(|b| b.as_str())
            .ok_or(VerificationError::MissingSignatureField)?;

        if issuer != signature_issuer {
            continue;
        }

        let mut sig_variable = sig_obj.clone();

        if let Some(sig_map) = sig_variable.as_object_mut() {
            sig_map.remove("signature");
            sig_map.insert("repository".to_string(), json!(repository));
            sig_map.insert("collection".to_string(), json!(collection));
        }

        let mut signed_record = record.clone();
        if let Some(record_map) = signed_record.as_object_mut() {
            record_map.remove("signatures");
            record_map.remove("sigs");
            record_map.insert("$sig".to_string(), sig_variable);
        }

        let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record)
            .map_err(|error| VerificationError::RecordSerializationFailed { error })?;

        let signature_bytes = STANDARD
            .decode(signature_value)
            .map_err(|error| VerificationError::SignatureDecodingFailed { error })?;

        validate(key_data, &signature_bytes, &serialized_record)
            .map_err(|error| VerificationError::CryptographicValidationFailed { error })?;

        return Ok(());
    }

    Err(VerificationError::NoValidSignatureForIssuer {
        issuer: issuer.to_string(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use atproto_identity::key::{KeyType, generate_key, to_public};
    use serde_json::json;

    #[test]
    fn test_create_sign_and_verify_record_p256() -> Result<(), Box<dyn std::error::Error>> {
        // Step 1: Generate a P-256 key pair
        let private_key = generate_key(KeyType::P256Private)?;
        let public_key = to_public(&private_key)?;

        // Step 2: Create a sample record
        let record = json!({
            "text": "Hello AT Protocol!",
            "createdAt": "2025-01-19T10:00:00Z",
            "langs": ["en"]
        });

        // Step 3: Define signature metadata
        let issuer_did = "did:plc:test123";
        let repository = "did:plc:repo456";
        let collection = "app.bsky.feed.post";

        let signature_object = json!({
            "issuer": issuer_did,
            "issuedAt": "2025-01-19T10:00:00Z",
            "purpose": "attestation"
        });

        // Step 4: Sign the record
        let signed_record = create(
            &private_key,
            &record,
            repository,
            collection,
            signature_object.clone(),
        )?;

        // Verify that the signed record contains signatures array
        assert!(signed_record.get("signatures").is_some());
        let signatures = signed_record
            .get("signatures")
            .and_then(|v| v.as_array())
            .expect("signatures should be an array");
        assert_eq!(signatures.len(), 1);

        // Verify signature object structure
        let sig = &signatures[0];
        assert_eq!(sig.get("issuer").and_then(|v| v.as_str()), Some(issuer_did));
        assert!(sig.get("signature").is_some());
        assert_eq!(
            sig.get("$type").and_then(|v| v.as_str()),
            Some("community.lexicon.attestation.signature")
        );

        // Step 5: Verify the signature
        verify(
            issuer_did,
            &public_key,
            signed_record.clone(),
            repository,
            collection,
        )?;

        Ok(())
    }

    #[test]
    fn test_create_sign_and_verify_record_k256() -> Result<(), Box<dyn std::error::Error>> {
        // Test with K-256 curve
        let private_key = generate_key(KeyType::K256Private)?;
        let public_key = to_public(&private_key)?;

        let record = json!({
            "subject": "at://did:plc:example/app.bsky.feed.post/123",
            "likedAt": "2025-01-19T10:00:00Z"
        });

        let issuer_did = "did:plc:issuer789";
        let repository = "did:plc:repo789";
        let collection = "app.bsky.feed.like";

        let signature_object = json!({
            "issuer": issuer_did,
            "issuedAt": "2025-01-19T10:00:00Z"
        });

        let signed_record = create(
            &private_key,
            &record,
            repository,
            collection,
            signature_object,
        )?;

        verify(
            issuer_did,
            &public_key,
            signed_record,
            repository,
            collection,
        )?;

        Ok(())
    }

    #[test]
    fn test_create_sign_and_verify_record_p384() -> Result<(), Box<dyn std::error::Error>> {
        // Test with P-384 curve
        let private_key = generate_key(KeyType::P384Private)?;
        let public_key = to_public(&private_key)?;

        let record = json!({
            "displayName": "Test User",
            "description": "Testing P-384 signatures"
        });

        let issuer_did = "did:web:example.com";
        let repository = "did:plc:profile123";
        let collection = "app.bsky.actor.profile";

        let signature_object = json!({
            "issuer": issuer_did,
            "issuedAt": "2025-01-19T10:00:00Z",
            "expiresAt": "2025-01-20T10:00:00Z",
            "customField": "custom value"
        });

        let signed_record = create(
            &private_key,
            &record,
            repository,
            collection,
            signature_object.clone(),
        )?;

        // Verify custom fields are preserved in signature
        let signatures = signed_record
            .get("signatures")
            .and_then(|v| v.as_array())
            .expect("signatures should exist");
        let sig = &signatures[0];
        assert_eq!(
            sig.get("customField").and_then(|v| v.as_str()),
            Some("custom value")
        );

        verify(
            issuer_did,
            &public_key,
            signed_record,
            repository,
            collection,
        )?;

        Ok(())
    }

    #[test]
    fn test_multiple_signatures() -> Result<(), Box<dyn std::error::Error>> {
        // Create a record with multiple signatures from different issuers
        let private_key1 = generate_key(KeyType::P256Private)?;
        let public_key1 = to_public(&private_key1)?;

        let private_key2 = generate_key(KeyType::K256Private)?;
        let public_key2 = to_public(&private_key2)?;

        let record = json!({
            "text": "Multi-signed content",
            "important": true
        });

        let repository = "did:plc:repo_multi";
        let collection = "app.example.document";

        // First signature
        let issuer1 = "did:plc:issuer1";
        let sig_obj1 = json!({
            "issuer": issuer1,
            "issuedAt": "2025-01-19T09:00:00Z",
            "role": "author"
        });

        let signed_once = create(&private_key1, &record, repository, collection, sig_obj1)?;

        // Second signature on already signed record
        let issuer2 = "did:plc:issuer2";
        let sig_obj2 = json!({
            "issuer": issuer2,
            "issuedAt": "2025-01-19T10:00:00Z",
            "role": "reviewer"
        });

        let signed_twice = create(
            &private_key2,
            &signed_once,
            repository,
            collection,
            sig_obj2,
        )?;

        // Verify we have two signatures
        let signatures = signed_twice
            .get("signatures")
            .and_then(|v| v.as_array())
            .expect("signatures should exist");
        assert_eq!(signatures.len(), 2);

        // Verify both signatures independently
        verify(
            issuer1,
            &public_key1,
            signed_twice.clone(),
            repository,
            collection,
        )?;
        verify(
            issuer2,
            &public_key2,
            signed_twice.clone(),
            repository,
            collection,
        )?;

        Ok(())
    }

    #[test]
    fn test_verify_wrong_issuer_fails() -> Result<(), Box<dyn std::error::Error>> {
        let private_key = generate_key(KeyType::P256Private)?;
        let public_key = to_public(&private_key)?;

        let record = json!({"test": "data"});
        let repository = "did:plc:repo";
        let collection = "app.test";

        let sig_obj = json!({
            "issuer": "did:plc:correct_issuer"
        });

        let signed = create(&private_key, &record, repository, collection, sig_obj)?;

        // Try to verify with wrong issuer
        let result = verify(
            "did:plc:wrong_issuer",
            &public_key,
            signed,
            repository,
            collection,
        );

        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            VerificationError::NoValidSignatureForIssuer { .. }
        ));

        Ok(())
    }

    #[test]
    fn test_verify_wrong_key_fails() -> Result<(), Box<dyn std::error::Error>> {
        let private_key = generate_key(KeyType::P256Private)?;
        let wrong_private_key = generate_key(KeyType::P256Private)?;
        let wrong_public_key = to_public(&wrong_private_key)?;

        let record = json!({"test": "data"});
        let repository = "did:plc:repo";
        let collection = "app.test";
        let issuer = "did:plc:issuer";

        let sig_obj = json!({ "issuer": issuer });

        let signed = create(&private_key, &record, repository, collection, sig_obj)?;

        // Try to verify with wrong key
        let result = verify(issuer, &wrong_public_key, signed, repository, collection);

        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            VerificationError::CryptographicValidationFailed { .. }
        ));

        Ok(())
    }

    #[test]
    fn test_verify_tampered_record_fails() -> Result<(), Box<dyn std::error::Error>> {
        let private_key = generate_key(KeyType::P256Private)?;
        let public_key = to_public(&private_key)?;

        let record = json!({"text": "original"});
        let repository = "did:plc:repo";
        let collection = "app.test";
        let issuer = "did:plc:issuer";

        let sig_obj = json!({ "issuer": issuer });

        let mut signed = create(&private_key, &record, repository, collection, sig_obj)?;

        // Tamper with the record content
        if let Some(obj) = signed.as_object_mut() {
            obj.insert("text".to_string(), json!("tampered"));
        }

        // Verification should fail
        let result = verify(issuer, &public_key, signed, repository, collection);

        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            VerificationError::CryptographicValidationFailed { .. }
        ));

        Ok(())
    }

    #[test]
    fn test_create_missing_issuer_fails() -> Result<(), Box<dyn std::error::Error>> {
        let private_key = generate_key(KeyType::P256Private)?;

        let record = json!({"test": "data"});
        let repository = "did:plc:repo";
        let collection = "app.test";

        // Signature object without issuer field
        let sig_obj = json!({
            "issuedAt": "2025-01-19T10:00:00Z"
        });

        let result = create(&private_key, &record, repository, collection, sig_obj);

        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            VerificationError::SignatureObjectMissingField { field } if field == "issuer"
        ));

        Ok(())
    }

    #[test]
    fn test_verify_supports_sigs_field() -> Result<(), Box<dyn std::error::Error>> {
        // Test backward compatibility with "sigs" field name
        let private_key = generate_key(KeyType::P256Private)?;
        let public_key = to_public(&private_key)?;

        let record = json!({"test": "data"});
        let repository = "did:plc:repo";
        let collection = "app.test";
        let issuer = "did:plc:issuer";

        let sig_obj = json!({ "issuer": issuer });

        let mut signed = create(&private_key, &record, repository, collection, sig_obj)?;

        // Rename "signatures" to "sigs"
        if let Some(obj) = signed.as_object_mut() {
            if let Some(signatures) = obj.remove("signatures") {
                obj.insert("sigs".to_string(), signatures);
            }
        }

        // Should still verify successfully
        verify(issuer, &public_key, signed, repository, collection)?;

        Ok(())
    }

    #[test]
    fn test_signature_preserves_original_record() -> Result<(), Box<dyn std::error::Error>> {
        let private_key = generate_key(KeyType::P256Private)?;

        let original_record = json!({
            "text": "Original content",
            "metadata": {
                "author": "Test",
                "version": 1
            },
            "tags": ["test", "sample"]
        });

        let repository = "did:plc:repo";
        let collection = "app.test";

        let sig_obj = json!({
            "issuer": "did:plc:issuer"
        });

        let signed = create(
            &private_key,
            &original_record,
            repository,
            collection,
            sig_obj,
        )?;

        // All original fields should be preserved
        assert_eq!(signed.get("text"), original_record.get("text"));
        assert_eq!(signed.get("metadata"), original_record.get("metadata"));
        assert_eq!(signed.get("tags"), original_record.get("tags"));

        // Plus the new signatures field
        assert!(signed.get("signatures").is_some());

        Ok(())
    }
}