use crate::error::{Error, Result};
use crate::geometry::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DigestAlgorithm {
Sha1,
#[default]
Sha256,
Sha384,
Sha512,
}
impl DigestAlgorithm {
pub fn oid(&self) -> &'static [u8] {
match self {
DigestAlgorithm::Sha1 => &[0x2B, 0x0E, 0x03, 0x02, 0x1A], DigestAlgorithm::Sha256 => &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01], DigestAlgorithm::Sha384 => &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02], DigestAlgorithm::Sha512 => &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03], }
}
pub fn name(&self) -> &'static str {
match self {
DigestAlgorithm::Sha1 => "SHA-1",
DigestAlgorithm::Sha256 => "SHA-256",
DigestAlgorithm::Sha384 => "SHA-384",
DigestAlgorithm::Sha512 => "SHA-512",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SignatureSubFilter {
#[default]
Pkcs7Detached,
Pkcs7Sha1,
CadesDetached,
Rfc3161,
}
impl SignatureSubFilter {
pub fn as_pdf_name(&self) -> &'static str {
match self {
SignatureSubFilter::Pkcs7Detached => "adbe.pkcs7.detached",
SignatureSubFilter::Pkcs7Sha1 => "adbe.pkcs7.sha1",
SignatureSubFilter::CadesDetached => "ETSI.CAdES.detached",
SignatureSubFilter::Rfc3161 => "ETSI.RFC3161",
}
}
pub fn from_pdf_name(name: &str) -> Option<Self> {
match name {
"adbe.pkcs7.detached" => Some(SignatureSubFilter::Pkcs7Detached),
"adbe.pkcs7.sha1" => Some(SignatureSubFilter::Pkcs7Sha1),
"ETSI.CAdES.detached" => Some(SignatureSubFilter::CadesDetached),
"ETSI.RFC3161" => Some(SignatureSubFilter::Rfc3161),
_ => None,
}
}
}
#[derive(Clone)]
pub struct SigningCredentials {
pub certificate: Vec<u8>,
pub private_key: Vec<u8>,
pub chain: Vec<Vec<u8>>,
}
impl SigningCredentials {
pub fn new(certificate: Vec<u8>, private_key: Vec<u8>) -> Self {
Self {
certificate,
private_key,
chain: Vec::new(),
}
}
pub fn with_chain(mut self, chain: Vec<Vec<u8>>) -> Self {
self.chain = chain;
self
}
#[cfg(feature = "signatures")]
pub fn from_pkcs12(data: &[u8], password: &str) -> Result<Self> {
let _ = (data, password);
Err(Error::InvalidPdf("PKCS#12 loading not yet implemented".to_string()))
}
#[cfg(feature = "signatures")]
pub fn from_pem(cert_pem: &str, key_pem: &str) -> Result<Self> {
let _ = (cert_pem, key_pem);
Err(Error::InvalidPdf("PEM loading not yet implemented".to_string()))
}
}
impl std::fmt::Debug for SigningCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SigningCredentials")
.field("certificate", &format!("{} bytes", self.certificate.len()))
.field("private_key", &"[REDACTED]")
.field("chain", &format!("{} certificates", self.chain.len()))
.finish()
}
}
#[derive(Debug, Clone)]
pub struct SignOptions {
pub digest_algorithm: DigestAlgorithm,
pub sub_filter: SignatureSubFilter,
pub reason: Option<String>,
pub location: Option<String>,
pub contact_info: Option<String>,
pub name: Option<String>,
pub appearance: Option<SignatureAppearance>,
pub embed_timestamp: bool,
pub timestamp_url: Option<String>,
pub estimated_size: usize,
}
impl Default for SignOptions {
fn default() -> Self {
Self {
digest_algorithm: DigestAlgorithm::Sha256,
sub_filter: SignatureSubFilter::Pkcs7Detached,
reason: None,
location: None,
contact_info: None,
name: None,
appearance: None,
embed_timestamp: false,
timestamp_url: None,
estimated_size: 8192, }
}
}
impl SignOptions {
pub fn with_appearance(mut self, appearance: SignatureAppearance) -> Self {
self.appearance = Some(appearance);
self
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
pub fn with_timestamp(mut self, tsa_url: impl Into<String>) -> Self {
self.embed_timestamp = true;
self.timestamp_url = Some(tsa_url.into());
self
}
}
#[derive(Debug, Clone)]
pub struct SignatureAppearance {
pub page: usize,
pub rect: Rect,
pub show_name: bool,
pub show_date: bool,
pub show_reason: bool,
pub show_location: bool,
pub background_image: Option<Vec<u8>>,
pub font_size: f32,
}
impl Default for SignatureAppearance {
fn default() -> Self {
Self {
page: 0,
rect: Rect::new(72.0, 72.0, 200.0, 50.0),
show_name: true,
show_date: true,
show_reason: true,
show_location: true,
background_image: None,
font_size: 10.0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SignatureInfo {
pub signer_name: Option<String>,
pub signing_time: Option<String>,
pub reason: Option<String>,
pub location: Option<String>,
pub contact_info: Option<String>,
pub sub_filter: Option<SignatureSubFilter>,
pub covers_whole_document: bool,
pub byte_range: Vec<i64>,
pub certificate_cn: Option<String>,
pub certificate_issuer: Option<String>,
pub valid_from: Option<String>,
pub valid_to: Option<String>,
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub status: VerificationStatus,
pub signature_info: SignatureInfo,
pub messages: Vec<String>,
pub document_modified: bool,
pub certificate_trusted: bool,
pub chain_valid: bool,
pub certificate_expired: bool,
pub timestamp_valid: Option<bool>,
}
impl Default for VerificationResult {
fn default() -> Self {
Self {
status: VerificationStatus::Unknown,
signature_info: SignatureInfo::default(),
messages: Vec::new(),
document_modified: false,
certificate_trusted: false,
chain_valid: false,
certificate_expired: false,
timestamp_valid: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerificationStatus {
Valid,
Invalid,
Unknown,
ValidWithWarnings,
}
impl VerificationStatus {
pub fn is_valid(&self) -> bool {
matches!(self, VerificationStatus::Valid)
}
pub fn is_ok(&self) -> bool {
matches!(self, VerificationStatus::Valid | VerificationStatus::ValidWithWarnings)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_digest_algorithm_names() {
assert_eq!(DigestAlgorithm::Sha256.name(), "SHA-256");
assert_eq!(DigestAlgorithm::Sha1.name(), "SHA-1");
}
#[test]
fn test_sub_filter_names() {
assert_eq!(SignatureSubFilter::Pkcs7Detached.as_pdf_name(), "adbe.pkcs7.detached");
assert_eq!(
SignatureSubFilter::from_pdf_name("adbe.pkcs7.detached"),
Some(SignatureSubFilter::Pkcs7Detached)
);
}
#[test]
fn test_sign_options_default() {
let opts = SignOptions::default();
assert_eq!(opts.digest_algorithm, DigestAlgorithm::Sha256);
assert_eq!(opts.sub_filter, SignatureSubFilter::Pkcs7Detached);
assert!(!opts.embed_timestamp);
}
#[test]
fn test_sign_options_builder() {
let opts = SignOptions::default()
.with_reason("Test signing")
.with_location("Test City");
assert_eq!(opts.reason, Some("Test signing".to_string()));
assert_eq!(opts.location, Some("Test City".to_string()));
}
#[test]
fn test_verification_status() {
assert!(VerificationStatus::Valid.is_valid());
assert!(!VerificationStatus::Invalid.is_valid());
assert!(VerificationStatus::ValidWithWarnings.is_ok());
assert!(!VerificationStatus::Unknown.is_valid());
}
#[test]
fn test_signing_credentials_debug() {
let creds = SigningCredentials::new(vec![1, 2, 3], vec![4, 5, 6]);
let debug = format!("{:?}", creds);
assert!(debug.contains("[REDACTED]"));
assert!(debug.contains("3 bytes"));
}
}