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))?;
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))?;
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
),
};
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
)
})?;
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))?;
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(())
}