atproto-record 0.9.3

AT Protocol record signature operations - cryptographic signing and verification for AT Protocol records
Documentation
//! CLI tool for verifying AT Protocol record signatures.

use anyhow::Result;
use atproto_identity::{
    key::{KeyData, identify_key},
    resolve::{InputType, parse_input},
};
use atproto_record::errors::CliError;
use atproto_record::signature::verify;
use clap::Parser;
use std::{
    fs,
    io::{self, Read},
};

/// AT Protocol Record Verification CLI
#[derive(Parser)]
#[command(
    name = "atproto-record-verify",
    version,
    about = "Verify cryptographic signatures of AT Protocol records",
    long_about = "
A command-line tool for verifying cryptographic signatures of AT Protocol records.
Reads a signed JSON record from a file or stdin, validates the embedded signatures
using a public key, and reports verification success or failure.

The tool accepts flexible argument ordering with issuer DIDs, verification keys,
record inputs, and key=value parameters for repository and collection context.

REQUIRED PARAMETERS:
  repository=<DID>        Repository context used during signing
  collection=<name>       Collection type context used during signing

EXAMPLES:
  # Basic verification:
  atproto-record-verify \\
    did:plc:tgudj2fjm77pzkuawquqhsxm \\
    did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\
    ./signed_post.json \\
    repository=did:plc:4zutorghlchjxzgceklue4la \\
    collection=app.bsky.feed.post

  # Verify from stdin:
  echo '{\"signatures\":[...]}' | atproto-record-verify \\
    did:plc:issuer... did:key:z42tv1pb3... -- \\
    repository=did:plc:repo... collection=app.bsky.feed.post

VERIFICATION PROCESS:
  - Extracts signatures from the signatures array
  - Finds signatures matching the specified issuer DID  
  - Reconstructs $sig object with repository and collection context
  - Validates ECDSA signatures using P-256 or K-256 curves
"
)]
struct Args {
    /// All arguments - flexible parsing handles issuer DIDs, verification keys, files, and key=value pairs
    args: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();

    let arguments = args.args.into_iter();

    let mut collection: Option<String> = None;
    let mut repository: Option<String> = None;
    let mut record: Option<serde_json::Value> = None;
    let mut issuer: Option<String> = None;
    let mut key_data: Option<KeyData> = None;

    for argument in arguments {
        if let Some((key, value)) = argument.split_once("=") {
            match key {
                "collection" => {
                    collection = Some(value.to_string());
                }
                "repository" => {
                    repository = Some(value.to_string());
                }
                _ => {}
            }
        } else if argument.starts_with("did:key:") {
            // Parse the did:key to extract key data for verification
            key_data = Some(identify_key(&argument)?);
        } else if argument.starts_with("did:") {
            match parse_input(&argument) {
                Ok(InputType::Plc(did)) | Ok(InputType::Web(did)) => {
                    issuer = Some(did);
                }
                Ok(_) => {
                    return Err(CliError::UnsupportedDidMethod {
                        method: argument.clone(),
                    }
                    .into());
                }
                Err(_) => {
                    return Err(CliError::DidParseFailed {
                        did: argument.clone(),
                    }
                    .into());
                }
            }
        } else if argument == "--" {
            // Read record from stdin
            if record.is_none() {
                let mut stdin_content = String::new();
                io::stdin()
                    .read_to_string(&mut stdin_content)
                    .map_err(|_| CliError::StdinReadFailed)?;
                record = Some(
                    serde_json::from_str(&stdin_content)
                        .map_err(|_| CliError::StdinJsonParseFailed)?,
                );
            } else {
                return Err(CliError::UnexpectedArgument {
                    argument: argument.clone(),
                }
                .into());
            }
        } else {
            // Assume it's a file path to read the record from
            if record.is_none() {
                let file_content =
                    fs::read_to_string(&argument).map_err(|_| CliError::FileReadFailed {
                        path: argument.clone(),
                    })?;
                record = Some(serde_json::from_str(&file_content).map_err(|_| {
                    CliError::FileJsonParseFailed {
                        path: argument.clone(),
                    }
                })?);
            } else {
                return Err(CliError::UnexpectedArgument {
                    argument: argument.clone(),
                }
                .into());
            }
        }
    }

    let collection = collection.ok_or(CliError::MissingRequiredValue {
        name: "collection".to_string(),
    })?;
    let repository = repository.ok_or(CliError::MissingRequiredValue {
        name: "repository".to_string(),
    })?;
    let record = record.ok_or(CliError::MissingRequiredValue {
        name: "record".to_string(),
    })?;
    let issuer = issuer.ok_or(CliError::MissingRequiredValue {
        name: "issuer".to_string(),
    })?;
    let key_data = key_data.ok_or(CliError::MissingRequiredValue {
        name: "key".to_string(),
    })?;

    verify(&issuer, &key_data, record, &repository, &collection).await?;

    println!("OK");

    Ok(())
}