Skip to content

Sign a PDF: Digital Signatures & PAdES

Added in v0.3.50 (issue #235). PDF Oxide signs PDFs with a CMS/PKCS#7 detached signature embedded in /Contents, fills /ByteRange, and — for PAdES — appends an RFC 3161 timestamp and a Document Security Store (DSS) via incremental update. This page covers the create side. To inspect or verify an existing signature, see Digital Signatures (extraction).

Feature gate. Signing requires the signatures Cargo feature. PAdES-B-T and B-LT additionally need the tsa-client feature for the RFC 3161 timestamp source. Where a binding is built without these features, the signing calls raise the binding’s “not implemented” error.

Binding coverage

Signing is exposed by the bindings that wrap the C ABI’s pdf_sign_bytes* and pdf_certificate_load_* families: Rust, Python, Go (CGo), C#, and Swift. Node and WASM expose the read/verify side only — they ship the unsigned /Sig placeholder field on the page builder but not the CMS signer. Call a server-side binding to produce the signed bytes.

Surface Rust Python Go C# Swift Node WASM
Load credentials (PEM / PKCS#12)
sign_pdf_bytes (CMS detached)
sign_pdf_bytes_pades (B-B / B-T / B-LT)
Read DSS (/DSS) counts

is intentional: Node/WASM have no CMS signer wired.

How do I load signing credentials?

A signer needs a certificate and the matching private key. Load either from a PKCS#12 (.p12 / .pfx) blob or from separate PEM strings.

Rust

let creds = SigningCredentials::from_pkcs12(&data, "password")?;
// or from PEM strings:
let creds = SigningCredentials::from_pem(&cert_pem, &key_pem)?;

Python

cert = pdf_oxide.Certificate.load_pkcs12(data, "password")
# or from PEM strings:
cert = pdf_oxide.Certificate.load_pem(cert_pem, key_pem)

Go

cert, err := pdfoxide.LoadCertificate(data, "password")
// or from PEM strings:
cert, err := pdfoxide.LoadCertificateFromPem(certPem, keyPem)

C#

var cert = Certificate.Load(data, "password");
// or from PEM strings:
var cert = Certificate.LoadFromPem(certPem, keyPem);

Swift

let cert = try Certificate.loadFromBytes(bytes, password: "password")
// or from PEM strings:
let cert = try Certificate.loadFromPem(certPem: certPem, keyPem: keyPem)

Once loaded, the same handle exposes the embedded certificate’s subject, issuer, serial, validity, and is_valid — see the Certificate table on the extraction page.

How do I sign a PDF (CMS/PKCS#7)?

sign_pdf_bytes produces a legacy adbe.pkcs7.detached signature: a CMS envelope over the document byte-range, with the optional /Reason and /Location entries written into the signature dictionary.

Rust

use pdf_oxide::signatures::{sign_pdf_bytes, SignOptions, SigningCredentials};

let creds = SigningCredentials::from_pkcs12(&std::fs::read("id.p12")?, "testpass")?;
println!("signing as {}", creds.subject()?);

let pdf = std::fs::read("invoice.pdf")?;
let opts = SignOptions {
    reason: Some("Approved".into()),
    location: Some("HQ".into()),
    ..Default::default()
};
let signed = sign_pdf_bytes(&pdf, &creds, opts)?;
std::fs::write("invoice-signed.pdf", &signed)?;
assert!(signed.windows(10).any(|w| w == b"/ByteRange"));

Python

import pdf_oxide

with open("id.p12", "rb") as f:
    cert = pdf_oxide.Certificate.load_pkcs12(f.read(), "testpass")
print("signing as", cert.subject)

with open("invoice.pdf", "rb") as f:
    pdf_bytes = f.read()

signed = pdf_oxide.sign_pdf_bytes(pdf_bytes, cert, reason="Approved", location="HQ")
with open("invoice-signed.pdf", "wb") as f:
    f.write(signed)
assert b"/ByteRange" in signed

Go (CGo-only)

import (
    "fmt"
    "os"

    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

p12, _ := os.ReadFile("id.p12")
cert, err := pdfoxide.LoadCertificate(p12, "testpass")
if err != nil {
    panic(err)
}
defer cert.Close()

pdf, _ := os.ReadFile("invoice.pdf")
signed, err := pdfoxide.SignPdfBytes(pdf, cert, "Approved", "HQ")
if err != nil {
    panic(err)
}
os.WriteFile("invoice-signed.pdf", signed, 0o644)
fmt.Println("signed", len(signed), "bytes")

C#

using PdfOxide;

var cert = Certificate.Load(File.ReadAllBytes("id.p12"), "testpass");
Console.WriteLine($"signing as {cert.Subject}");

byte[] pdf = File.ReadAllBytes("invoice.pdf");
byte[] signed = cert.SignPdfBytes(pdf, reason: "Approved", location: "HQ");
File.WriteAllBytes("invoice-signed.pdf", signed);

Swift

import PdfOxide

let cert = try Certificate.loadFromBytes(Array(try Data(contentsOf: URL(fileURLWithPath: "id.p12"))), password: "testpass")
let pdf = Array(try Data(contentsOf: URL(fileURLWithPath: "invoice.pdf")))
let signed = try signBytes(pdf, certificate: cert, reason: "Approved", location: "HQ")
try Data(signed).write(to: URL(fileURLWithPath: "invoice-signed.pdf"))

Java

import fyi.oxide.pdf.PdfSigner;
import fyi.oxide.pdf.signature.SignOptions;
import fyi.oxide.pdf.signature.SignatureLevel;
import java.nio.file.*;

PdfSigner signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass");
byte[] pdf = Files.readAllBytes(Path.of("invoice.pdf"));

byte[] signed = signer.sign(pdf, SignOptions.builder()
        .withLevel(SignatureLevel.B_B)      // plain CMS / PKCS#7
        .withReason("Approved")
        .withLocation("HQ")
        .build());
Files.write(Path.of("invoice-signed.pdf"), signed);

PHP

use PdfOxide\PdfSigner;

$signer = PdfSigner::fromPkcs12('id.p12', 'testpass');
$pdf    = file_get_contents('invoice.pdf');

$signed = $signer->sign(
    $pdf,
    level:    PdfSigner::LEVEL_B_B,   // plain CMS / PKCS#7
    reason:   'Approved',
    location: 'HQ',
);
file_put_contents('invoice-signed.pdf', $signed);
$signer->close();

Ruby

require 'pdf_oxide'

# certificate_handle comes from your PKCS#12 / PEM credentials API
signer = PdfOxide::PdfSigner.new(certificate_handle)
pdf    = File.binread('invoice.pdf')

signed = signer.sign(pdf, level: :b, reason: 'Approved', location: 'HQ')
File.binwrite('invoice-signed.pdf', signed)

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto cert = pdf_oxide::Certificate::load_from_bytes(read_bytes("id.p12"), "testpass");
auto pdf  = read_bytes("invoice.pdf");

auto signed = pdf_oxide::sign_bytes(pdf, cert, "Approved", "HQ");
write_bytes("invoice-signed.pdf", signed);

Kotlin

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.SignOptions
import fyi.oxide.pdf.signature.SignatureLevel
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
    .withLevel(SignatureLevel.B_B)          // plain CMS / PKCS#7
    .withReason("Approved")
    .withLocation("HQ")
    .build())
Files.write(Path.of("invoice-signed.pdf"), signed)

Dart

import 'dart:io';
import 'package:pdf_oxide/pdf_oxide.dart';

final cert = Certificate.loadFromBytes(File('id.p12').readAsBytesSync(), 'testpass');
final pdf  = File('invoice.pdf').readAsBytesSync();

final signed = signBytes(pdf, cert, reason: 'Approved', location: 'HQ');
File('invoice-signed.pdf').writeAsBytesSync(signed);

R

library(pdfoxide)

cert <- pdf_certificate_load_from_bytes(readBin("id.p12", "raw", file.size("id.p12")), "testpass")
pdf  <- readBin("invoice.pdf", "raw", file.size("invoice.pdf"))

signed <- pdf_sign_bytes(pdf, cert, reason = "Approved", location = "HQ")
writeBin(signed, "invoice-signed.pdf")

Julia

using PdfOxide

cert = certificate_load_from_bytes(read("id.p12"), "testpass")
pdf  = read("invoice.pdf")

signed = sign_bytes(pdf, cert, "Approved", "HQ")
write("invoice-signed.pdf", signed)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

const cert = try pdf_oxide.Certificate.loadFromBytes(p12_bytes, "testpass");
const signed = try pdf_oxide.signBytes(a, pdf_bytes, cert, "Approved", "HQ");
defer a.free(signed);
try std.fs.cwd().writeFile(.{ .sub_path = "invoice-signed.pdf", .data = signed });

Scala

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.{SignOptions, SignatureLevel}
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
  .withLevel(SignatureLevel.B_B)            // plain CMS / PKCS#7
  .withReason("Approved")
  .withLocation("HQ")
  .build())
Files.write(Path.of("invoice-signed.pdf"), signed)

Clojure

(import '[fyi.oxide.pdf PdfSigner]
        '[fyi.oxide.pdf.signature SignOptions SignatureLevel])
(require '[clojure.java.io :as io])

(let [signer (PdfSigner/fromPkcs12 (java.nio.file.Path/of "id.p12" (make-array String 0)) "testpass")
      pdf    (java.nio.file.Files/readAllBytes (java.nio.file.Path/of "invoice.pdf" (make-array String 0)))
      opts   (-> (SignOptions/builder)
                 (.withLevel SignatureLevel/B_B)   ; plain CMS / PKCS#7
                 (.withReason "Approved")
                 (.withLocation "HQ")
                 (.build))
      signed (.sign signer pdf opts)]
  (java.nio.file.Files/write (java.nio.file.Path/of "invoice-signed.pdf" (make-array String 0)) signed
                             (make-array java.nio.file.OpenOption 0)))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXCertificate *cert = [POXCertificate loadFromBytes:p12Data password:@"testpass" error:&err];
NSData *pdf = [NSData dataWithContentsOfFile:@"invoice.pdf"];

NSData *signed = [POXSigning signBytes:pdf certificate:cert reason:@"Approved" location:@"HQ" error:&err];
[signed writeToFile:@"invoice-signed.pdf" atomically:YES];

Elixir

{:ok, cert} = PdfOxide.certificate_from_bytes(File.read!("id.p12"), "testpass")
pdf = File.read!("invoice.pdf")

{:ok, signed} = PdfOxide.sign_bytes(pdf, cert, "Approved", "HQ")
File.write!("invoice-signed.pdf", signed)

You can also build the document and sign it in one pass — DocumentBuilder().…​.build() returns the unsigned bytes, which you feed straight into sign_pdf_bytes. See DocumentBuilder.

What are the PAdES baseline levels?

PAdES (PDF Advanced Electronic Signatures, ETSI EN 319 142) layers long-term validation on top of the CMS signature. PDF Oxide ships an ETSI.CAdES.detached signer with a frozen level mapping shared across the C ABI and every binding:

Level Code What it adds
B-B 0 CAdES baseline: signed attributes incl. the ESS signing-certificate-v2 (RFC 5035).
B-T 1 B-B + an RFC 3161 signature-time-stamp unsigned attribute over the signature value.
B-LT 2 B-T + a Document Security Store (certs / CRLs / OCSPs + per-signature VRI).
B-LTA 3 B-LT + a document /DocTimeStamp. Reserved for signing — passing level 3 to the signer returns Unsupported. Detect an existing B-LTA file with the document-timestamp reader signal below.

B-T, B-LT, and B-LTA require a timestamper. For B-T and B-LT you supply a TSA URL and the binding fetches the RFC 3161 token; signing without one fails closed with Unsupported.

How do I create a PAdES B-LT signature?

sign_pdf_bytes_pades takes the level, an optional TSA URL, and the revocation material (DER-encoded certificates, CRLs, and OCSP responses) that becomes the B-LT DSS.

Rust

use pdf_oxide::signatures::{
    sign_pdf_bytes_pades, PadesLevel, RevocationMaterial, SignOptions, SigningCredentials,
};
use pdf_oxide::signatures::{TsaClient, TsaClientConfig};

let creds = SigningCredentials::from_pkcs12(&std::fs::read("id.p12")?, "testpass")?;
let pdf = std::fs::read("invoice.pdf")?;

// RFC 3161 token source for B-T / B-LT.
let tsa = TsaClient::new(TsaClientConfig::new("https://freetsa.org/tsr".into()));
let timestamper = |sig: &[u8]| tsa.request_timestamp(sig).map(|t| t.token_bytes().to_vec());

let material = RevocationMaterial {
    certificates: vec![std::fs::read("ca.der")?],
    ..Default::default()
};

let signed = sign_pdf_bytes_pades(
    &pdf,
    &creds,
    SignOptions { reason: Some("Approved".into()), ..Default::default() },
    PadesLevel::BLt,
    Some(&timestamper),
    &material,
)?;
std::fs::write("invoice-pades.pdf", &signed)?;

Python

import pdf_oxide
from pdf_oxide import PadesLevel, RevocationMaterial

with open("id.p12", "rb") as f:
    cert = pdf_oxide.Certificate.load_pkcs12(f.read(), "testpass")
with open("invoice.pdf", "rb") as f:
    pdf_bytes = f.read()
with open("ca.der", "rb") as f:
    ca_der = f.read()

signed = pdf_oxide.sign_pdf_bytes_pades(
    pdf_bytes,
    cert,
    PadesLevel.B_LT,
    tsa_url="https://freetsa.org/tsr",
    reason="Approved",
    revocation=RevocationMaterial(certs=[ca_der]),
)
with open("invoice-pades.pdf", "wb") as f:
    f.write(signed)

Go (CGo-only)

ca, _ := os.ReadFile("ca.der")
opts := pdfoxide.PAdESOptions{
    Level:      pdfoxide.PAdESBLt,
    TSAURL:     "https://freetsa.org/tsr",
    Reason:     "Approved",
    Revocation: &pdfoxide.RevocationMaterial{Certs: [][]byte{ca}},
}
signed, err := pdfoxide.SignPdfBytesPAdES(pdf, cert, opts)
if err != nil {
    panic(err)
}
os.WriteFile("invoice-pades.pdf", signed, 0o644)

C#

var material = new RevocationMaterial();
material.Certificates.Add(File.ReadAllBytes("ca.der"));

byte[] signed = cert.SignPdfBytesPades(pdf, new PadesSignOptions
{
    Level = PadesLevel.BLt,
    TsaUrl = "https://freetsa.org/tsr",
    Reason = "Approved",
    Revocation = material,
});
File.WriteAllBytes("invoice-pades.pdf", signed);

Swift

let ca = Array(try Data(contentsOf: URL(fileURLWithPath: "ca.der")))
let signed = try signBytesPades(
    pdf,
    certificate: cert,
    level: 2,                              // 0=B-B 1=B-T 2=B-LT
    tsaUrl: "https://freetsa.org/tsr",
    reason: "Approved",
    certs: [ca]
)
try Data(signed).write(to: URL(fileURLWithPath: "invoice-pades.pdf"))

Java

import fyi.oxide.pdf.PdfSigner;
import fyi.oxide.pdf.signature.SignOptions;
import fyi.oxide.pdf.signature.SignatureLevel;
import java.nio.file.*;

PdfSigner signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass");
byte[] pdf = Files.readAllBytes(Path.of("invoice.pdf"));

byte[] signed = signer.sign(pdf, SignOptions.builder()
        .withLevel(SignatureLevel.B_LT)           // B-T / B-LT require a TSA
        .withTsaUrl("https://freetsa.org/tsr")
        .withReason("Approved")
        .build());
Files.write(Path.of("invoice-pades.pdf"), signed);

PHP

use PdfOxide\PdfSigner;

$signer = PdfSigner::fromPkcs12('id.p12', 'testpass');
$pdf    = file_get_contents('invoice.pdf');

$signed = $signer->sign(
    $pdf,
    level:  PdfSigner::LEVEL_B_LT,             // B-T / B-LT require a TSA
    tsaUrl: 'https://freetsa.org/tsr',
    reason: 'Approved',
);
file_put_contents('invoice-pades.pdf', $signed);
$signer->close();

Ruby

require 'pdf_oxide'

signer = PdfOxide::PdfSigner.new(certificate_handle)
pdf    = File.binread('invoice.pdf')

signed = signer.sign(
  pdf,
  level:   :lt,                               # B-T / B-LT require a TSA
  tsa_url: 'https://freetsa.org/tsr',
  reason:  'Approved',
)
File.binwrite('invoice-pades.pdf', signed)

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto cert = pdf_oxide::Certificate::load_from_bytes(read_bytes("id.p12"), "testpass");
auto pdf  = read_bytes("invoice.pdf");

pdf_oxide::RevocationMaterial material;
material.certs.push_back(read_bytes("ca.der"));

auto signed = pdf_oxide::sign_bytes_pades(
    pdf, cert,
    /*level=*/2,                              // 0=B-B 1=B-T 2=B-LT
    "https://freetsa.org/tsr",
    "Approved", /*location=*/"",
    material);
write_bytes("invoice-pades.pdf", signed);

Kotlin

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.SignOptions
import fyi.oxide.pdf.signature.SignatureLevel
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
    .withLevel(SignatureLevel.B_LT)               // B-T / B-LT require a TSA
    .withTsaUrl("https://freetsa.org/tsr")
    .withReason("Approved")
    .build())
Files.write(Path.of("invoice-pades.pdf"), signed)

Dart

import 'dart:io';
import 'package:pdf_oxide/pdf_oxide.dart';

final cert = Certificate.loadFromBytes(File('id.p12').readAsBytesSync(), 'testpass');
final pdf  = File('invoice.pdf').readAsBytesSync();
final ca   = File('ca.der').readAsBytesSync();

final signed = signBytesPades(
  pdf, cert,
  2,                                          // 0=B-B 1=B-T 2=B-LT
  tsaUrl: 'https://freetsa.org/tsr',
  reason: 'Approved',
  certs: [ca],
);
File('invoice-pades.pdf').writeAsBytesSync(signed);

R

library(pdfoxide)

cert <- pdf_certificate_load_from_bytes(readBin("id.p12", "raw", file.size("id.p12")), "testpass")
pdf  <- readBin("invoice.pdf", "raw", file.size("invoice.pdf"))
ca   <- readBin("ca.der", "raw", file.size("ca.der"))

signed <- pdf_sign_bytes_pades(
  pdf, cert,
  level = 2L,                                 # 0=B-B 1=B-T 2=B-LT
  tsa_url = "https://freetsa.org/tsr",
  reason = "Approved",
  certs = list(ca)
)
writeBin(signed, "invoice-pades.pdf")

Julia

using PdfOxide

cert = certificate_load_from_bytes(read("id.p12"), "testpass")
pdf  = read("invoice.pdf")
ca   = read("ca.der")

signed = sign_bytes_pades(
    pdf, cert,
    2,                                        # 0=B-B 1=B-T 2=B-LT
    "https://freetsa.org/tsr",
    "Approved", "";
    certs = [ca],
)
write("invoice-pades.pdf", signed)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

const cert = try pdf_oxide.Certificate.loadFromBytes(p12_bytes, "testpass");
const signed = try pdf_oxide.signBytesPades(
    a, pdf_bytes, cert,
    2,                                        // 0=B-B 1=B-T 2=B-LT
    "https://freetsa.org/tsr",
    "Approved", "",
    &[_][]const u8{ca_der}, &.{}, &.{},
);
defer a.free(signed);
try std.fs.cwd().writeFile(.{ .sub_path = "invoice-pades.pdf", .data = signed });

Scala

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.{SignOptions, SignatureLevel}
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
  .withLevel(SignatureLevel.B_LT)             // B-T / B-LT require a TSA
  .withTsaUrl("https://freetsa.org/tsr")
  .withReason("Approved")
  .build())
Files.write(Path.of("invoice-pades.pdf"), signed)

Clojure

(import '[fyi.oxide.pdf PdfSigner]
        '[fyi.oxide.pdf.signature SignOptions SignatureLevel])

(let [signer (PdfSigner/fromPkcs12 (java.nio.file.Path/of "id.p12" (make-array String 0)) "testpass")
      pdf    (java.nio.file.Files/readAllBytes (java.nio.file.Path/of "invoice.pdf" (make-array String 0)))
      opts   (-> (SignOptions/builder)
                 (.withLevel SignatureLevel/B_LT)    ; B-T / B-LT require a TSA
                 (.withTsaUrl "https://freetsa.org/tsr")
                 (.withReason "Approved")
                 (.build))
      signed (.sign signer pdf opts)]
  (java.nio.file.Files/write (java.nio.file.Path/of "invoice-pades.pdf" (make-array String 0)) signed
                             (make-array java.nio.file.OpenOption 0)))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXCertificate *cert = [POXCertificate loadFromBytes:p12Data password:@"testpass" error:&err];
NSData *pdf = [NSData dataWithContentsOfFile:@"invoice.pdf"];
NSData *ca  = [NSData dataWithContentsOfFile:@"ca.der"];

NSData *signed = [POXSigning signBytesPades:pdf
                               certificate:cert
                                     level:2           // 0=B-B 1=B-T 2=B-LT
                                    tsaUrl:@"https://freetsa.org/tsr"
                                    reason:@"Approved"
                                  location:nil
                                     certs:@[ca]
                                      crls:@[]
                                     ocsps:@[]
                                     error:&err];
[signed writeToFile:@"invoice-pades.pdf" atomically:YES];

Elixir

{:ok, cert} = PdfOxide.certificate_from_bytes(File.read!("id.p12"), "testpass")
pdf = File.read!("invoice.pdf")
ca  = File.read!("ca.der")

{:ok, signed} =
  PdfOxide.sign_bytes_pades(pdf, cert, 2, "https://freetsa.org/tsr",   # 0=B-B 1=B-T 2=B-LT
    reason: "Approved",
    certs: [ca]
  )
File.write!("invoice-pades.pdf", signed)

For a quick B-B signature with no timestamp, pass level 0 / PadesLevel.B_B and omit the TSA URL.

How do I inspect the Document Security Store I produced?

After a B-LT sign, reopen the PDF and read its /DSS to confirm the validation material landed. dss() returns None/null when the document has no DSS — that is not an error.

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::signatures::read_dss;

let doc = PdfDocument::open("invoice-pades.pdf")?;
if let Some(dss) = read_dss(&doc)? {
    println!("certs={} crls={} ocsps={} vri={}",
        dss.certificates.len(), dss.crls.len(), dss.ocsp_responses.len(), dss.vri.len());
}

Python

import pdf_oxide

doc = pdf_oxide.PdfDocument("invoice-pades.pdf")
dss = doc.dss()
if dss is not None:
    print("certs", len(dss.certs), "crls", len(dss.crls),
          "ocsps", len(dss.ocsps), "vri", len(dss.vri))

Go (CGo-only)

doc, _ := pdfoxide.Open("invoice-pades.pdf")
defer doc.Close()

dss, _ := doc.DSS()           // nil when the PDF has no /DSS
if dss != nil {
    fmt.Printf("certs=%d crls=%d ocsps=%d vri=%d\n",
        len(dss.Certs), len(dss.CRLs), len(dss.OCSPs), dss.VRICount)
}

C#

using var doc = PdfDocument.Open("invoice-pades.pdf");
var dss = doc.GetDss();        // null when the PDF has no /DSS
if (dss is not null)
{
    Console.WriteLine($"certs={dss.Certificates.Count} crls={dss.Crls.Count} " +
                      $"ocsps={dss.OcspResponses.Count} vri={dss.VriCount}");
}

Swift

let doc = try Document.open("invoice-pades.pdf")
if let dss = try doc.dss() {
    print("certs=\(try dss.certCount()) crls=\(try dss.crlCount()) " +
          "ocsps=\(try dss.ocspCount()) vri=\(try dss.vriCount())")
}

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("invoice-pades.pdf");
auto dss = doc.get_dss();          // throws if the PDF has no /DSS
std::cout << "certs=" << dss.cert_count() << " crls=" << dss.crl_count()
          << " ocsps=" << dss.ocsp_count() << " vri=" << dss.vri_count() << "\n";

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('invoice-pades.pdf');
final dss = doc.getDss();          // throws if the PDF has no /DSS
print('certs=${dss.certCount} crls=${dss.crlCount} '
      'ocsps=${dss.ocspCount} vri=${dss.vriCount}');

R

library(pdfoxide)

doc <- pdf_open("invoice-pades.pdf")
dss <- pdf_get_dss(doc)            # errors if the PDF has no /DSS
cat(sprintf("certs=%d crls=%d ocsps=%d vri=%d\n",
            pdf_dss_cert_count(dss), pdf_dss_crl_count(dss),
            pdf_dss_ocsp_count(dss), pdf_dss_vri_count(dss)))

Julia

using PdfOxide

doc = open_document("invoice-pades.pdf")
dss = document_get_dss(doc)        # errors if the PDF has no /DSS
println("certs=$(dss_cert_count(dss)) crls=$(dss_crl_count(dss)) ",
        "ocsps=$(dss_ocsp_count(dss)) vri=$(dss_vri_count(dss))")

Zig

const pdf_oxide = @import("pdf_oxide");

var doc = try pdf_oxide.Document.open("invoice-pades.pdf");
const dss = try doc.dss();         // errors if the PDF has no /DSS
std.debug.print("certs={d} crls={d} ocsps={d} vri={d}\n", .{
    try dss.certCount(), try dss.crlCount(), try dss.ocspCount(), try dss.vriCount(),
});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"invoice-pades.pdf" error:&err];
POXDss *dss = [doc dssWithError:&err];     // nil + error when the PDF has no /DSS
if (dss != nil) {
    NSLog(@"certs=%d crls=%d ocsps=%d vri=%d",
          [dss certCount], [dss crlCount], [dss ocspCount], [dss vriCount]);
}

Elixir

{:ok, doc} = PdfOxide.open("invoice-pades.pdf")

case PdfOxide.document_dss(doc) do      # {:error, _} when the PDF has no /DSS
  {:ok, dss} ->
    IO.puts("certs=#{PdfOxide.dss_cert_count(dss)} crls=#{PdfOxide.dss_crl_count(dss)} " <>
            "ocsps=#{PdfOxide.dss_ocsp_count(dss)} vri=#{PdfOxide.dss_vri_count(dss)}")
  _ -> :no_dss
end

DSS accessors

Surface (C ABI / Swift) Python / Go / C# equivalent Description
dss.cert_count() len(dss.certs) / len(dss.Certs) / dss.Certificates.Count Document-level DER certificates (/Certs)
dss.crl_count() len(dss.crls) / len(dss.CRLs) / dss.Crls.Count Document-level DER CRLs (/CRLs)
dss.ocsp_count() len(dss.ocsps) / len(dss.OCSPs) / dss.OcspResponses.Count Document-level DER OCSP responses (/OCSPs)
dss.vri_count() len(dss.vri) / dss.VRICount / dss.VriCount Per-signature /VRI entries (uppercase-hex SHA-1 of /Contents)

The C ABI / Swift expose count methods plus per-index getters (pdf_dss_get_cert etc.); Python and Go return the DER blobs as lists directly.

How do I detect a B-LTA archival timestamp?

Signature.pades_level is signature-scoped and tops out at B-LT by design. B-LTA adds a document-level /DocTimeStamp, which is a document-wide signal — read it with the document-timestamp check:

Python

import pdf_oxide

with open("archive.pdf", "rb") as f:
    is_lta = pdf_oxide.has_document_timestamp(f.read())

Rust

use pdf_oxide::signatures::has_document_timestamp;

let pdf = std::fs::read("archive.pdf")?;
let is_lta = has_document_timestamp(&pdf);

Go

doc, _ := pdfoxide.Open("archive.pdf")
defer doc.Close()

is_lta, _ := doc.HasDocumentTimestamp()

C#

using var doc = PdfDocument.Open("archive.pdf");
bool isLta = doc.HasDocumentTimestamp();

Swift

let doc = try Document.open("archive.pdf")
let isLta = try doc.hasTimestamp()

Performance

PDF Oxide’s extraction core runs at 0.8 ms mean / 9 ms p99 with a 100% pass rate on the reference corpus (v0.3.8 benchmark). Signing cost is dominated by the CMS/RSA operation plus, for B-T/B-LT, the network round-trip to the TSA — the PDF byte-range hashing and incremental-update write add negligible overhead on top.

FAQ

Do I need a network connection to sign? Only for B-T and B-LT (and B-LTA), which embed an RFC 3161 timestamp fetched from a TSA URL. A plain sign_pdf_bytes and a PAdES B-B signature are fully offline.

Why does B-T/B-LT fail without a TSA URL? By design — those levels require a timestamp token, so the signer fails closed with an Unsupported error rather than silently downgrading the level.

Can I sign from Node or the browser (WASM)? Not yet. Node and WASM expose signature reading/verification and the unsigned /Sig placeholder field, but the CMS signer is wired only into Rust, Python, Go, C#, and Swift. Sign on a server-side binding and ship the signed bytes.

What signature algorithm does the signer use? RSA-PKCS#1 v1.5 over SHA-256, the padding accepted by essentially every PDF reader. A FIPS crypto-provider policy can make signing fail closed if the configured policy disallows an algorithm.