Skip to content

PDF Crypto Policy, FIPS & CBOM

PDF Oxide abstracts every encryption and signature operation behind a pluggable cryptographic provider, layers a fail-closed runtime policy on top, and records a per-process inventory of the algorithms it actually exercised. That inventory exports as a CycloneDX 1.6 Cryptographic Bill of Materials (CBOM) — the machine-readable artifact regulated buyers and auditors ask for.

This page covers provider selection, FIPS 140-3 mode, the runtime policy grammar, the crypto inventory, and CBOM export.

Binding coverage. The whole crypto-governance surface is process-global (it takes no document handle) and is exposed by Rust, Python, Go, Node.js / TypeScript and C#. Method names differ per binding (idiomatic casing). The provider-selection half (active_provider / fips_available / use_fips) is the FIPS surface from issue #236; the policy half (set_policy / policy / inventory / cbom) is the runtime-governance surface from issue #230. FIPS mode itself only does anything when the native library was compiled with --features fips — otherwise use_fips returns/raises an error and fips_available is false.

The two providers

Provider Build Algorithms Use
rust-crypto (default) always present every algorithm the PDF spec references, including the legacy MD5 + RC4 path for ISO 32000-1 R≤4 documents general-purpose
aws-lc-rs opt-in, --features fips FIPS 140-3 validated; refuses MD5, RC4, and SHA-1 signing FIPS compliance

The default provider is installed lazily — the first crypto operation (opening an encrypted document, verifying a signature) commits it. That is why you must switch to FIPS before any crypto work happens in the process.

How do I check the active crypto provider?

active_provider is non-initializing: if no provider has been committed yet it returns "rust-crypto (default, lazy)" without locking in the default, so a later use_fips call can still succeed. Once a provider is committed (explicitly or lazily) it returns the real name ("rust-crypto" or "aws-lc-rs").

import pdf_oxide

# Safe to call first — does not commit a provider.
print(pdf_oxide.crypto_active_provider())      # "rust-crypto (default, lazy)"
print(pdf_oxide.crypto_available_providers())  # ["rust-crypto"]  (+ "aws-lc-rs" on a FIPS wheel)
use pdf_oxide::crypto;

// Non-initializing read for display/audit.
let name = if crypto::is_set() {
    crypto::active().name().to_string()
} else {
    format!("{} (default, lazy)", crypto::RustCryptoProvider.name())
};
println!("active crypto provider: {name}");
package main

import (
    "fmt"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    fmt.Println(pdfoxide.ActiveCryptoProvider())   // "rust-crypto"
    fmt.Println(pdfoxide.IsFipsCryptoAvailable())  // false unless built with FIPS
}
const pdf_oxide = require("pdf-oxide");

console.log(pdf_oxide.getActiveCryptoProvider()); // "rust-crypto (default, lazy)"
console.log(pdf_oxide.isFipsCryptoAvailable());   // false unless the -fips package
using PdfOxide;

Console.WriteLine(PdfDocument.GetActiveCryptoProvider()); // "rust-crypto"
Console.WriteLine(PdfDocument.IsFipsCryptoAvailable());   // False unless FIPS lib

Signatures

Operation Rust Python Go Node.js C#
Active provider crypto::active().name() -> &'static str crypto_active_provider() -> str ActiveCryptoProvider() -> string getActiveCryptoProvider(): string GetActiveCryptoProvider() -> string
FIPS available? cfg(feature = "fips") crypto_available_providers() -> list[str] IsFipsCryptoAvailable() -> bool isFipsCryptoAvailable(): boolean IsFipsCryptoAvailable() -> bool

The C ABI these wrap: char *pdf_oxide_crypto_active_provider(void) and int32_t pdf_oxide_crypto_fips_available(void) (returns 1 if the FIPS provider was compiled in, 0 otherwise).

How do I enable FIPS mode for a PDF?

Call use_fips (or, in Rust, set_provider) before any crypto operation. It is set-once: a second call — or any call after the default provider was lazily installed — fails.

C ABI error codes for int32_t pdf_oxide_crypto_use_fips(void):

Code Meaning
0 success
1 FIPS feature not compiled in
2 a provider is already set
import pdf_oxide

