PDF 加密策略、FIPS 与 CBOM
PDF Oxide 将每一项加密和签名操作都抽象在可插拔的加密提供方之后,并在其上叠加一层失败即关闭(fail-closed)的运行时策略,同时按进程记录它实际行使过的算法清单。该清单可导出为 CycloneDX 1.6 加密物料清单 (CBOM)——这正是受监管的买方和审计人员所要求的机器可读产物。
本页涵盖提供方选择、FIPS 140-3 模式、运行时策略语法、加密清单以及 CBOM 导出。
绑定覆盖范围。 整个加密治理面是进程全局的(不接受文档句柄),并由 Rust、Python、Go、Node.js / TypeScript 和 C# 公开。方法名因绑定而异(各语言惯用的大小写风格)。提供方选择部分(
active_provider/fips_available/use_fips)是来自 issue #236 的 FIPS 接口;策略部分(set_policy/policy/inventory/cbom)是来自 issue #230 的运行时治理接口。只有当本地库以--features fips编译时,FIPS 模式本身才会起作用——否则use_fips会返回/抛出错误,且fips_available为 false。
两个提供方
| 提供方 | 构建 | 算法 | 用途 |
|---|---|---|---|
rust-crypto(默认) |
始终存在 | PDF 规范引用的所有算法,包括用于 ISO 32000-1 R≤4 文档的旧版 MD5 + RC4 路径 | 通用 |
aws-lc-rs |
选择性启用,--features fips |
经 FIPS 140-3 验证;拒绝 MD5、RC4 和 SHA-1 签名 | FIPS 合规 |
默认提供方是惰性安装的——第一次加密操作(打开加密文档、验证签名)会将其确定下来。这就是为什么你必须在进程中发生任何加密工作之前切换到 FIPS。
如何查看当前激活的加密提供方?
active_provider 是非初始化的:如果尚未确定任何提供方,它会返回 "rust-crypto (default, lazy)" 而不锁定默认值,因此后续的 use_fips 调用仍可成功。一旦提供方被确定(显式或惰性),它就会返回真实名称("rust-crypto" 或 "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
签名
| 操作 | Rust | Python | Go | Node.js | C# |
|---|---|---|---|---|---|
| 激活的提供方 | crypto::active().name() -> &'static str |
crypto_active_provider() -> str |
ActiveCryptoProvider() -> string |
getActiveCryptoProvider(): string |
GetActiveCryptoProvider() -> string |
| FIPS 是否可用? | cfg(feature = "fips") |
crypto_available_providers() -> list[str] |
IsFipsCryptoAvailable() -> bool |
isFipsCryptoAvailable(): boolean |
IsFipsCryptoAvailable() -> bool |
它们所封装的 C ABI:char *pdf_oxide_crypto_active_provider(void) 和 int32_t pdf_oxide_crypto_fips_available(void)(如果编译时包含了 FIPS 提供方则返回 1,否则返回 0)。
如何为 PDF 启用 FIPS 模式?
在任何加密操作之前调用 use_fips(在 Rust 中为 set_provider)。它是仅可设置一次的:第二次调用——或在默认提供方被惰性安装之后的任何调用——都会失败。
int32_t pdf_oxide_crypto_use_fips(void) 的 C ABI 错误码:
| 错误码 | 含义 |
|---|---|
0 |
成功 |
1 |
未编译 FIPS 特性 |
2 |
已设置提供方 |
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}");
}
FIPS 会拒绝什么?
PDF Standard Security R≤4(PDF 1.4–1.6 密码加密,ISO 32000-1 §7.6.3)使用 MD5 进行密钥派生,并使用 RC4/AES-128 作为加密算法。FIPS 140-3 禁止 MD5 和 RC4,因此在 aws-lc-rs 下打开此类文档会失败并附带补救信息:
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.
解决方法:在不受 FIPS 限制的环境中以 R=6(PDF 2.0 AES-256,ISO 32000-2 §7.6.4)重新加密。R=6 文档可在 aws-lc-rs 下正常打开。
安装 FIPS 变体。
-fips发行版提供单一提供方的二进制文件(审计人员通常要求如此)。使用pip install pdf_oxide_fips、npm install pdf-oxide-fips、dotnet add package PdfOxide.Fips、go get github.com/yfedoseev/pdf_oxide/go-fips,或用cargo build --features fips构建 Rust。同一发行标签的默认变体和 FIPS 变体在所有非加密路径上都逐字节相同。
如何设置运行时加密策略?
策略会收窄允许使用的算法,与当前激活哪个提供方无关。它是仅可设置一次的(运行时降级是一种攻击途径),并且是失败即关闭的(无法解析的规格会被拒绝,并且不会安装任何策略)。默认值(从不设置时)为 "compat",与引入策略之前的行为逐字节相同。
语法: mode[;clause]*
mode∈compat|strict|fips-strictclause=(allow|deny):<alg>@<read|write>—— 例如"compat;deny:rc4@write;deny:md5@write"或"fips-strict"
int32_t pdf_oxide_crypto_set_policy(const char *spec) 的 C ABI 错误码:
| 错误码 | 含义 |
|---|---|
0 |
成功 |
1 |
无效参数(null / 非 UTF-8 规格) |
2 |
解析错误——规格被拒绝,策略未安装 |
3 |
已设置策略 |
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());
签名
| 操作 | Rust | Python | Go | Node.js | C# |
|---|---|---|---|---|---|
| 设置策略 | crypto::set_policy(SecurityPolicy) -> Result<(), SetPolicyError> |
crypto_set_policy(spec: str) -> None |
SetCryptoPolicy(spec string) -> error |
setCryptoPolicy(spec: string): void |
SetCryptoPolicy(string spec) -> void |
| 读取策略 | crypto::active_policy() -> &'static SecurityPolicy |
crypto_policy() -> str |
CryptoPolicy() -> string |
cryptoPolicy(): string |
CryptoPolicy() -> string |
如何读取加密清单?
清单是本进程到目前为止实际行使过的算法令牌的集合——一个在每个强制边界处记录的无锁原子位集。它是关于“本次运行用了哪些加密?”的最小化报告。令牌是稳定的小写字符串,例如 md5、aes256、rc4、sha256。
C ABI 返回一个以逗号连接的字符串(char *pdf_oxide_crypto_inventory(void),当未行使任何算法时为 "");高级绑定会为你将其拆分为列表/切片/数组。
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);
如何为 PDF 导出 CBOM (CycloneDX)?
crypto_cbom 将清单序列化为一份 CycloneDX 1.6 cryptographic-asset 物料清单,以 JSON 字符串形式输出。该文档始终包含 bomFormat: "CycloneDX"、specVersion: "1.6"、一个 metadata 块(RFC 3339 时间戳 + pdf_oxide 工具组件),以及每个被行使算法对应的一个 cryptographic-asset 组件。空清单会生成一份不含组件的有效 BOM。
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 结构
每个被行使的算法都会成为一个组件:
{
"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"]
}
}
}
]
}
primitive 由算法类别映射而来(hash、block-cipher、用于 RC4 的 stream-cipher、signature、kdf、drbg);对于 FIPS 批准的算法,certificationLevel 为 ["fips140-3"],否则为 ["none"]。
签名
| 操作 | Rust | Python | Go | Node.js | C# |
|---|---|---|---|---|---|
| 清单 | 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 |
推荐的启动顺序
由于提供方和策略都仅可设置一次,且默认提供方是惰性安装的,请将加密治理配置为你的进程最先执行的事情:
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())
常见问题
确定加密提供方后还能切换回来吗? 不能。use_fips(提供方)和 set_policy(策略)按设计都是仅可设置一次的——在存在进行中的加密状态(FIPS 自检、HSM 会话)时于进程中途切换是一种正确性隐患,而运行时策略降级是一种攻击途径。需要全新提供方的测试必须在独立的进程命名空间中运行。
为什么我什么都没做时 crypto_active_provider() 就显示 "rust-crypto (default, lazy)"? 该读取被刻意设计为非初始化的:它报告默认值而不将其确定下来,因此后续的 crypto_use_fips() 仍可成功。真实名称会在提供方被确定后出现。
FIPS 在每个包中都可用吗? 仅当本地库以 --features fips 构建时才可用。请使用提供单一提供方 FIPS 二进制文件的并行 -fips 发行版(pdf_oxide_fips、pdf-oxide-fips、PdfOxide.Fips、go-fips)。在非 FIPS 构建中,fips_available 为 false,且 use_fips 会报错。
CBOM 是标准的吗? 是的——它是一份有效的 CycloneDX 1.6 文档(bomFormat: "CycloneDX"、specVersion: "1.6"),因此可直接接入 SBOM/CBOM 工具链和供应链审计。
设置策略会拖慢 PDF 处理吗? 不会。PDF Oxide 仍保持其 0.8ms 平均 / 100% 通过的提取特性;清单是一个无锁原子位集,策略检查仅在加密强制边界处发生。