Digital Signatures
Added in v0.3.38. PDF Oxide reads signatures from /AcroForm /Fields → /Sig, inspects the CMS envelope inside /Contents, and runs the RFC 5652 §5.4 signer-attributes check against the embedded certificate plus the §11.2 messageDigest check against the caller-provided document bytes.
Scope. This release covers verification. Signing PDFs (as opposed to verifying existing signatures) is tracked as the remaining half of issue #208 and is not shipped yet.
What’s verified
- RSA-PKCS#1 v1.5 over SHA-1 / SHA-256 / SHA-384 / SHA-512 — the padding used by effectively every signed PDF in the wild — returns
Valid/Invalid. - RSA-PSS and ECDSA surface as
Unknown/UnsupportedFeatureException. Callers that need those can still read the embedded certificate viaSignature.GetCertificate()/.get_certificate()and run their own check. - Trust-root lookup, expiry window, and signer DN are stamped onto the verification result from the embedded certificate.
Quick Example
Rust
use pdf_oxide::PdfDocument;
let doc = PdfDocument::open("signed.pdf")?;
for sig in doc.signatures() {
println!("{} → {:?}", sig.signer_name(), sig.verify()?);
// End-to-end with document bytes
let pdf_bytes = std::fs::read("signed.pdf")?;
println!("detached ok = {:?}", sig.verify_detached(&pdf_bytes)?);
// Cert inspection + trust-root / expiry / signer DN
let result = pdf_oxide::SignatureVerifier::verify(&sig, &pdf_bytes)?;
println!("signer DN = {}", result.signer_dn);
}
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("signed.pdf")
for sig in doc.signatures():
print(sig.signer_name, "→", sig.verify())
# End-to-end with document bytes
with open("signed.pdf", "rb") as f:
pdf_bytes = f.read()
for sig in doc.signatures():
print("detached ok =", sig.verify_detached(pdf_bytes))
Node / TypeScript
import { PdfDocument } from "pdf-oxide";
import { readFileSync } from "fs";
const doc = PdfDocument.open("signed.pdf");
for (const sig of doc.signatures()) {
console.log(sig.signerName, "→", sig.verify());
console.log("detached ok =", sig.verifyDetached(readFileSync("signed.pdf")));
}
C#
using PdfOxide;
using var doc = PdfDocument.Open("signed.pdf");
foreach (var sig in doc.Signatures)
{
Console.WriteLine($"{sig.SignerName} → {sig.Verify()}");
var cert = sig.GetCertificate();
Console.WriteLine($"subject={cert.Subject} issuer={cert.Issuer} valid={cert.IsValid}");
}
Go (CGo-only)
import (
"fmt"
"os"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
doc, _ := pdfoxide.Open("signed.pdf")
defer doc.Close()
pdfBytes, _ := os.ReadFile("signed.pdf")
sigs, _ := doc.Signatures()
for _, sig := range sigs {
ok, _ := sig.Verify()
fmt.Printf("%s → %v\n", sig.SignerName, ok)
// End-to-end with document bytes
detachedOk, _ := sig.VerifyDetached(pdfBytes)
fmt.Println("detached ok =", detachedOk)
}
WASM
import init, { PdfDocument } from "pdf-oxide-wasm/web";
await init();
const doc = new PdfDocument(bytes);
for (const sig of doc.signatures()) {
console.log(sig.signerName, "→", sig.verify());
}
Signature
Enumerate and inspect signatures. Available in every binding.
| Property / Method | Description |
|---|---|
.signer_name / .SignerName |
Common Name from the signer’s certificate |
.reason / .Reason |
Signing reason (e.g. “I approve this document”) |
.location / .Location |
Location field |
.contact_info |
Contact info field |
.signing_time / .SigningTime |
UTC signing time from the CMS envelope (DateTimeOffset? in C#) |
.verify() / .Verify() |
RFC 5652 §5.4 signer-attributes check against embedded cert. Returns Valid / Invalid / Unknown. |
.verify_detached(pdf_bytes) / .VerifyDetached(pdfBytes) |
Adds the RFC 5652 §11.2 messageDigest check against caller-supplied document bytes. |
.get_certificate() / .GetCertificate() |
Returns a Certificate for deeper inspection. |
Certificate
X.509 certificate extracted from the CMS /Contents blob via x509-parser. Available in every binding as of v0.3.38 (Python / Go / WASM accessors landed right after the initial release).
| Property / Method | Description |
|---|---|
.subject |
Distinguished Name of the certificate holder |
.issuer |
DN of the issuing CA |
.serial |
Serial number (as big-endian bytes or string) |
.not_before |
Validity window start (DateTimeOffset) |
.not_after |
Validity window end |
.is_valid |
True if not_before ≤ now ≤ not_after |
Timestamp — RFC 3161 TSTInfo
Parse the timestamp blob from a signature’s TimeStampToken attribute, or from a standalone RFC 3161 response. Available in every binding (Node support landed post-release in v0.3.38).
Rust
use pdf_oxide::Timestamp;
let ts = Timestamp::parse(&tst_bytes)?;
println!("{} serial={} tsa={}", ts.time(), ts.serial(), ts.tsa_name());
Python
from pdf_oxide import Timestamp
ts = Timestamp.parse(tst_bytes)
print(ts.time, ts.serial, ts.policy_oid, ts.tsa_name, ts.hash_algorithm)
C#
var ts = Timestamp.Parse(tstBytes);
Console.WriteLine($"{ts.Time} serial={ts.Serial} tsa={ts.TsaName}");
Node / TypeScript
import { Timestamp } from "pdf-oxide";
const ts = Timestamp.parse(tstBytes);
console.log(ts.time, ts.serial, ts.policyOid, ts.tsaName, ts.hashAlgorithm);
ts.close();
Go
ts, _ := pdfoxide.ParseTimestamp(tstBytes)
fmt.Println(ts.Time, ts.Serial, ts.PolicyOid, ts.TsaName, ts.HashAlgorithm)
WASM
import init, { Timestamp } from "pdf-oxide-wasm/web";
await init();
const ts = Timestamp.parse(tstBytes);
console.log(ts.time, ts.serial, ts.policyOid, ts.tsaName, ts.hashAlgorithm);
| Property | Description |
|---|---|
.time |
UTC time asserted by the TSA |
.serial |
Unique serial for this timestamp |
.policy_oid |
TSA policy OID |
.tsa_name |
TSA identifier |
.hash_algorithm |
Hash algorithm used for message_imprint |
.message_imprint |
Hash of the signed payload |
.verify() |
Checks the TSTInfo signature against the embedded TSA cert |
TsaClient — RFC 3161 HTTP Client
Request a fresh timestamp from a Time Stamp Authority over HTTP. Behind the tsa-client Cargo feature. Available in every binding except WASM (Node support landed post-release in v0.3.38; WASM is intentionally not wired — ureq doesn’t compile to wasm32).
Rust
use pdf_oxide::TsaClient;
let client = TsaClient::new("https://freetsa.org/tsr")
.with_timeout(std::time::Duration::from_secs(30))
.with_hash_algorithm(pdf_oxide::HashAlgorithm::Sha256)
.with_nonce(true);
let ts = client.request_timestamp(&pdf_bytes)?;
println!("{} serial={}", ts.time(), ts.serial());
Python
from pdf_oxide import TsaClient
client = TsaClient(
url="https://freetsa.org/tsr",
username=None,
password=None,
timeout_seconds=30,
hash_algorithm=2, # 2 = SHA-256
use_nonce=True,
cert_req=True,
)
ts = client.request_timestamp(pdf_bytes)
print(ts.time, ts.serial)
Node / TypeScript
import { TsaClient } from "pdf-oxide";
const client = new TsaClient({
url: "https://freetsa.org/tsr",
timeoutSeconds: 30,
hashAlgorithm: 2, // 2 = SHA-256
useNonce: true,
certReq: true,
});
const ts = client.requestTimestamp(pdfBytes);
console.log(ts.time, ts.serial);
ts.close();
client.close();
C#
var client = new TsaClient("https://freetsa.org/tsr");
var ts = client.RequestTimestamp(pdfBytes);
Console.WriteLine($"{ts.Time} serial={ts.Serial}");
Go
client := pdfoxide.NewTsaClient("https://freetsa.org/tsr")
ts, err := client.RequestTimestamp(pdfBytes)
if err != nil { log.Fatal(err) }
fmt.Println(ts.Time, ts.Serial)
TsaClient sends an RFC 3161 TimeStampReq over HTTP POST with a nonce and HTTP Basic auth (when username / password are set). The response is unwrapped and parsed through Timestamp::parse.
Binding coverage summary
Full parity as of v0.3.38: every signature surface ships in every binding, with the single intentional exception of TsaClient on WASM (ureq is wasm-incompatible).
| Surface | Rust | Python | Node | C# | Go | WASM |
|---|---|---|---|---|---|---|
Signature enumerate + verify |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Certificate inspect |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Timestamp parse + verify |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
TsaClient HTTP request |
✓ | ✓ | ✓ | ✓ | ✓ | — |
— on TsaClient × WASM is intentional and permanent: ureq doesn’t compile to wasm32. Call TsaClient from a server-side binding and feed the raw timestamp bytes into Timestamp.parse() on WASM if you need to inspect the response in a browser.
Related Pages
- Metadata & XMP — document-level metadata extraction
- API Reference — Rust-side
SignatureVerifiersurface