Skip to content

PDF 加密策略、FIPS 与 CBOM

PDF Oxide 将每一项加密和签名操作都抽象在可插拔的加密提供方之后,并在其上叠加一层失败即关闭(fail-closed)的运行时策略,同时按进程记录它实际行使过的算法清单。该清单可导出为 CycloneDX 1.6 加密物料清单 (CBOM)——这正是受监管的买方和审计人员所要求的机器可读产物。

本页涵盖提供方选择、FIPS 140-3 模式、运行时策略语法、加密清单以及 CBOM 导出。

绑定覆盖范围。 整个加密治理面是进程全局的(不接受文档句柄),并由 RustPythonGoNode.js / TypeScriptC# 公开。方法名因绑定而异(各语言惯用的大小写风格)。提供方选择部分(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_fipsnpm install pdf-oxide-fipsdotnet add package PdfOxide.Fipsgo get github.com/yfedoseev/pdf_oxide/go-fips,或用 cargo build --features fips 构建 Rust。同一发行标签的默认变体和 FIPS 变体在所有非加密路径上都逐字节相同。

如何设置运行时加密策略?

策略会收窄允许使用的算法,与当前激活哪个提供方无关。它是仅可设置一次的(运行时降级是一种攻击途径),并且是失败即关闭的(无法解析的规格会被拒绝,并且不会安装任何策略)。默认值(从不设置时)为 "compat",与引入策略之前的行为逐字节相同。

语法: mode[;clause]*

  • modecompat | strict | fips-strict
  • clause = (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

如何读取加密清单?

清单是本进程到目前为止实际行使过的算法令牌的集合——一个在每个强制边界处记录的无锁原子位集。它是关于“本次运行用了哪些加密?”的最小化报告。令牌是稳定的小写字符串,例如 md5aes256rc4sha256

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 由算法类别映射而来(hashblock-cipher、用于 RC4 的 stream-ciphersignaturekdfdrbg);对于 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_fipspdf-oxide-fipsPdfOxide.Fipsgo-fips)。在非 FIPS 构建中,fips_available 为 false,且 use_fips 会报错。

CBOM 是标准的吗? 是的——它是一份有效的 CycloneDX 1.6 文档(bomFormat: "CycloneDX"specVersion: "1.6"),因此可直接接入 SBOM/CBOM 工具链和供应链审计。

设置策略会拖慢 PDF 处理吗? 不会。PDF Oxide 仍保持其 0.8ms 平均 / 100% 通过的提取特性;清单是一个无锁原子位集,策略检查仅在加密强制边界处发生。

后续步骤