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— otherwiseuse_fipsreturns/raises an error andfips_availableis 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
-fipsdistributions ship single-provider binaries (auditors usually require this). Usepip 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 withcargo 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]*
mode∈compat|strict|fips-strictclause=(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 |
Recommended startup sequence
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
- Digital Signatures — sign and verify PDFs (the main consumer of crypto policy)
- PDF/A Validation — archival compliance (rejects encryption)
- API Reference — complete Rust API