try:
    pdf_oxide.crypto_use_fips()   # raises RuntimeError if not compiled in / already set
    print("FIPS provider active:", pdf_oxide.crypto_active_provider())  # "aws-lc-rs"
except RuntimeError as e:
    print("FIPS unavailable:", e)

# Now every encrypted-PDF open / signature verify routes through aws-lc-rs.
doc = pdf_oxide.PdfDocument("encrypted-r6.pdf")
print(doc.page_count())
use std::sync::Arc;
use pdf_oxide::PdfDocument;
use pdf_oxide::crypto::{set_provider, AwsLcProvider};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Must run before any pdf_oxide operation that uses crypto.
    set_provider(Arc::new(AwsLcProvider::new()))
        .expect("crypto provider already installed");

    // Every PDF open / signature verify now routes through aws-lc-rs.
    let doc = PdfDocument::open("encrypted-r6.pdf")?;
    println!("pages: {}", doc.page_count());
    Ok(())
}
package main

import (
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    // Before any crypto operation.
    if err := pdfoxide.UseFipsCryptoProvider(); err != nil {
        log.Fatalf("FIPS unavailable: %v", err) // ErrFipsNotCompiled / ErrCryptoProviderAlreadySet
    }
    log.Println("active:", pdfoxide.ActiveCryptoProvider()) // "aws-lc-rs"
}
const pdf_oxide = require("pdf-oxide");

try {
  pdf_oxide.useFipsCryptoProvider();
  console.log("active:", pdf_oxide.getActiveCryptoProvider()); // "aws-lc-rs"
} catch (e) {
  console.error("FIPS unavailable:", e.message);
}
using PdfOxide;

try
{
    PdfDocument.UseFipsCryptoProvider();
    Console.WriteLine(PdfDocument.GetActiveCryptoProvider()); // "aws-lc-rs"
}
catch (InvalidOperationException e)
{
    Console.WriteLine($"FIPS unavailable: {e.Message}");
}

What does FIPS reject?

PDF Standard Security R≤4 (PDF 1.4–1.6 password encryption, ISO 32000-1 §7.6.3) uses MD5 for key derivation and RC4/AES-128 as the cipher. FIPS 140-3 forbids MD5 and RC4, so opening such a document under aws-lc-rs fails with a remediation message:

active CryptoProvider 'aws-lc-rs' rejects PDF Standard Security R=4
(R≤4 requires MD5; FIPS 140-3 forbids MD5).
Re-encrypt the document at R=6 (AES-256) or build pdf_oxide
without the 'fips' feature so the default 'rust-crypto'
provider stays active.

The fix: re-encrypt at R=6 (PDF 2.0 AES-256, ISO 32000-2 §7.6.4) in a non-FIPS-restricted environment. R=6 documents open cleanly under aws-lc-rs.

Installing the FIPS variant. The -fips distributions ship single-provider binaries (auditors usually require this). Use pip install pdf_oxide_fips, npm install pdf-oxide-fips, dotnet add package PdfOxide.Fips, go get github.com/yfedoseev/pdf_oxide/go-fips, or build Rust with cargo build --features fips. Default and FIPS variants of the same release tag are byte-equal on all non-crypto paths.

How do I set a runtime crypto policy?

A policy narrows what algorithms are allowed, independent of which provider is active. It is set-once (a runtime downgrade is an attack vector) and fail-closed (an unparseable spec is rejected and no policy is installed). The default — never set — is "compat", which is byte-for-byte identical to pre-policy behaviour.

Grammar: mode[;clause]*

  • modecompat | strict | fips-strict
  • clause = (allow|deny):<alg>@<read|write> — e.g. "compat;deny:rc4@write;deny:md5@write" or "fips-strict"

C ABI error codes for int32_t pdf_oxide_crypto_set_policy(const char *spec):

Code Meaning
0 success
1 invalid argument (null / non-UTF-8 spec)
2 parse error — spec rejected, policy not installed
3 a policy is already set
import pdf_oxide

