Sign, verify, and timestamp PDFs with Rust

A practical guide for Rust developers to add and validate digital signatures, extract signature details, and apply trusted timestamps to PDF documents.

Add PDFluent to your project

Add the pdfluent crate to Cargo.toml.

toml
[dependencies]
pdfluent = "1.0.0-beta.18"

Add a digital signature to a PDF in Rust

Sign a PDF with a PKCS#12 certificate. PDFluent writes a conforming ISO 32000 signature that Adobe Acrobat, Preview, and other viewers can verify.

rust
use pdfluent::{PdfDocument, Pkcs12Signer, SignOptions};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("contract.pdf")?;
    let signer = Pkcs12Signer::from_pfx_file("cert.p12", "pfx-password")?;
    doc.sign(&signer, SignOptions::new().reason("Approved"))?;
    doc.save("signed.pdf")?;
    Ok(())
}
  1. Load your PKCS#12 certificate

    Read the .p12 or .pfx certificate file and create a PdfSigner. The certificate must include the private key.

    rust
    use pdfluent::Pkcs12Signer;
    
    let signer = Pkcs12Signer::from_pfx_file("my_cert.p12", "your_p12_password")?;
  2. Configure signature metadata

    Set the reason, location, and contact info. These appear in the signature panel in PDF viewers.

    rust
    use pdfluent::SignOptions;
    
    let opts = SignOptions::new()
        .reason("I approve the content of this document")
        .location("Amsterdam, NL")
        .contact_info("[email protected]")
        .field_name("Signature1");
  3. Position the visible signature appearance

    Add a visible signature box on a specific page and position. Skip this step for invisible signatures.

    rust
    use pdfluent::SignOptions;
    
    // Place a visible signature rectangle on page 0: [x1, y1, x2, y2] in points
    let opts = SignOptions::new()
        .reason("Approved")
        .visible_rect(0, [350.0, 50.0, 550.0, 110.0]);
  4. Sign the document and save

    Call sign() then save(). The output file contains the cryptographic signature bytes embedded in the PDF structure.

    rust
    doc.sign(&signer, opts)?;
    doc.save("contract_signed.pdf")?;
    
    println!("Signed.");
  • PDF signing is incremental: the original bytes are not modified, the signature is appended. This preserves prior signatures.
  • For LTV (Long-Term Validation), call opts.embed_ocsp(true) to include the OCSP response in the signature.
  • Self-signed certificates will produce a warning in Acrobat. Use a certificate from a trusted CA for production use.
  • The signature field name must be unique in the document. Signing a field that already exists replaces the signature.

Verify a PDF digital signature in Rust

Check that a PDF signature is cryptographically valid and that the document has not been modified since it was signed.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("signed.pdf")?;
    let report = doc.verify_signatures()?;
    println!("signed: {}, all valid: {}", report.is_signed(), report.all_valid());
    Ok(())
}
  1. Open the signed PDF

    Load the document. A read-only borrow is sufficient for verification.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("signed_invoice.pdf")?;
  2. List signatures in the document

    Call doc.signatures() to get all signature fields. Each item includes the field name, signing time, and the raw certificate chain.

    rust
    let signatures = doc.signatures()?;
    println!("Found {} signature(s)", signatures.len());
    
    for sig in &signatures {
        println!("Field: {}", sig.field_name);
        println!("Signer: {}", sig.signer_name);
        println!("Timestamp: {:?}", sig.timestamp);
    }
  3. Verify signature integrity

    SignatureVerifier checks that the signed byte range matches the current file contents. If any byte outside the signature field has changed, integrity_valid is false.

    rust
    let report = doc.verify_signatures()?;
    
    if report.all_valid() {
        println!("OK - all signatures valid, document not modified");
    } else {
        println!("FAIL - a signature is invalid or the document was modified");
    }
  4. Verify the certificate chain

    Check that the signing certificate chains to a trusted root. Supply your own trust store or use the system store.

    rust
    let report = doc.verify_signatures()?;
    println!("Signed: {}", report.is_signed());
    
    for v in report.validations() {
        println!("Field {}: {:?}", v.info.field_name, v.status);
    }
  • integrity_valid checks cryptographic hash only. certificate_trusted checks the CA chain separately.
  • PDF signatures cover a specific byte range. Content added after signing (incremental updates) falls outside that range.
  • For LTV-enabled signatures, call result.ltv_valid() to check embedded OCSP and CRL data.
  • A self-signed certificate will produce certificate_trusted = false unless you add it to a custom TrustStore.

