printwell-cli 0.1.11

Command-line tool for HTML to PDF conversion
Documentation
//! Signing commands implementation.

use anyhow::{Context, Result};

use crate::cli::args::{ListFieldsArgs, SignArgs, VerifyArgs};
use crate::cli::utils::{parse_page_coords, resolve_password};

pub fn sign(args: SignArgs) -> Result<()> {
    use printwell::signing::{
        MdpPermissions, SignatureAppearance, SignatureLevel, SigningCertificate, SigningOptions,
        sign_pdf, sign_pdf_visible,
    };

    let pdf_data = std::fs::read(&args.input)
        .with_context(|| format!("Failed to read input file: {}", args.input))?;

    // Resolve password from direct, file, or env sources
    let password = resolve_password(
        args.password.as_deref(),
        args.password_file.as_deref(),
        args.password_env.as_deref(),
        "certificate password",
    )?;

    let certificate = SigningCertificate::from_pkcs12_file(&args.certificate, &password)
        .with_context(|| format!("Failed to load certificate from: {}", args.certificate))?;

    // Show certificate info
    if let Some(cn) = certificate.subject_common_name() {
        eprintln!("Signing with certificate: {cn}");
    }

    let level = match args.level.to_uppercase().as_str() {
        "B" => SignatureLevel::PadesB,
        "T" => SignatureLevel::PadesT,
        "LT" => SignatureLevel::PadesLT,
        "LTA" => SignatureLevel::PadesLTA,
        _ => anyhow::bail!(
            "Invalid signature level: {}. Use: B, T, LT, or LTA",
            args.level
        ),
    };

    // Parse MDP permissions for certification signatures
    let mdp_permissions = MdpPermissions::parse_arg(&args.mdp).ok_or_else(|| {
        anyhow::anyhow!(
            "Invalid MDP permission: {}. Use: 1/no-changes, 2/form-filling, or 3/annotations",
            args.mdp
        )
    })?;

    // Build options with field name
    let field_name = args
        .visible
        .clone()
        .unwrap_or_else(|| "Signature".to_string());
    let options = SigningOptions::builder()
        .reason(args.reason)
        .location(args.location)
        .signature_level(level)
        .timestamp_url(args.timestamp_url)
        .field_name(field_name)
        .certify(args.certify)
        .mdp_permissions(mdp_permissions)
        .build();

    if args.certify {
        eprintln!(
            "Creating certification signature with MDP={}",
            mdp_permissions.as_str()
        );
    }

    let result = if let Some(position) = &args.position {
        let (page, c) = parse_page_coords(position, 4, "page:x,y,w,h")
            .with_context(|| format!("Invalid position: {position}"))?;
        let appearance = SignatureAppearance::builder()
            .page(page)
            .x(c[0])
            .y(c[1])
            .width(c[2])
            .height(c[3])
            .build();
        sign_pdf_visible(&pdf_data, &certificate, &options, &appearance)
            .context("Failed to sign PDF with visible signature")?
    } else {
        sign_pdf(&pdf_data, &certificate, &options).context("Failed to sign PDF")?
    };

    result
        .write_to_file(&args.output)
        .with_context(|| format!("Failed to write signed PDF to: {}", args.output))?;
    eprintln!("Signed PDF written to: {}", args.output);

    Ok(())
}

pub fn verify(args: &VerifyArgs) -> Result<()> {
    use printwell::crypto::TrustStore;
    use printwell::signing::verify_signatures_with_trust;

    let pdf_data = std::fs::read(&args.input)
        .with_context(|| format!("Failed to read input file: {}", args.input))?;

    // Build trust store if requested
    let trust_store = if args.use_system_trust {
        Some(TrustStore::system().context("Failed to load system trust store")?)
    } else {
        None
    };

    let signatures = verify_signatures_with_trust(&pdf_data, trust_store.as_ref())
        .context("Failed to verify signatures")?;

    if signatures.is_empty() {
        println!("No signatures found in document.");
        return Ok(());
    }

    if args.format.as_str() == "json" {
        let json = serde_json::to_string_pretty(
            &signatures
                .iter()
                .map(|s| {
                    serde_json::json!({
                        "signer_name": s.signer_name,
                        "signing_time": s.signing_time,
                        "reason": s.reason,
                        "location": s.location,
                        "is_valid": s.status.document.is_valid,
                        "covers_whole_document": s.status.document.covers_whole_document,
                        "cert_time_valid": s.status.certificate.time_valid,
                        "cert_usage_valid": s.status.certificate.usage_valid,
                        "cert_warnings": s.cert_warnings,
                        "error": s.error,
                    })
                })
                .collect::<Vec<_>>(),
        )
        .context("Failed to serialize signature info")?;
        println!("{json}");
    } else {
        println!("Found {} signature(s):\n", signatures.len());
        for (i, sig) in signatures.iter().enumerate() {
            println!("Signature #{}:", i + 1);
            println!("  Signer: {}", sig.signer_name);
            if let Some(ref time) = sig.signing_time {
                println!("  Time: {time}");
            }
            if let Some(ref reason) = sig.reason {
                println!("  Reason: {reason}");
            }
            if let Some(ref location) = sig.location {
                println!("  Location: {location}");
            }
            println!(
                "  Valid: {}",
                if sig.status.document.is_valid {
                    "Yes"
                } else {
                    "No"
                }
            );
            println!(
                "  Covers whole document: {}",
                if sig.status.document.covers_whole_document {
                    "Yes"
                } else {
                    "No"
                }
            );
            println!("  Certificate validity:");
            println!(
                "    Time valid: {}",
                if sig.status.certificate.time_valid {
                    "Yes"
                } else {
                    "No"
                }
            );
            println!(
                "    Key usage valid: {}",
                if sig.status.certificate.usage_valid {
                    "Yes"
                } else {
                    "No"
                }
            );
            if !sig.cert_warnings.is_empty() {
                println!("    Warnings:");
                for warning in &sig.cert_warnings {
                    println!("      - {warning}");
                }
            }
            if let Some(ref error) = sig.error {
                println!("  Error: {error}");
            }
            println!();
        }
    }

    Ok(())
}

pub fn list_fields(args: &ListFieldsArgs) -> Result<()> {
    use printwell::signing::list_signature_fields;

    let pdf_data = std::fs::read(&args.input)
        .with_context(|| format!("Failed to read input file: {}", args.input))?;

    let fields = list_signature_fields(&pdf_data).context("Failed to list signature fields")?;

    if fields.is_empty() {
        println!("No signature fields found in document.");
        return Ok(());
    }

    if args.format.as_str() == "json" {
        let json = serde_json::to_string_pretty(
            &fields
                .iter()
                .map(|f| {
                    serde_json::json!({
                        "name": f.name,
                        "page": f.page,
                        "x": f.x,
                        "y": f.y,
                        "width": f.width,
                        "height": f.height,
                        "is_signed": f.is_signed,
                    })
                })
                .collect::<Vec<_>>(),
        )
        .context("Failed to serialize signature fields")?;
        println!("{json}");
    } else {
        println!("Found {} signature field(s):\n", fields.len());
        for (i, field) in fields.iter().enumerate() {
            println!("Field #{}:", i + 1);
            println!("  Name: {}", field.name);
            println!("  Page: {}", field.page);
            println!("  Position: ({}, {})", field.x, field.y);
            println!("  Size: {} x {}", field.width, field.height);
            println!("  Signed: {}", if field.is_signed { "Yes" } else { "No" });
            println!();
        }
    }

    Ok(())
}