+1
-1
crates/atproto-jetstream/src/consumer.rs
+1
-1
crates/atproto-jetstream/src/consumer.rs
···
153
pub(crate) enum SubscriberSourcedMessage {
154
#[serde(rename = "options_update")]
155
Update {
156
+
#[serde(rename = "wantedCollections", skip_serializing_if = "Vec::is_empty", default)]
157
wanted_collections: Vec<String>,
158
159
#[serde(rename = "wantedDids", skip_serializing_if = "Vec::is_empty", default)]
+12
-4
crates/atproto-record/src/bytes.rs
+12
-4
crates/atproto-record/src/bytes.rs
···
21
//!
22
//! ## Base64 Encoding
23
//!
24
-
//! The serialization uses standard base64 encoding (RFC 4648) with padding,
25
-
//! compatible with the `$bytes` field format used throughout AT Protocol.
26
27
/// Base64 serialization format for byte arrays.
28
///
···
34
use serde::{Deserialize, Serialize};
35
use serde::{Deserializer, Serializer};
36
37
-
use base64::{Engine, engine::general_purpose::STANDARD};
38
39
/// Serializes a byte vector to a base64 encoded string.
40
pub fn serialize<S: Serializer>(value: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error> {
···
43
}
44
45
/// Deserializes a base64 encoded string to a byte vector.
46
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
47
let encoded_value = String::deserialize(d)?;
48
STANDARD
49
-
.decode(encoded_value)
50
.map_err(serde::de::Error::custom)
51
}
52
}
···
21
//!
22
//! ## Base64 Encoding
23
//!
24
+
//! The serialization uses standard base64 encoding (RFC 4648) with padding.
25
+
//! The deserialization accepts both padded and unpadded base64 strings for
26
+
//! compatibility with various AT Protocol implementations.
27
28
/// Base64 serialization format for byte arrays.
29
///
···
35
use serde::{Deserialize, Serialize};
36
use serde::{Deserializer, Serializer};
37
38
+
use base64::{Engine, engine::general_purpose::{STANDARD, STANDARD_NO_PAD}};
39
40
/// Serializes a byte vector to a base64 encoded string.
41
pub fn serialize<S: Serializer>(value: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error> {
···
44
}
45
46
/// Deserializes a base64 encoded string to a byte vector.
47
+
/// Handles both padded and unpadded base64 strings for compatibility.
48
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
49
let encoded_value = String::deserialize(d)?;
50
+
51
+
// Try standard base64 with padding first
52
STANDARD
53
+
.decode(&encoded_value)
54
+
.or_else(|_| {
55
+
// If that fails, try without padding requirement
56
+
STANDARD_NO_PAD.decode(&encoded_value)
57
+
})
58
.map_err(serde::de::Error::custom)
59
}
60
}
+70
crates/atproto-record/src/lexicon/community_lexicon_attestation.rs
+70
crates/atproto-record/src/lexicon/community_lexicon_attestation.rs
···
145
use serde_json::json;
146
147
#[test]
148
+
fn test_real_signature_or_ref_deserialization() {
149
+
// Test with the exact signature structure from the user's RSVP
150
+
let json_str = r#"{
151
+
"$type": "community.lexicon.attestation.signature",
152
+
"issuedAt": "2025-08-19T20:17:17.133Z",
153
+
"issuer": "did:web:acudo-dev.smokesignal.tools",
154
+
"signature": {
155
+
"$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA"
156
+
}
157
+
}"#;
158
+
159
+
// First, try to deserialize as TypedSignature
160
+
let typed_sig_result: Result<TypedSignature, _> = serde_json::from_str(json_str);
161
+
match &typed_sig_result {
162
+
Ok(sig) => {
163
+
println!("TypedSignature OK: issuer={}", sig.inner.issuer);
164
+
assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools");
165
+
}
166
+
Err(e) => {
167
+
eprintln!("TypedSignature deserialization error: {}", e);
168
+
}
169
+
}
170
+
171
+
// Then try as SignatureOrRef
172
+
let sig_or_ref_result: Result<SignatureOrRef, _> = serde_json::from_str(json_str);
173
+
match &sig_or_ref_result {
174
+
Ok(SignatureOrRef::Inline(sig)) => {
175
+
println!("SignatureOrRef OK (Inline): issuer={}", sig.inner.issuer);
176
+
assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools");
177
+
}
178
+
Ok(SignatureOrRef::Reference(_)) => {
179
+
panic!("Expected Inline signature, got Reference");
180
+
}
181
+
Err(e) => {
182
+
eprintln!("SignatureOrRef deserialization error: {}", e);
183
+
}
184
+
}
185
+
186
+
// Try without $type field
187
+
let json_no_type = r#"{
188
+
"issuedAt": "2025-08-19T20:17:17.133Z",
189
+
"issuer": "did:web:acudo-dev.smokesignal.tools",
190
+
"signature": {
191
+
"$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA"
192
+
}
193
+
}"#;
194
+
195
+
let no_type_result: Result<Signature, _> = serde_json::from_str(json_no_type);
196
+
match &no_type_result {
197
+
Ok(sig) => {
198
+
println!("Signature (no type) OK: issuer={}", sig.issuer);
199
+
assert_eq!(sig.issuer, "did:web:acudo-dev.smokesignal.tools");
200
+
assert_eq!(sig.signature.bytes.len(), 64);
201
+
202
+
// Now wrap it in TypedLexicon and try as SignatureOrRef
203
+
let typed = TypedLexicon::new(sig.clone());
204
+
let _as_sig_or_ref = SignatureOrRef::Inline(typed);
205
+
println!("Successfully created SignatureOrRef from Signature");
206
+
}
207
+
Err(e) => {
208
+
eprintln!("Signature (no type) deserialization error: {}", e);
209
+
}
210
+
}
211
+
212
+
// Check that at least one worked
213
+
assert!(typed_sig_result.is_ok() || sig_or_ref_result.is_ok() || no_type_result.is_ok(),
214
+
"Failed to deserialize signature in any form");
215
+
}
216
+
217
+
#[test]
218
fn test_signature_deserialization() {
219
let json_str = r#"{
220
"$type": "community.lexicon.attestation.signature",
+78
crates/atproto-record/src/lexicon/community_lexicon_calendar_rsvp.rs
+78
crates/atproto-record/src/lexicon/community_lexicon_calendar_rsvp.rs
···
302
303
Ok(())
304
}
305
+
306
+
#[test]
307
+
fn test_deserialize_real_rsvp_with_signature() -> Result<()> {
308
+
use crate::lexicon::community_lexicon_attestation::SignatureOrRef;
309
+
use chrono::Timelike;
310
+
use serde_json::Value;
311
+
312
+
// Real RSVP JSON with actual signature from the user
313
+
let json_str = r#"{
314
+
"$type": "community.lexicon.calendar.rsvp",
315
+
"createdAt": "2025-08-19T20:17:17.133Z",
316
+
"signatures": [
317
+
{
318
+
"$type": "community.lexicon.attestation.signature",
319
+
"issuedAt": "2025-08-19T20:17:17.133Z",
320
+
"issuer": "did:web:acudo-dev.smokesignal.tools",
321
+
"signature": {
322
+
"$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA"
323
+
}
324
+
}
325
+
],
326
+
"status": "community.lexicon.calendar.rsvp#going",
327
+
"subject": {
328
+
"cid": "bafyreidtr72nriwlscae3gfwhckxiy427b6ni5jmetzkvaafimeuelnlxa",
329
+
"uri": "at://did:web:acudo-dev.smokesignal.tools/community.lexicon.calendar.event/3lwrfkzy36k2s"
330
+
}
331
+
}"#;
332
+
333
+
// First parse as generic JSON to verify structure
334
+
let json_value: Value = serde_json::from_str(json_str)?;
335
+
assert_eq!(json_value["$type"], "community.lexicon.calendar.rsvp");
336
+
assert_eq!(json_value["status"], "community.lexicon.calendar.rsvp#going");
337
+
338
+
// Deserialize the JSON
339
+
let typed_rsvp: TypedRsvp = serde_json::from_str(json_str)?;
340
+
341
+
// Verify the basic fields
342
+
assert_eq!(typed_rsvp.inner.status, RsvpStatus::Going);
343
+
assert_eq!(
344
+
typed_rsvp.inner.subject.uri,
345
+
"at://did:web:acudo-dev.smokesignal.tools/community.lexicon.calendar.event/3lwrfkzy36k2s"
346
+
);
347
+
assert_eq!(
348
+
typed_rsvp.inner.subject.cid,
349
+
"bafyreidtr72nriwlscae3gfwhckxiy427b6ni5jmetzkvaafimeuelnlxa"
350
+
);
351
+
352
+
// Verify the timestamp
353
+
let expected_time = Utc.with_ymd_and_hms(2025, 8, 19, 20, 17, 17)
354
+
.unwrap()
355
+
.with_nanosecond(133_000_000)
356
+
.unwrap();
357
+
assert_eq!(typed_rsvp.inner.created_at, expected_time);
358
+
359
+
// Verify the signature
360
+
assert_eq!(typed_rsvp.inner.signatures.len(), 1);
361
+
match &typed_rsvp.inner.signatures[0] {
362
+
SignatureOrRef::Inline(sig) => {
363
+
assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools");
364
+
365
+
// Verify the issuedAt field if present
366
+
if let Some(issued_at_value) = sig.inner.extra.get("issuedAt") {
367
+
assert_eq!(issued_at_value, "2025-08-19T20:17:17.133Z");
368
+
}
369
+
370
+
// Verify the signature is base64 encoded data
371
+
// The signature should be 64 bytes for P-256 ECDSA (32 bytes for r, 32 bytes for s)
372
+
assert_eq!(sig.inner.signature.bytes.len(), 64);
373
+
}
374
+
_ => panic!("Expected inline signature"),
375
+
}
376
+
377
+
// Verify the type field is present
378
+
assert!(typed_rsvp.has_type_field());
379
+
assert!(typed_rsvp.validate().is_ok());
380
+
381
+
Ok(())
382
+
}
383
}
+423
-8
crates/atproto-record/src/signature.rs
+423
-8
crates/atproto-record/src/signature.rs
···
39
//! });
40
//!
41
//! let signed = create(&key, &record, "did:plc:repo",
42
-
//! "app.bsky.feed.post", sig_obj).await?;
43
//!
44
//! // Verify the signature
45
//! verify("did:plc:issuer", &key, signed,
46
-
//! "did:plc:repo", "app.bsky.feed.post").await?;
47
//! ```
48
49
use atproto_identity::key::{KeyData, sign, validate};
50
-
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
51
use serde_json::json;
52
53
use crate::errors::VerificationError;
···
83
/// - IPLD DAG-CBOR serialization fails
84
/// - Cryptographic signing operation fails
85
/// - JSON structure manipulation fails
86
-
pub async fn create(
87
key_data: &KeyData,
88
record: &serde_json::Value,
89
repository: &str,
···
123
let serialized_signing_record = serde_ipld_dagcbor::to_vec(&signing_record)?;
124
125
let signature: Vec<u8> = sign(key_data, &serialized_signing_record)?;
126
-
let encoded_signature = URL_SAFE_NO_PAD.encode(&signature);
127
128
// Compose the proof object
129
let mut proof = signature_object.clone();
···
187
/// - No `signatures` or `sigs` field exists in the record
188
/// - No signature from the specified issuer is found
189
/// - The issuer's signature is malformed or missing required fields
190
/// - Base64 decoding of the signature fails
191
/// - IPLD DAG-CBOR serialization of reconstructed content fails
192
/// - Cryptographic verification fails (invalid signature)
···
195
///
196
/// This function supports both `signatures` and `sigs` field names for
197
/// backward compatibility with different AT Protocol implementations.
198
-
pub async fn verify(
199
issuer: &str,
200
key_data: &KeyData,
201
record: serde_json::Value,
···
217
218
let signature_value = sig_obj
219
.get("signature")
220
-
.and_then(|v| v.as_str())
221
.ok_or(VerificationError::MissingSignatureField)?;
222
223
if issuer != signature_issuer {
···
235
let mut signed_record = record.clone();
236
if let Some(record_map) = signed_record.as_object_mut() {
237
record_map.remove("signatures");
238
record_map.insert("$sig".to_string(), sig_variable);
239
}
240
241
let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record)
242
.map_err(|error| VerificationError::RecordSerializationFailed { error })?;
243
244
-
let signature_bytes = URL_SAFE_NO_PAD
245
.decode(signature_value)
246
.map_err(|error| VerificationError::SignatureDecodingFailed { error })?;
247
···
255
issuer: issuer.to_string(),
256
})
257
}
···
39
//! });
40
//!
41
//! let signed = create(&key, &record, "did:plc:repo",
42
+
//! "app.bsky.feed.post", sig_obj)?;
43
//!
44
//! // Verify the signature
45
//! verify("did:plc:issuer", &key, signed,
46
+
//! "did:plc:repo", "app.bsky.feed.post")?;
47
//! ```
48
49
use atproto_identity::key::{KeyData, sign, validate};
50
+
use base64::{Engine, engine::general_purpose::STANDARD};
51
use serde_json::json;
52
53
use crate::errors::VerificationError;
···
83
/// - IPLD DAG-CBOR serialization fails
84
/// - Cryptographic signing operation fails
85
/// - JSON structure manipulation fails
86
+
pub fn create(
87
key_data: &KeyData,
88
record: &serde_json::Value,
89
repository: &str,
···
123
let serialized_signing_record = serde_ipld_dagcbor::to_vec(&signing_record)?;
124
125
let signature: Vec<u8> = sign(key_data, &serialized_signing_record)?;
126
+
let encoded_signature = STANDARD.encode(&signature);
127
128
// Compose the proof object
129
let mut proof = signature_object.clone();
···
187
/// - No `signatures` or `sigs` field exists in the record
188
/// - No signature from the specified issuer is found
189
/// - The issuer's signature is malformed or missing required fields
190
+
/// - The signature is not in the expected `{"$bytes": "..."}` format
191
/// - Base64 decoding of the signature fails
192
/// - IPLD DAG-CBOR serialization of reconstructed content fails
193
/// - Cryptographic verification fails (invalid signature)
···
196
///
197
/// This function supports both `signatures` and `sigs` field names for
198
/// backward compatibility with different AT Protocol implementations.
199
+
pub fn verify(
200
issuer: &str,
201
key_data: &KeyData,
202
record: serde_json::Value,
···
218
219
let signature_value = sig_obj
220
.get("signature")
221
+
.and_then(|v| v.as_object())
222
+
.and_then(|obj| obj.get("$bytes"))
223
+
.and_then(|b| b.as_str())
224
.ok_or(VerificationError::MissingSignatureField)?;
225
226
if issuer != signature_issuer {
···
238
let mut signed_record = record.clone();
239
if let Some(record_map) = signed_record.as_object_mut() {
240
record_map.remove("signatures");
241
+
record_map.remove("sigs");
242
record_map.insert("$sig".to_string(), sig_variable);
243
}
244
245
let serialized_record = serde_ipld_dagcbor::to_vec(&signed_record)
246
.map_err(|error| VerificationError::RecordSerializationFailed { error })?;
247
248
+
let signature_bytes = STANDARD
249
.decode(signature_value)
250
.map_err(|error| VerificationError::SignatureDecodingFailed { error })?;
251
···
259
issuer: issuer.to_string(),
260
})
261
}
262
+
263
+
#[cfg(test)]
264
+
mod tests {
265
+
use super::*;
266
+
use atproto_identity::key::{KeyType, generate_key, to_public};
267
+
use serde_json::json;
268
+
269
+
#[test]
270
+
fn test_create_sign_and_verify_record_p256() -> Result<(), Box<dyn std::error::Error>> {
271
+
// Step 1: Generate a P-256 key pair
272
+
let private_key = generate_key(KeyType::P256Private)?;
273
+
let public_key = to_public(&private_key)?;
274
+
275
+
// Step 2: Create a sample record
276
+
let record = json!({
277
+
"text": "Hello AT Protocol!",
278
+
"createdAt": "2025-01-19T10:00:00Z",
279
+
"langs": ["en"]
280
+
});
281
+
282
+
// Step 3: Define signature metadata
283
+
let issuer_did = "did:plc:test123";
284
+
let repository = "did:plc:repo456";
285
+
let collection = "app.bsky.feed.post";
286
+
287
+
let signature_object = json!({
288
+
"issuer": issuer_did,
289
+
"issuedAt": "2025-01-19T10:00:00Z",
290
+
"purpose": "attestation"
291
+
});
292
+
293
+
// Step 4: Sign the record
294
+
let signed_record = create(
295
+
&private_key,
296
+
&record,
297
+
repository,
298
+
collection,
299
+
signature_object.clone(),
300
+
)?;
301
+
302
+
// Verify that the signed record contains signatures array
303
+
assert!(signed_record.get("signatures").is_some());
304
+
let signatures = signed_record
305
+
.get("signatures")
306
+
.and_then(|v| v.as_array())
307
+
.expect("signatures should be an array");
308
+
assert_eq!(signatures.len(), 1);
309
+
310
+
// Verify signature object structure
311
+
let sig = &signatures[0];
312
+
assert_eq!(sig.get("issuer").and_then(|v| v.as_str()), Some(issuer_did));
313
+
assert!(sig.get("signature").is_some());
314
+
assert_eq!(
315
+
sig.get("$type").and_then(|v| v.as_str()),
316
+
Some("community.lexicon.attestation.signature")
317
+
);
318
+
319
+
// Step 5: Verify the signature
320
+
verify(
321
+
issuer_did,
322
+
&public_key,
323
+
signed_record.clone(),
324
+
repository,
325
+
collection,
326
+
)?;
327
+
328
+
Ok(())
329
+
}
330
+
331
+
#[test]
332
+
fn test_create_sign_and_verify_record_k256() -> Result<(), Box<dyn std::error::Error>> {
333
+
// Test with K-256 curve
334
+
let private_key = generate_key(KeyType::K256Private)?;
335
+
let public_key = to_public(&private_key)?;
336
+
337
+
let record = json!({
338
+
"subject": "at://did:plc:example/app.bsky.feed.post/123",
339
+
"likedAt": "2025-01-19T10:00:00Z"
340
+
});
341
+
342
+
let issuer_did = "did:plc:issuer789";
343
+
let repository = "did:plc:repo789";
344
+
let collection = "app.bsky.feed.like";
345
+
346
+
let signature_object = json!({
347
+
"issuer": issuer_did,
348
+
"issuedAt": "2025-01-19T10:00:00Z"
349
+
});
350
+
351
+
let signed_record = create(
352
+
&private_key,
353
+
&record,
354
+
repository,
355
+
collection,
356
+
signature_object,
357
+
)?;
358
+
359
+
verify(
360
+
issuer_did,
361
+
&public_key,
362
+
signed_record,
363
+
repository,
364
+
collection,
365
+
)?;
366
+
367
+
Ok(())
368
+
}
369
+
370
+
#[test]
371
+
fn test_create_sign_and_verify_record_p384() -> Result<(), Box<dyn std::error::Error>> {
372
+
// Test with P-384 curve
373
+
let private_key = generate_key(KeyType::P384Private)?;
374
+
let public_key = to_public(&private_key)?;
375
+
376
+
let record = json!({
377
+
"displayName": "Test User",
378
+
"description": "Testing P-384 signatures"
379
+
});
380
+
381
+
let issuer_did = "did:web:example.com";
382
+
let repository = "did:plc:profile123";
383
+
let collection = "app.bsky.actor.profile";
384
+
385
+
let signature_object = json!({
386
+
"issuer": issuer_did,
387
+
"issuedAt": "2025-01-19T10:00:00Z",
388
+
"expiresAt": "2025-01-20T10:00:00Z",
389
+
"customField": "custom value"
390
+
});
391
+
392
+
let signed_record = create(
393
+
&private_key,
394
+
&record,
395
+
repository,
396
+
collection,
397
+
signature_object.clone(),
398
+
)?;
399
+
400
+
// Verify custom fields are preserved in signature
401
+
let signatures = signed_record
402
+
.get("signatures")
403
+
.and_then(|v| v.as_array())
404
+
.expect("signatures should exist");
405
+
let sig = &signatures[0];
406
+
assert_eq!(
407
+
sig.get("customField").and_then(|v| v.as_str()),
408
+
Some("custom value")
409
+
);
410
+
411
+
verify(
412
+
issuer_did,
413
+
&public_key,
414
+
signed_record,
415
+
repository,
416
+
collection,
417
+
)?;
418
+
419
+
Ok(())
420
+
}
421
+
422
+
#[test]
423
+
fn test_multiple_signatures() -> Result<(), Box<dyn std::error::Error>> {
424
+
// Create a record with multiple signatures from different issuers
425
+
let private_key1 = generate_key(KeyType::P256Private)?;
426
+
let public_key1 = to_public(&private_key1)?;
427
+
428
+
let private_key2 = generate_key(KeyType::K256Private)?;
429
+
let public_key2 = to_public(&private_key2)?;
430
+
431
+
let record = json!({
432
+
"text": "Multi-signed content",
433
+
"important": true
434
+
});
435
+
436
+
let repository = "did:plc:repo_multi";
437
+
let collection = "app.example.document";
438
+
439
+
// First signature
440
+
let issuer1 = "did:plc:issuer1";
441
+
let sig_obj1 = json!({
442
+
"issuer": issuer1,
443
+
"issuedAt": "2025-01-19T09:00:00Z",
444
+
"role": "author"
445
+
});
446
+
447
+
let signed_once = create(&private_key1, &record, repository, collection, sig_obj1)?;
448
+
449
+
// Second signature on already signed record
450
+
let issuer2 = "did:plc:issuer2";
451
+
let sig_obj2 = json!({
452
+
"issuer": issuer2,
453
+
"issuedAt": "2025-01-19T10:00:00Z",
454
+
"role": "reviewer"
455
+
});
456
+
457
+
let signed_twice = create(
458
+
&private_key2,
459
+
&signed_once,
460
+
repository,
461
+
collection,
462
+
sig_obj2,
463
+
)?;
464
+
465
+
// Verify we have two signatures
466
+
let signatures = signed_twice
467
+
.get("signatures")
468
+
.and_then(|v| v.as_array())
469
+
.expect("signatures should exist");
470
+
assert_eq!(signatures.len(), 2);
471
+
472
+
// Verify both signatures independently
473
+
verify(
474
+
issuer1,
475
+
&public_key1,
476
+
signed_twice.clone(),
477
+
repository,
478
+
collection,
479
+
)?;
480
+
verify(
481
+
issuer2,
482
+
&public_key2,
483
+
signed_twice.clone(),
484
+
repository,
485
+
collection,
486
+
)?;
487
+
488
+
Ok(())
489
+
}
490
+
491
+
#[test]
492
+
fn test_verify_wrong_issuer_fails() -> Result<(), Box<dyn std::error::Error>> {
493
+
let private_key = generate_key(KeyType::P256Private)?;
494
+
let public_key = to_public(&private_key)?;
495
+
496
+
let record = json!({"test": "data"});
497
+
let repository = "did:plc:repo";
498
+
let collection = "app.test";
499
+
500
+
let sig_obj = json!({
501
+
"issuer": "did:plc:correct_issuer"
502
+
});
503
+
504
+
let signed = create(&private_key, &record, repository, collection, sig_obj)?;
505
+
506
+
// Try to verify with wrong issuer
507
+
let result = verify(
508
+
"did:plc:wrong_issuer",
509
+
&public_key,
510
+
signed,
511
+
repository,
512
+
collection,
513
+
);
514
+
515
+
assert!(result.is_err());
516
+
assert!(matches!(
517
+
result.unwrap_err(),
518
+
VerificationError::NoValidSignatureForIssuer { .. }
519
+
));
520
+
521
+
Ok(())
522
+
}
523
+
524
+
#[test]
525
+
fn test_verify_wrong_key_fails() -> Result<(), Box<dyn std::error::Error>> {
526
+
let private_key = generate_key(KeyType::P256Private)?;
527
+
let wrong_private_key = generate_key(KeyType::P256Private)?;
528
+
let wrong_public_key = to_public(&wrong_private_key)?;
529
+
530
+
let record = json!({"test": "data"});
531
+
let repository = "did:plc:repo";
532
+
let collection = "app.test";
533
+
let issuer = "did:plc:issuer";
534
+
535
+
let sig_obj = json!({ "issuer": issuer });
536
+
537
+
let signed = create(&private_key, &record, repository, collection, sig_obj)?;
538
+
539
+
// Try to verify with wrong key
540
+
let result = verify(issuer, &wrong_public_key, signed, repository, collection);
541
+
542
+
assert!(result.is_err());
543
+
assert!(matches!(
544
+
result.unwrap_err(),
545
+
VerificationError::CryptographicValidationFailed { .. }
546
+
));
547
+
548
+
Ok(())
549
+
}
550
+
551
+
#[test]
552
+
fn test_verify_tampered_record_fails() -> Result<(), Box<dyn std::error::Error>> {
553
+
let private_key = generate_key(KeyType::P256Private)?;
554
+
let public_key = to_public(&private_key)?;
555
+
556
+
let record = json!({"text": "original"});
557
+
let repository = "did:plc:repo";
558
+
let collection = "app.test";
559
+
let issuer = "did:plc:issuer";
560
+
561
+
let sig_obj = json!({ "issuer": issuer });
562
+
563
+
let mut signed = create(&private_key, &record, repository, collection, sig_obj)?;
564
+
565
+
// Tamper with the record content
566
+
if let Some(obj) = signed.as_object_mut() {
567
+
obj.insert("text".to_string(), json!("tampered"));
568
+
}
569
+
570
+
// Verification should fail
571
+
let result = verify(issuer, &public_key, signed, repository, collection);
572
+
573
+
assert!(result.is_err());
574
+
assert!(matches!(
575
+
result.unwrap_err(),
576
+
VerificationError::CryptographicValidationFailed { .. }
577
+
));
578
+
579
+
Ok(())
580
+
}
581
+
582
+
#[test]
583
+
fn test_create_missing_issuer_fails() -> Result<(), Box<dyn std::error::Error>> {
584
+
let private_key = generate_key(KeyType::P256Private)?;
585
+
586
+
let record = json!({"test": "data"});
587
+
let repository = "did:plc:repo";
588
+
let collection = "app.test";
589
+
590
+
// Signature object without issuer field
591
+
let sig_obj = json!({
592
+
"issuedAt": "2025-01-19T10:00:00Z"
593
+
});
594
+
595
+
let result = create(&private_key, &record, repository, collection, sig_obj);
596
+
597
+
assert!(result.is_err());
598
+
assert!(matches!(
599
+
result.unwrap_err(),
600
+
VerificationError::SignatureObjectMissingField { field } if field == "issuer"
601
+
));
602
+
603
+
Ok(())
604
+
}
605
+
606
+
#[test]
607
+
fn test_verify_supports_sigs_field() -> Result<(), Box<dyn std::error::Error>> {
608
+
// Test backward compatibility with "sigs" field name
609
+
let private_key = generate_key(KeyType::P256Private)?;
610
+
let public_key = to_public(&private_key)?;
611
+
612
+
let record = json!({"test": "data"});
613
+
let repository = "did:plc:repo";
614
+
let collection = "app.test";
615
+
let issuer = "did:plc:issuer";
616
+
617
+
let sig_obj = json!({ "issuer": issuer });
618
+
619
+
let mut signed = create(&private_key, &record, repository, collection, sig_obj)?;
620
+
621
+
// Rename "signatures" to "sigs"
622
+
if let Some(obj) = signed.as_object_mut() {
623
+
if let Some(signatures) = obj.remove("signatures") {
624
+
obj.insert("sigs".to_string(), signatures);
625
+
}
626
+
}
627
+
628
+
// Should still verify successfully
629
+
verify(issuer, &public_key, signed, repository, collection)?;
630
+
631
+
Ok(())
632
+
}
633
+
634
+
#[test]
635
+
fn test_signature_preserves_original_record() -> Result<(), Box<dyn std::error::Error>> {
636
+
let private_key = generate_key(KeyType::P256Private)?;
637
+
638
+
let original_record = json!({
639
+
"text": "Original content",
640
+
"metadata": {
641
+
"author": "Test",
642
+
"version": 1
643
+
},
644
+
"tags": ["test", "sample"]
645
+
});
646
+
647
+
let repository = "did:plc:repo";
648
+
let collection = "app.test";
649
+
650
+
let sig_obj = json!({
651
+
"issuer": "did:plc:issuer"
652
+
});
653
+
654
+
let signed = create(
655
+
&private_key,
656
+
&original_record,
657
+
repository,
658
+
collection,
659
+
sig_obj,
660
+
)?;
661
+
662
+
// All original fields should be preserved
663
+
assert_eq!(signed.get("text"), original_record.get("text"));
664
+
assert_eq!(signed.get("metadata"), original_record.get("metadata"));
665
+
assert_eq!(signed.get("tags"), original_record.get("tags"));
666
+
667
+
// Plus the new signatures field
668
+
assert!(signed.get("signatures").is_some());
669
+
670
+
Ok(())
671
+
}
672
+
}