Skip to content

Цифрові підписи

Додано у v0.3.38. PDF Oxide читає підписи з /AcroForm /Fields → /Sig, інспектує CMS-конверт усередині /Contents та виконує перевірку signer-attributes за RFC 5652 §5.4 проти вбудованого сертифіката, а також перевірку §11.2 messageDigest проти байтів документа, які передав викликач.

Обсяг. Ця версія покриває верифікацію. Підписування PDF (на противагу до перевірки наявних підписів) залишається частиною issue #208, що ще не реалізована.

Що перевіряється

  • RSA-PKCS#1 v1.5 поверх SHA-1 / SHA-256 / SHA-384 / SHA-512 — padding, яким фактично підписана вся маса PDF «у дикій природі» — повертає Valid / Invalid.
  • RSA-PSS і ECDSA проявляються як Unknown / 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 UTC-час підписання з CMS-конверта (DateTimeOffset? у C#)
.verify() / .Verify() Перевірка signer-attributes за RFC 5652 §5.4 проти вбудованого сертифіката. Повертає Valid / Invalid / Unknown.
.verify_detached(pdf_bytes) / .VerifyDetached(pdfBytes) Додає перевірку messageDigest за RFC 5652 §11.2 проти байтів документа, переданих викликачем.
.get_certificate() / .GetCertificate() Повертає Certificate для глибшої інспекції.

Certificate

Сертифікат X.509, витягнутий з блоба CMS /Contents через x509-parser. Доступний у C FFI, C#, Node.

Властивість / метод Опис
.subject Distinguished Name власника сертифіката
.issuer DN центру сертифікації-видавця
.serial Серійний номер (big-endian байти або рядок)
.not_before Початок вікна валідності (DateTimeOffset)
.not_after Кінець вікна валідності
.is_valid True, якщо not_before ≤ now ≤ not_after

Timestamp — RFC 3161 TSTInfo

Розбирає timestamp-блоб з атрибута TimeStampToken підпису або з окремої відповіді RFC 3161. Доступний у 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 UTC-час, заявлений TSA
.serial Унікальний серійний номер цієї мітки часу
.policy_oid OID політики TSA
.tsa_name Ідентифікатор TSA
.hash_algorithm Хеш-алгоритм для message_imprint
.message_imprint Хеш підписаного payload’у
.verify() Перевіряє підпис TSTInfo проти вбудованого TSA-сертифіката

TsaClient — HTTP-клієнт RFC 3161

Запитує свіжу мітку часу в Time Stamp Authority через HTTP. За Cargo-фічею tsa-client. Доступний у 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 надсилає RFC 3161 TimeStampReq через HTTP POST з nonce та HTTP Basic auth (коли задано username / password). Відповідь розгортається і проходить через Timestamp::parse.

Зведення покриття прив’язок

Поверхня Rust Python Node C# Go WASM
Signature перелік + verify
Certificate інспекція
Timestamp parse + verify
TsaClient HTTP-запит

Значення у клітинці TsaClient × WASM — свідоме й остаточне рішення: ureq не компілюється у wasm32. Якщо треба переглянути відповідь у браузері, викликайте TsaClient із серверної прив’язки, а сирі байти міток часу передавайте у Timestamp.parse() на боці WASM.

Пов’язані сторінки