# Deny the legacy write paths while staying compatible for reads.
pdf_oxide.crypto_set_policy("compat;deny:rc4@write;deny:md5@write")
print(pdf_oxide.crypto_policy())  # canonical grammar string
use pdf_oxide::crypto::{self, SecurityPolicy};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse fail-closed, then install set-once.
    let policy: SecurityPolicy = "fips-strict".parse()?;
    crypto::set_policy(policy).expect("a crypto policy is already set");

    println!("active policy: {}", crypto::active_policy());
    Ok(())
}
package main

import (
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    // Treat any error as fatal — the policy is not installed on failure.
    if err := pdfoxide.SetCryptoPolicy("strict;deny:md5@write"); err != nil {
        log.Fatalf("policy rejected: %v", err)
    }
    log.Println("active policy:", pdfoxide.CryptoPolicy())
}
const pdf_oxide = require("pdf-oxide");

// Throws on an unparseable spec (fail-closed) or if already set.
pdf_oxide.setCryptoPolicy("fips-strict");
console.log(pdf_oxide.cryptoPolicy()); // "fips-strict"
using PdfOxide;

// Throws ArgumentException (rejected) or InvalidOperationException (already set).
PdfDocument.SetCryptoPolicy("compat;deny:rc4@write");
Console.WriteLine(PdfDocument.CryptoPolicy());

Signatures

Operation Rust Python Go Node.js C#
Set policy crypto::set_policy(SecurityPolicy) -> Result<(), SetPolicyError> crypto_set_policy(spec: str) -> None SetCryptoPolicy(spec string) -> error setCryptoPolicy(spec: string): void SetCryptoPolicy(string spec) -> void
Read policy crypto::active_policy() -> &'static SecurityPolicy crypto_policy() -> str CryptoPolicy() -> string cryptoPolicy(): string CryptoPolicy() -> string

How do I read the crypto inventory?

The inventory is the set of algorithm tokens actually exercised so far this process — a lock-free atomic bitset recorded at each enforcement boundary. It is the minimal “what crypto did this run use?” report. Tokens are stable lowercase strings such as md5, aes256, rc4, sha256.

The C ABI returns a comma-joined string (char *pdf_oxide_crypto_inventory(void), "" when nothing was exercised); the high-level bindings split it for you into a list/slice/array.

import pdf_oxide

doc = pdf_oxide.PdfDocument("signed.pdf")
doc.verify_signatures()                  # exercises some algorithms

print(pdf_oxide.crypto_inventory())      # e.g. ["sha256", "aes256"]  (list[str])
use pdf_oxide::crypto;

// `inventory()` returns the exercised algorithms in declaration order.
let used: Vec<&'static str> = crypto::inventory()
    .into_iter()
    .map(|a| a.token())
    .collect();
println!("exercised: {used:?}"); // e.g. ["md5", "aes256"]
package main

import (
    "fmt"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    inv := pdfoxide.CryptoInventory() // []string, empty when nothing exercised
    fmt.Println("exercised:", inv)
}
const pdf_oxide = require("pdf-oxide");

console.log(pdf_oxide.cryptoInventory()); // string[], e.g. ["sha256"]
using PdfOxide;

foreach (var token in PdfDocument.CryptoInventory()) // string[]
    Console.WriteLine(token);

How do I export a CBOM (CycloneDX) for a PDF?

crypto_cbom serialises the inventory into a CycloneDX 1.6 cryptographic-asset Bill of Materials as a JSON string. The document always has bomFormat: "CycloneDX", specVersion: "1.6", a metadata block (RFC 3339 timestamp + the pdf_oxide tool component), and one cryptographic-asset component per exercised algorithm. An empty inventory yields a valid BOM with no components.

import json
import pdf_oxide

# Run your workload first so the inventory reflects what was used.
doc = pdf_oxide.PdfDocument("signed.pdf")
doc.verify_signatures()

cbom = pdf_oxide.crypto_cbom()           # JSON string
report = json.loads(cbom)
assert report["bomFormat"] == "CycloneDX"
assert report["specVersion"] == "1.6"

with open("pdf-cbom.json", "w") as f:
    f.write(cbom)
use std::fs;
use pdf_oxide::crypto;

fn main() -> std::io::Result<()> {
    // ... run the crypto workload first ...
    let cbom: String = crypto::cbom_json();   // CycloneDX 1.6 JSON
    fs::write("pdf-cbom.json", cbom)?;
    Ok(())
}
package main