Read digital signature details from a PDF in Rust

Inspect the signer certificate, signing time, and signature coverage for each digital signature in a PDF.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("signed.pdf")?;
    for sig in doc.signatures()? {
        println!("{} by {}", sig.field_name, sig.signer_name);
    }
    Ok(())
}
  1. Open the signed PDF

    doc.signatures() returns an iterator over all digital signature fields in the AcroForm.

    rust
    let doc = PdfDocument::open("signed.pdf")?;
  2. List all signatures

    Each Signature object provides access to the signer name from the certificate, signing time, and field name.

    rust
    for sig in doc.signatures() {
        println!("Field: {}", sig.field_name());
        println!("Signer: {}", sig.signer_name().unwrap_or("(unknown)"));
    }
  3. Read certificate details

    sig.certificate() returns the end-entity certificate. You can inspect the subject, issuer, serial number, and validity period.

    rust
    let cert = sig.certificate()?;
    println!("Subject:    {}", cert.subject());
    println!("Issuer:     {}", cert.issuer());
    println!("Serial:     {}", cert.serial_number_hex());
    println!("Valid from: {:?}", cert.not_before());
    println!("Valid to:   {:?}", cert.not_after());
  4. Check signing time and byte range

    The signing time may come from the certificate or from an embedded timestamp token. covers_whole_document checks whether the byte ranges cover the entire file.

    rust
    println!("Signing time: {:?}", sig.signing_time());
    println!("Has timestamp token: {}", sig.has_timestamp_token());
    println!("Covers whole document: {}", sig.covers_whole_document());
    
    let (ranges_bytes, total_bytes) = sig.byte_range_coverage(&doc)?;
    println!("Covered {}/{} bytes", ranges_bytes, total_bytes);
  5. Check if the document has been modified after signing

    If the byte ranges do not cover the entire file, content was appended after signing. This does not mean the signature is invalid, but it may indicate incremental updates.

    rust
    if !sig.covers_whole_document() {
        println!("Warning: document was modified after signing.");
    }
  • Reading signature info does not verify the cryptographic integrity. Call sig.verify() to perform a cryptographic check.
  • A PDF may contain multiple signatures, each covering different byte ranges. This is normal in multi-party signing workflows.
  • The signing time in the signature dictionary is set by the signer and can be spoofed. Use has_timestamp_token() and embedded TSA tokens for trusted time.
  • Certificate chain validation requires a trust store. Pass a custom trust store with sig.verify_with_trust_store(&store).

Add a PAdES-LTV digital signature to a PDF in Rust

Embed OCSP responses, CRLs, and certificate chains so your PDF signatures remain verifiable years after the signing certificate expires — required for eIDAS long-term validity.

