easypdf-core 0.1.1

Core types, traits, enums, converters, and errors for easypdf-rust
Documentation
use super::cms::build_cms_signed_data;
use super::der::{concat, der_ctx, der_int, der_seq, der_set, der_tlv};
use super::pdf::{sign_pdf, verify_pdf_signature};
use super::*;
use crate::crypto::{CryptoError, PdfEncryption};

const OID_RSA_ENCRYPTION_DER: &[u8] = &[
    0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
];
const OID_CN_DER: &[u8] = &[0x06, 0x03, 0x55, 0x04, 0x03];

fn der_bits(value: &[u8]) -> Vec<u8> {
    let mut c = Vec::with_capacity(1 + value.len());
    c.push(0);
    c.extend(value);
    der_tlv(0x03, &c)
}
fn der_null() -> Vec<u8> {
    vec![0x05, 0x00]
}
fn der_utf8(s: &str) -> Vec<u8> {
    der_tlv(0x0C, s.as_bytes())
}
fn der_utc(s: &str) -> Vec<u8> {
    der_tlv(0x17, s.as_bytes())
}

fn generate_test_cert_and_key() -> (Vec<u8>, Vec<u8>) {
    use rand::rngs::OsRng;
    use rsa::RsaPrivateKey;
    use rsa::pkcs1::EncodeRsaPrivateKey;
    let mut rng = OsRng;
    let pk = RsaPrivateKey::new(&mut rng, 2048).expect("generate RSA key");
    let pub_key = pk.to_public_key();
    let priv_der = pk
        .to_pkcs1_der()
        .expect("encode private key")
        .as_bytes()
        .to_vec();
    let cert_der = build_self_signed_cert(&priv_der, &pub_key);
    (cert_der, priv_der)
}

fn build_self_signed_cert(priv_der: &[u8], public_key: &rsa::RsaPublicKey) -> Vec<u8> {
    use ring::rand::SystemRandom;
    use ring::signature::{RSA_PKCS1_SHA256, RsaKeyPair};
    use rsa::traits::PublicKeyParts;
    let cn_attr = der_seq(&concat(&[OID_CN_DER, &der_utf8("easypdf-test")]));
    let name = der_seq(&der_set(&cn_attr));
    let validity = der_seq(&concat(&[
        &der_utc("200101000000Z"),
        &der_utc("491231235959Z"),
    ]));
    let n_bytes = public_key.n().to_bytes_be();
    let e_bytes = public_key.e().to_bytes_be();
    let rsa_pub_key = der_seq(&concat(&[&der_int(&n_bytes), &der_int(&e_bytes)]));
    let spki = der_seq(&concat(&[
        &der_seq(&concat(&[OID_RSA_ENCRYPTION_DER, &der_null()])),
        &der_bits(&rsa_pub_key),
    ]));
    let tbs = der_seq(&concat(&[
        &der_ctx(0, true, &der_int(&[2u8])),
        &der_int(&[0x01u8]),
        &der_seq(OID_SHA256_WITH_RSA_DER),
        &name,
        &validity,
        &name,
        &spki,
    ]));
    let kp = RsaKeyPair::from_der(priv_der).expect("ring key parse");
    let rng = SystemRandom::new();
    let mut sig = vec![0u8; kp.public().modulus_len()];
    kp.sign(&RSA_PKCS1_SHA256, &rng, &tbs, &mut sig)
        .expect("sign cert TBS");
    der_seq(&concat(&[
        &tbs,
        &der_seq(OID_SHA256_WITH_RSA_DER),
        &der_bits(&sig),
    ]))
}

#[test]
fn signed_data_detached_builds_and_parses() {
    use x509_parser::prelude::{FromDer, X509Certificate};
    let (cert_der, _) = generate_test_cert_and_key();
    let dummy_sig = vec![0xAAu8; 256];
    let (_, cert) = X509Certificate::from_der(&cert_der).unwrap();
    let issuer_der = cert.issuer().as_raw().to_vec();
    let serial = cert.raw_serial();
    let cms = build_cms_signed_data(&cert_der, &dummy_sig, &issuer_der, serial);
    assert!(!cms.is_empty());
    let parsed = super::cms::parse_cms_signed_data(&cms).expect("parse CMS");
    assert_eq!(parsed.certificate_der, cert_der);
    assert_eq!(parsed.signature, dummy_sig);
}

#[test]
fn sign_and_verify_roundtrip_valid() {
    let (cert_der, priv_der) = generate_test_cert_and_key();
    let signer = PdfSigner::new(cert_der, priv_der)
        .with_reason("Approval")
        .with_location("Berlin")
        .with_contact_info("test@example.com");
    let signed = sign_pdf(b"%PDF-1.4 test content", &signer).unwrap();
    let info = verify_pdf_signature(&signed).unwrap();
    assert!(info.is_valid);
    assert_eq!(info.reason.as_deref(), Some("Approval"));
    assert_eq!(info.location.as_deref(), Some("Berlin"));
    assert_eq!(info.signer_name.as_deref(), Some("easypdf-test"));
}

#[test]
fn verify_tampered_pdf_invalid() {
    let (cert_der, priv_der) = generate_test_cert_and_key();
    let mut signed = sign_pdf(b"%PDF-1.4 test", &PdfSigner::new(cert_der, priv_der)).unwrap();
    signed[10] = b'X';
    let info = verify_pdf_signature(&signed).unwrap();
    assert!(!info.is_valid);
}

