atproto-record 0.11.0

AT Protocol record signature operations - cryptographic signing and verification for AT Protocol records
Documentation
//! 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).await?;
//!
//! // Verify the signature
//! verify("did:plc:issuer", &key, signed,
//!        "did:plc:repo", "app.bsky.feed.post").await?;
//! ```

use atproto_identity::key::{KeyData, sign, validate};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
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 async 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 = URL_SAFE_NO_PAD.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
/// - 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 async 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_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.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 = URL_SAFE_NO_PAD
            .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(),
    })
}