rust
use pdfluent::{PdfDocument, Pkcs12Signer, SignOptions, PadesProfile};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("contract.pdf")?;
    let signer = Pkcs12Signer::from_pfx_file("cert.p12", "pfx-password")?;
    doc.sign(
        &signer,
        SignOptions::new().reason("Approved").profile(PadesProfile::LongTerm),
    )?;
    doc.save("signed-ltv.pdf")?;
    Ok(())
}
  1. Load your signing certificate

    Read the PKCS#12 (.p12 or .pfx) file containing your signing certificate and private key. For production use, the certificate must be issued by a trusted CA — self-signed certificates will not satisfy eIDAS requirements.

    rust
    use pdfluent::{PdfDocument, Pkcs12Signer, SignOptions, PadesProfile};
    
    let signer = Pkcs12Signer::from_pfx_file("signing_cert.p12", "cert_password")?;
  2. Choose the right PAdES level

    PAdES defines four levels of increasing archival strength. B-B is basic; B-T adds a timestamp; B-LT embeds OCSP responses and CRL data for long-term validation; B-LTA adds a second archival timestamp over all that data. For eIDAS compliance on documents that must remain valid for years, use B-LT at minimum.

    rust
    use pdfluent::PadesProfile;
    
    // PadesProfile::BasicSignature  - basic, short-term
    // PadesProfile::Timestamped     - adds a TSA timestamp
    // PadesProfile::LongTerm        - embeds revocation data (LTV) [default]
    // PadesProfile::LongTermArchive - archival timestamp over LT data
    
    let profile = PadesProfile::LongTerm;
  3. Sign the document — PDFluent embeds validation data automatically

    At PAdES-B-LT, PDFluent contacts the OCSP responder and fetches current CRLs for every certificate in the chain, then embeds them in the PDF's Document Security Store (DSS). This happens automatically — you do not need to fetch OCSP responses manually.

    rust
    let opts = SignOptions::new()
        .reason("Contract approval")
        .location("Amsterdam, NL")
        .profile(PadesProfile::LongTerm);
    
    let mut doc = PdfDocument::open("contract.pdf")?;
    doc.sign(&signer, opts)?;
    doc.save("contract_signed_ltv.pdf")?;
  4. Optionally upgrade to PAdES-B-LTA for maximum archival longevity

    PAdES-B-LTA adds a document timestamp over the entire DSS structure, including the embedded OCSP and CRL data. This seals the validation material against modification and extends the effective archival period as long as the timestamp algorithm remains trusted — typically several decades.

    rust
    let opts = SignOptions::new()
        .reason("Contract approval")
        .profile(PadesProfile::LongTermArchive);
    
    doc.sign(&signer, opts)?;
    doc.save("contract_signed_lta.pdf")?;
  • PAdES-B-LT signing requires network access at signing time to fetch OCSP responses and CRLs. Ensure your signing environment can reach the CA's OCSP endpoint.
  • The Document Security Store (DSS) is written as part of the signed update. It does not invalidate or change the signature bytes.
  • For B-LTA, a Time Stamp Authority (TSA) URL is required. Use a publicly trusted TSA or your organization's internal TSA.
  • Redaction is irreversible — always test PAdES signing on a copy of the document before applying to originals.
  • EU eIDAS requires a Qualified Certificate for QES (Qualified Electronic Signature). PAdES-B-LT/LTA defines the format; the certificate class determines the legal weight.

Add a trusted timestamp to a PDF in Rust

Embed an RFC 3161 timestamp token from a Time Stamping Authority into an existing PDF signature.

rust
use pdfluent::{PdfDocument, timestamp::{TsaConfig, TimestampOptions}};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("signed.pdf")?;

    let tsa = TsaConfig::new("http://timestamp.digicert.com");
    let opts = TimestampOptions::new(tsa);

    // Add a document-level timestamp signature field
    doc.add_timestamp(&opts)?;
    doc.save("timestamped.pdf")?;
    Ok(())
}
  1. Open the PDF to timestamp

    The document may already be signed by a signer, or you may add a standalone timestamp signature field.

    rust
    let mut doc = PdfDocument::open("signed.pdf")?;
  2. Configure the TSA endpoint

    Provide the URL of a trusted RFC 3161 Time Stamping Authority. Many CAs offer free TSA endpoints.

    rust
    use pdfluent::timestamp::TsaConfig;
    
    let tsa = TsaConfig::new("http://timestamp.digicert.com")
        .hash_algorithm(pdfluent::digest::HashAlgorithm::Sha256);
  3. Build timestamp options

    TimestampOptions wraps the TsaConfig and specifies the field name and policy OID if required by the TSA.

    rust
    use pdfluent::timestamp::TimestampOptions;
    
    let opts = TimestampOptions::new(tsa)
        .field_name("DocTimestamp")
        .policy_oid(None); // None = TSA default policy
  4. Request the timestamp and embed it

    PDFluent computes the document digest, sends a TSQ to the TSA, receives a TSR, and embeds the RFC 3161 token in a new signature field.

    rust
    doc.add_timestamp(&opts)?;
  5. Save and verify

    Save the file and confirm the timestamp token is present.

    rust
    doc.save("timestamped.pdf")?;
    
    // Verify the timestamp
    let doc2 = PdfDocument::open("timestamped.pdf")?;
    for sig in doc2.signatures() {
        if sig.has_timestamp_token() {
            let ts = sig.timestamp_token()?;
            println!("Timestamp time: {:?}", ts.gen_time());
            println!("TSA: {}", ts.tsa_name().unwrap_or("unknown"));
        }
    }
  • A document-level timestamp (LTV timestamp) provides proof of existence at a point in time even after the signer certificate expires.
  • Some TSA services require HTTP Basic Auth or an API key. Pass credentials with TsaConfig::with_credentials(user, pass).
  • The hash algorithm in the TSQ must be accepted by the TSA. SHA-256 is accepted by all modern TSAs. SHA-1 is deprecated.
  • For long-term validation (LTV), also embed OCSP responses or CRLs for all certificates in the chain with doc.add_ltv_info().