Skip to content

数字签名

v0.3.38 新增。PDF Oxide 从 /AcroForm /Fields → /Sig 读取签名,检查 /Contents 中的 CMS 信封,并针对嵌入的证书运行 RFC 5652 §5.4 signer-attributes 检查,以及针对调用方提供的文档字节运行 §11.2 messageDigest 检查。

范围。 本次发布覆盖的是验证。PDF 的签署(而非验证现有签名)属于 issue #208 剩下的一半,尚未随本版本提供。

验证能力

  • RSA-PKCS#1 v1.5 搭配 SHA-1 / SHA-256 / SHA-384 / SHA-512(实际上几乎所有已签署 PDF 都使用这种填充方式)— 返回 Valid / Invalid
  • RSA-PSSECDSAUnknown / UnsupportedFeatureException 形式呈现。如有需要,调用方仍可通过 Signature.GetCertificate() / .get_certificate() 读取嵌入证书,自行执行验证。
  • 信任根查找、有效期窗口与签名者 DN 会从嵌入证书中提取并附加到验证结果上。

快速示例

Rust

use pdf_oxide::PdfDocument;

let doc = PdfDocument::open("signed.pdf")?;
for sig in doc.signatures() {
    println!("{} → {:?}", sig.signer_name(), sig.verify()?);

    // End-to-end with document bytes
    let pdf_bytes = std::fs::read("signed.pdf")?;
    println!("detached ok = {:?}", sig.verify_detached(&pdf_bytes)?);

    // Cert inspection + trust-root / expiry / signer DN
    let result = pdf_oxide::SignatureVerifier::verify(&sig, &pdf_bytes)?;
    println!("signer DN = {}", result.signer_dn);
}

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("signed.pdf")
for sig in doc.signatures():
    print(sig.signer_name, "→", sig.verify())

# End-to-end with document bytes
with open("signed.pdf", "rb") as f:
    pdf_bytes = f.read()
for sig in doc.signatures():
    print("detached ok =", sig.verify_detached(pdf_bytes))

Node / TypeScript

import { PdfDocument } from "pdf-oxide";
import { readFileSync } from "fs";

const doc = PdfDocument.open("signed.pdf");
for (const sig of doc.signatures()) {
  console.log(sig.signerName, "→", sig.verify());
  console.log("detached ok =", sig.verifyDetached(readFileSync("signed.pdf")));
}

C#

using PdfOxide;

using var doc = PdfDocument.Open("signed.pdf");
foreach (var sig in doc.Signatures)
{
    Console.WriteLine($"{sig.SignerName} → {sig.Verify()}");
    var cert = sig.GetCertificate();
    Console.WriteLine($"subject={cert.Subject} issuer={cert.Issuer} valid={cert.IsValid}");
}

Go(仅 CGo)