import (
    "os"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    cbom := pdfoxide.CryptoCBOM() // CycloneDX 1.6 JSON string
    _ = os.WriteFile("pdf-cbom.json", []byte(cbom), 0o644)
}
const fs = require("fs");
const pdf_oxide = require("pdf-oxide");

const cbom = pdf_oxide.cryptoCbom(); // CycloneDX 1.6 JSON string
fs.writeFileSync("pdf-cbom.json", cbom);
using System.IO;
using PdfOxide;

string cbom = PdfDocument.CryptoCbom(); // CycloneDX 1.6 JSON string
File.WriteAllText("pdf-cbom.json", cbom);

CBOM shape

Each exercised algorithm becomes one component:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "metadata": {
    "timestamp": "2026-06-22T12:00:00+00:00",
    "tools": { "components": [{ "type": "application", "name": "pdf_oxide", "version": "0.3.69" }] }
  },
  "components": [
    {
      "type": "cryptographic-asset",
      "name": "aes256",
      "bom-ref": "crypto/algorithm/aes256",
      "cryptoProperties": {
        "assetType": "algorithm",
        "algorithmProperties": {
          "primitive": "block-cipher",
          "classicalSecurityLevel": 256,
          "nistQuantumSecurityLevel": 0,
          "certificationLevel": ["fips140-3"]
        }
      }
    }
  ]
}

The primitive maps from the algorithm kind (hash, block-cipher, stream-cipher for RC4, signature, kdf, drbg); certificationLevel is ["fips140-3"] for FIPS-approved algorithms and ["none"] otherwise.

Signatures

Operation Rust Python Go Node.js C#
Inventory crypto::inventory() -> Vec<AlgorithmId> crypto_inventory() -> list[str] CryptoInventory() -> []string cryptoInventory(): string[] CryptoInventory() -> string[]
CBOM crypto::cbom_json() -> String crypto_cbom() -> str CryptoCBOM() -> string cryptoCbom(): string CryptoCbom() -> string

Because providers and policies are both set-once and the default provider installs lazily, configure crypto governance as the very first thing your process does:

import pdf_oxide

# 1. (optional) switch to FIPS — must precede any crypto operation.
if "aws-lc-rs" in pdf_oxide.crypto_available_providers():
    pdf_oxide.crypto_use_fips()

# 2. install the governance policy (fail-closed, set-once).
pdf_oxide.crypto_set_policy("fips-strict")

# 3. ... run your PDF workload ...
doc = pdf_oxide.PdfDocument("encrypted-r6.pdf")
doc.verify_signatures()

# 4. emit governance artifacts.
print("policy:   ", pdf_oxide.crypto_policy())
print("inventory:", pdf_oxide.crypto_inventory())
with open("pdf-cbom.json", "w") as f:
    f.write(pdf_oxide.crypto_cbom())

FAQ

Can I switch the crypto provider back after committing it? No. Both use_fips (provider) and set_policy (policy) are set-once by design — swapping mid-process while in-flight crypto state exists (FIPS self-test, HSM session) is a soundness hazard, and a runtime policy downgrade is an attack vector. Tests that need a fresh provider must run in separate process namespaces.

Why does crypto_active_provider() say "rust-crypto (default, lazy)" before I do anything? That reading is deliberately non-initializing: it reports the default without committing it, so a later crypto_use_fips() can still succeed. The real name appears once a provider is committed.

Is FIPS available in every package? Only when the native library was built with --features fips. Use the parallel -fips distributions (pdf_oxide_fips, pdf-oxide-fips, PdfOxide.Fips, go-fips) which ship single-provider FIPS binaries. On a non-FIPS build, fips_available is false and use_fips errors.

Is the CBOM standard? Yes — it is a valid CycloneDX 1.6 document (bomFormat: "CycloneDX", specVersion: "1.6") so it drops straight into SBOM/CBOM tooling and supply-chain audits.

Does setting a policy slow down PDF processing? No. PDF Oxide stays at its 0.8ms-mean / 100%-pass extraction profile; the inventory is a lock-free atomic bitset and policy checks happen only at crypto enforcement boundaries.

Next Steps