#[test]
fn byte_range_covers_content() {
    let pdf = b"%PDF-1.4 byte range test";
    let (cert_der, priv_der) = generate_test_cert_and_key();
    let signed = sign_pdf(pdf, &PdfSigner::new(cert_der, priv_der)).unwrap();
    let br_pos = super::pdf::find_bytes(&signed, b"/ByteRange [").unwrap();
    let br_vals_start = br_pos + 12;
    let br_close = signed[br_vals_start..]
        .iter()
        .position(|&b| b == b']')
        .unwrap();
    let parts: Vec<usize> = std::str::from_utf8(&signed[br_vals_start..br_vals_start + br_close])
        .unwrap()
        .split_ascii_whitespace()
        .map(|s| s.parse().unwrap())
        .collect();
    assert_eq!(parts.len(), 4);
    assert_eq!(parts[0], 0);
    assert_eq!(parts[1], pdf.len());
    assert_eq!(parts[2] + parts[3], signed.len());
}

#[test]
fn x509_metadata_extracted() {
    let (cert_der, priv_der) = generate_test_cert_and_key();
    let signed = sign_pdf(
        b"%PDF-1.4 x509",
        &PdfSigner::new(cert_der, priv_der)
            .with_reason("Review")
            .with_location("NYC"),
    )
    .unwrap();
    let info = verify_pdf_signature(&signed).unwrap();
    assert!(info.is_valid);
    assert_eq!(info.signer_name.as_deref(), Some("easypdf-test"));
    assert_eq!(info.reason.as_deref(), Some("Review"));
    assert_eq!(info.location.as_deref(), Some("NYC"));
}

#[test]
fn signature_dict_standard_fields() {
    let (cert_der, priv_der) = generate_test_cert_and_key();
    let signed = sign_pdf(
        b"%PDF-1.4 dict",
        &PdfSigner::new(cert_der, priv_der)
            .with_reason("Approval")
            .with_location("Berlin")
            .with_contact_info("signer@example.com"),
    )
    .unwrap();
    let s = String::from_utf8_lossy(&signed);
    assert!(s.contains("/Type /Sig"));
    assert!(s.contains("/ByteRange"));
    assert!(s.contains("/SubFilter /adbe.pkcs7.detached"));
    assert!(s.contains("/Reason (Approval)"));
    assert!(s.contains("/Location (Berlin)"));
}

#[test]
fn verify_unsigned_pdf_fails() {
    assert!(verify_pdf_signature(b"%PDF-1.4 no sig").is_err());
}

#[test]
fn sign_empty_content() {
    let (c, k) = generate_test_cert_and_key();
    let signed = sign_pdf(b"", &PdfSigner::new(c, k)).unwrap();
    assert!(!signed.is_empty());
    assert!(verify_pdf_signature(&signed).unwrap().is_valid);
}

#[test]
fn sign_large_content() {
    let (c, k) = generate_test_cert_and_key();
    let signed = sign_pdf(&vec![0x42u8; 100_000], &PdfSigner::new(c, k)).unwrap();
    assert!(signed.len() > 100_000);
}

#[test]
fn sign_with_all_optional_fields() {
    let (c, k) = generate_test_cert_and_key();
    let signed = sign_pdf(
        b"%PDF-1.4",
        &PdfSigner::new(c, k)
            .with_reason("R")
            .with_location("L")
            .with_contact_info("C")
            .with_timestamp_url("http://tsa"),
    )
    .unwrap();
    let info = verify_pdf_signature(&signed).unwrap();
    assert!(info.is_valid);
}

#[test]
fn roundtrip_empty_reason_and_location() {
    let (c, k) = generate_test_cert_and_key();
    let info =
        verify_pdf_signature(&sign_pdf(b"%PDF-1.4", &PdfSigner::new(c, k)).unwrap()).unwrap();
    assert!(info.is_valid);
    assert!(info.reason.is_none());
}

#[test]
fn multiple_signatures_last_one_verifies() {
    let (c, k) = generate_test_cert_and_key();
    let signer = PdfSigner::new(c, k);
    let s1 = sign_pdf(b"%PDF-1.4 multi", &signer).unwrap();
    let s2 = sign_pdf(&s1, &signer).unwrap();
    assert!(verify_pdf_signature(&s2).unwrap().is_valid);
}

#[test]
fn crypto_error_display_variants() {
    assert!(format!("{}", CryptoError::Aes("t".into())).contains("AES"));
    assert!(format!("{}", CryptoError::Rsa("t".into())).contains("RSA"));
    assert!(format!("{}", CryptoError::Signature("t".into())).contains("signature"));
    assert!(format!("{}", CryptoError::Verification("t".into())).contains("verification"));
    assert!(
        format!("{}", CryptoError::InvalidEncryptedPdf("t".into()))
            .contains("invalid encrypted PDF")
    );
    assert!(
        format!("{}", CryptoError::InvalidSignedPdf("t".into())).contains("invalid signed PDF")
    );
    assert!(format!("{}", CryptoError::InvalidKey("t".into())).contains("invalid key"));
    assert!(
        format!("{}", CryptoError::InvalidCertificate("t".into())).contains("invalid certificate")
    );
}

#[test]
fn pdf_encryption_new() {
    let enc = PdfEncryption::new("user", "owner");
    assert_eq!(enc.user_password, "user");
    assert_eq!(enc.owner_password, "owner");
}

#[test]
fn pdf_signer_builder_chain() {
    let s = PdfSigner::new(vec![1], vec![2])
        .with_reason("R")
        .with_location("L")
        .with_contact_info("C")
        .with_timestamp_url("http://tsa");
    assert_eq!(s.reason.as_deref(), Some("R"));
    assert_eq!(s.location.as_deref(), Some("L"));
    assert_eq!(s.contact_info.as_deref(), Some("C"));
    assert_eq!(s.timestamp_url.as_deref(), Some("http://tsa"));
}