import (
    "fmt"
    "os"

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

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

pdfBytes, _ := os.ReadFile("signed.pdf")

sigs, _ := doc.Signatures()
for _, sig := range sigs {
    ok, _ := sig.Verify()
    fmt.Printf("%s → %v\n", sig.SignerName, ok)

    // End-to-end with document bytes
    detachedOk, _ := sig.VerifyDetached(pdfBytes)
    fmt.Println("detached ok =", detachedOk)
}

WASM

import init, { PdfDocument } from "pdf-oxide-wasm/web";
await init();

const doc = new PdfDocument(bytes);
for (const sig of doc.signatures()) {
  console.log(sig.signerName, "→", sig.verify());
}

Signature

枚举并检查签名。支持 Python、WASM、C FFI、C#、Go、Node

属性 / 方法 描述
.signer_name / .SignerName 从签名者证书中提取的 Common Name
.reason / .Reason 签名原因(例如 “I approve this document”)
.location / .Location 地点字段
.contact_info 联系方式字段
.signing_time / .SigningTime 从 CMS 信封提取的 UTC 签名时间(C# 中为 DateTimeOffset?
.verify() / .Verify() 针对嵌入证书运行 RFC 5652 §5.4 signer-attributes 检查,返回 Valid / Invalid / Unknown
.verify_detached(pdf_bytes) / .VerifyDetached(pdfBytes) 追加 RFC 5652 §11.2 messageDigest 检查,用调用方提供的文档字节。
.get_certificate() / .GetCertificate() 返回可供深入检查的 Certificate

Certificate

通过 x509-parser 从 CMS /Contents 中提取的 X.509 证书。支持 C FFI、C#、Node

属性 / 方法 描述
.subject 证书持有者的 DN
.issuer 颁发 CA 的 DN
.serial 序列号(大端字节或字符串)
.not_before 有效期窗口起点(DateTimeOffset
.not_after 有效期窗口终点
.is_valid not_before ≤ now ≤ not_after 则为 true

Timestamp — RFC 3161 TSTInfo

从签名的 TimeStampToken 属性,或从独立的 RFC 3161 响应中解析时间戳 blob。支持 Python、WASM、C FFI、C#、Go

from pdf_oxide import Timestamp

ts = Timestamp.parse(tst_bytes)
print(ts.time, ts.serial, ts.policy_oid, ts.tsa_name, ts.hash_algorithm)

Node / TypeScript

import { Timestamp } from "pdf-oxide";

const ts = Timestamp.parse(tstBytes);
console.log(ts.time, ts.serial, ts.policyOid, ts.tsaName, ts.hashAlgorithm);
ts.close();
属性 描述
.time TSA 断言的 UTC 时间
.serial 本次时间戳的唯一序列号
.policy_oid TSA 策略 OID
.tsa_name TSA 标识
.hash_algorithm 用于 message_imprint 的哈希算法
.message_imprint 被签名负载的哈希
.verify() 针对嵌入的 TSA 证书验证 TSTInfo 签名

TsaClient — RFC 3161 HTTP 客户端

通过 HTTP 向时间戳授权机构请求新的时间戳。在 tsa-client Cargo feature 后面。支持 Python、C FFI、C#、Go(不在 WASM 中 — ureq 与 wasm 不兼容)。

Python

from pdf_oxide import TsaClient

client = TsaClient(
    url="https://freetsa.org/tsr",
    username=None,
    password=None,
    timeout_seconds=30,
    hash_algorithm=2,   # 2 = SHA-256
    use_nonce=True,
    cert_req=True,
)

ts = client.request_timestamp(pdf_bytes)
print(ts.time, ts.serial)

C#

import { TsaClient } from "pdf-oxide";

const client = new TsaClient({
  url: "https://freetsa.org/tsr",
  timeoutSeconds: 30,
  hashAlgorithm: 2,    // 2 = SHA-256
  useNonce: true,
  certReq: true,
});

const ts = client.requestTimestamp(pdfBytes);
console.log(ts.time, ts.serial);
ts.close();
client.close();

C#

var client = new TsaClient("https://freetsa.org/tsr");
var ts = client.RequestTimestamp(pdfBytes);
Console.WriteLine($"{ts.Time} serial={ts.Serial}");

Go

client := pdfoxide.NewTsaClient("https://freetsa.org/tsr")
ts, err := client.RequestTimestamp(pdfBytes)
if err != nil { log.Fatal(err) }
fmt.Println(ts.Time, ts.Serial)

TsaClient 通过 HTTP POST 发送 RFC 3161 TimeStampReq,包含 nonce 与(当设置 username / password 时的)HTTP Basic 认证。响应会被拆包并通过 Timestamp::parse 解析。

绑定覆盖总览

能力 Rust Python Node C# Go WASM
Signature 枚举 + 验证
Certificate 检查
Timestamp 解析 + 验证
TsaClient HTTP 请求

TsaClient × WASM 的 是刻意且永久的设计:ureq 无法编译到 wasm32。如果你需要在浏览器里查看响应,请从服务端绑定调用 TsaClient,再把原始时间戳字节交给 WASM 端的 Timestamp.parse()

相关页面