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 ks = p12_keystore::KeyStore::from_pkcs12(data, password)
.map_err(|e| Error::InvalidPdf(format!("PKCS#12 parse error: {e}")))?;
let (_, pkc) = ks
.private_key_chain()
.ok_or_else(|| Error::InvalidPdf("PKCS#12 contains no private key".into()))?;
let private_key = pkc.key().to_vec();
let mut cert_iter = pkc.chain().iter();
let certificate = cert_iter
.next()
.ok_or_else(|| Error::InvalidPdf("PKCS#12 contains no certificate".into()))?
.as_der()
.to_vec();
let chain: Vec<Vec<u8>> = cert_iter.map(|c| c.as_der().to_vec()).collect();
Ok(Self {
certificate,
private_key,
chain,
})
}
#[cfg(feature = "signatures")]
pub fn from_pem(cert_pem: &str, key_pem: &str) -> Result<Self> {
use x509_parser::pem::parse_x509_pem;
use x509_parser::prelude::*;
let (_, cert_block) = parse_x509_pem(cert_pem.as_bytes())
.map_err(|e| Error::InvalidPdf(format!("invalid certificate PEM: {e}")))?;
let cert_der = cert_block.contents;
let (_, _) = X509Certificate::from_der(&cert_der).map_err(|e| {
Error::InvalidPdf(format!("certificate PEM contains invalid X.509 DER: {e}"))
})?;
let (_, key_block) = parse_x509_pem(key_pem.as_bytes())
.map_err(|e| Error::InvalidPdf(format!("invalid private key PEM: {e}")))?;
let private_key = key_block.contents;
if private_key.is_empty() {
return Err(Error::InvalidPdf("private key PEM decoded to empty bytes".into()));
}
Ok(Self {
certificate: cert_der,
private_key,
chain: Vec::new(),
})
}
#[cfg(feature = "signatures")]
pub fn from_der(cert_der: Vec<u8>) -> Result<Self> {
use x509_parser::prelude::*;
let (_, _parsed) = X509Certificate::from_der(&cert_der)
.map_err(|e| Error::InvalidPdf(format!("invalid X.509 DER: {e}")))?;
Ok(Self {
certificate: cert_der,
private_key: Vec::new(),
chain: Vec::new(),
})
}
#[cfg(feature = "signatures")]
pub fn subject(&self) -> Result<String> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(&self.certificate)
.map_err(|e| Error::InvalidPdf(format!("invalid X.509 DER: {e}")))?;
Ok(cert.subject().to_string())
}
#[cfg(feature = "signatures")]
pub fn issuer(&self) -> Result<String> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(&self.certificate)
.map_err(|e| Error::InvalidPdf(format!("invalid X.509 DER: {e}")))?;
Ok(cert.issuer().to_string())
}
#[cfg(feature = "signatures")]
pub fn serial(&self) -> Result<String> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(&self.certificate)
.map_err(|e| Error::InvalidPdf(format!("invalid X.509 DER: {e}")))?;
Ok(cert.serial.to_str_radix(16))
}
#[cfg(feature = "signatures")]
pub fn validity(&self) -> Result<(i64, i64)> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(&self.certificate)
.map_err(|e| Error::InvalidPdf(format!("invalid X.509 DER: {e}")))?;
let nb = cert.validity().not_before.timestamp();
let na = cert.validity().not_after.timestamp();
Ok((nb, na))
}
#[cfg(feature = "signatures")]
pub fn is_valid(&self) -> Result<bool> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(&self.certificate)
.map_err(|e| Error::InvalidPdf(format!("invalid X.509 DER: {e}")))?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let nb = cert.validity().not_before.timestamp();
let na = cert.validity().not_after.timestamp();
Ok(now >= nb && now <= na)
}
}
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>,
pub contents: Option<Vec<u8>>,
}
impl SignatureInfo {
pub fn contents(&self) -> Option<&[u8]> {
self.contents.as_deref()
}
pub fn byte_range(&self) -> &[i64] {
&self.byte_range
}
}
#[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"));
}
#[test]
#[cfg(feature = "signatures")]
fn test_from_pem_loads_cert_and_key() {
let cert_pem = std::fs::read_to_string("tests/fixtures/test_signing_cert.pem")
.expect("test fixture must exist");
let key_pem = std::fs::read_to_string("tests/fixtures/test_signing_key.pem")
.expect("test fixture must exist");
let creds =
SigningCredentials::from_pem(&cert_pem, &key_pem).expect("from_pem should succeed");
assert!(!creds.certificate.is_empty(), "certificate must be non-empty");
assert!(!creds.private_key.is_empty(), "private key must be non-empty");
let subj = creds.subject().expect("subject must parse");
assert!(subj.contains("pdfoxide-test"), "subject must include CN: got {subj}");
}
#[test]
#[cfg(feature = "signatures")]
fn test_from_pem_rejects_invalid_cert() {
let result = SigningCredentials::from_pem("not a pem at all", "also bad");
assert!(result.is_err(), "should reject invalid PEM cert");
}
#[test]
#[cfg(feature = "signatures")]
fn test_from_pkcs12_loads_cert_and_key() {
let data =
std::fs::read("tests/fixtures/test_signing.p12").expect("test fixture must exist");
let creds = SigningCredentials::from_pkcs12(&data, "testpass")
.expect("from_pkcs12 should succeed with correct password");
assert!(!creds.certificate.is_empty(), "certificate must be non-empty");
assert!(!creds.private_key.is_empty(), "private key must be non-empty");
let subj = creds.subject().expect("subject must parse");
assert!(subj.contains("pdfoxide-test"), "subject must include CN: got {subj}");
}
#[test]
#[cfg(feature = "signatures")]
fn test_from_pkcs12_rejects_wrong_password() {
let data =
std::fs::read("tests/fixtures/test_signing.p12").expect("test fixture must exist");
let result = SigningCredentials::from_pkcs12(&data, "wrongpassword");
match result {
Err(_) => { },
Ok(c) => {
assert!(
c.subject().is_err() || c.certificate.is_empty() || c.private_key.is_empty(),
"wrong-password PKCS#12 must not yield valid usable credentials"
);
},
}
}
#[test]
#[cfg(feature = "signatures")]
fn test_from_pkcs12_rejects_garbage() {
let result = SigningCredentials::from_pkcs12(b"not pkcs12 data", "password");
assert!(result.is_err(), "should reject non-PKCS#12 data");
}
}