A practical guide for Rust developers to add and validate digital signatures, extract signature details, and apply trusted timestamps to PDF documents.
Add the pdfluent crate to Cargo.toml.
[dependencies]
pdfluent = "1.0.0-beta.18"Sign a PDF with a PKCS#12 certificate. PDFluent writes a conforming ISO 32000 signature that Adobe Acrobat, Preview, and other viewers can verify.
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(())
}Read the .p12 or .pfx certificate file and create a PdfSigner. The certificate must include the private key.
use pdfluent::Pkcs12Signer;
let signer = Pkcs12Signer::from_pfx_file("my_cert.p12", "your_p12_password")?;Set the reason, location, and contact info. These appear in the signature panel in PDF viewers.
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");Add a visible signature box on a specific page and position. Skip this step for invisible signatures.
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]);Call sign() then save(). The output file contains the cryptographic signature bytes embedded in the PDF structure.
doc.sign(&signer, opts)?;
doc.save("contract_signed.pdf")?;
println!("Signed.");Check that a PDF signature is cryptographically valid and that the document has not been modified since it was signed.
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(())
}Load the document. A read-only borrow is sufficient for verification.
use pdfluent::PdfDocument;
let doc = PdfDocument::open("signed_invoice.pdf")?;Call doc.signatures() to get all signature fields. Each item includes the field name, signing time, and the raw certificate chain.
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);
}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.
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");
}Check that the signing certificate chains to a trusted root. Supply your own trust store or use the system store.
let report = doc.verify_signatures()?;
println!("Signed: {}", report.is_signed());
for v in report.validations() {
println!("Field {}: {:?}", v.info.field_name, v.status);
}Inspect the signer certificate, signing time, and signature coverage for each digital signature in a PDF.
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(())
}doc.signatures() returns an iterator over all digital signature fields in the AcroForm.
let doc = PdfDocument::open("signed.pdf")?;Each Signature object provides access to the signer name from the certificate, signing time, and field name.
for sig in doc.signatures() {
println!("Field: {}", sig.field_name());
println!("Signer: {}", sig.signer_name().unwrap_or("(unknown)"));
}sig.certificate() returns the end-entity certificate. You can inspect the subject, issuer, serial number, and validity period.
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());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.
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);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.
if !sig.covers_whole_document() {
println!("Warning: document was modified after signing.");
}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.
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(())
}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.
use pdfluent::{PdfDocument, Pkcs12Signer, SignOptions, PadesProfile};
let signer = Pkcs12Signer::from_pfx_file("signing_cert.p12", "cert_password")?;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.
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;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.
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")?;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.
let opts = SignOptions::new()
.reason("Contract approval")
.profile(PadesProfile::LongTermArchive);
doc.sign(&signer, opts)?;
doc.save("contract_signed_lta.pdf")?;Embed an RFC 3161 timestamp token from a Time Stamping Authority into an existing PDF signature.
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(())
}The document may already be signed by a signer, or you may add a standalone timestamp signature field.
let mut doc = PdfDocument::open("signed.pdf")?;Provide the URL of a trusted RFC 3161 Time Stamping Authority. Many CAs offer free TSA endpoints.
use pdfluent::timestamp::TsaConfig;
let tsa = TsaConfig::new("http://timestamp.digicert.com")
.hash_algorithm(pdfluent::digest::HashAlgorithm::Sha256);TimestampOptions wraps the TsaConfig and specifies the field name and policy OID if required by the TSA.
use pdfluent::timestamp::TimestampOptions;
let opts = TimestampOptions::new(tsa)
.field_name("DocTimestamp")
.policy_oid(None); // None = TSA default policyPDFluent computes the document digest, sends a TSQ to the TSA, receives a TSR, and embeds the RFC 3161 token in a new signature field.
doc.add_timestamp(&opts)?;Save the file and confirm the timestamp token is present.
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"));
}
}