use atproto_identity::key::{KeyData, sign, validate};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde_json::json;
use crate::errors::VerificationError;
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);
};
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"),
);
}
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);
}
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);
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"),
);
}
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");
record_map.insert("signatures".to_string(), json!(signatures));
}
Ok(signed_record)
}
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 {
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>> {
let private_key = generate_key(KeyType::P256Private)?;
let public_key = to_public(&private_key)?;
let record = json!({
"text": "Hello AT Protocol!",
"createdAt": "2025-01-19T10:00:00Z",
"langs": ["en"]
});
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"
});
let signed_record = create(
&private_key,
&record,
repository,
collection,
signature_object.clone(),
)?;
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);
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")
);
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>> {
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>> {
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(),
)?;
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>> {
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";
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)?;
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,
)?;
let signatures = signed_twice
.get("signatures")
.and_then(|v| v.as_array())
.expect("signatures should exist");
assert_eq!(signatures.len(), 2);
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)?;
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)?;
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)?;
if let Some(obj) = signed.as_object_mut() {
obj.insert("text".to_string(), json!("tampered"));
}
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";
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>> {
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)?;
if let Some(obj) = signed.as_object_mut() {
if let Some(signatures) = obj.remove("signatures") {
obj.insert("sigs".to_string(), signatures);
}
}
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,
)?;
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"));
assert!(signed.get("signatures").is_some());
Ok(())
}
}