Skip to content

디지털 서명

v0.3.38에서 추가되었습니다. PDF Oxide는 /AcroForm /Fields → /Sig에서 서명을 읽고, /Contents 안의 CMS 봉투를 검사하며, 내장된 인증서에 대해 RFC 5652 §5.4의 서명자 속성 검사와 호출자가 제공한 문서 바이트에 대해 §11.2의 messageDigest 검사를 수행합니다.

범위. 이 릴리스는 검증만 다룹니다. 기존 서명을 확인하는 것과 달리 PDF에 새 서명을 추가하는 기능은 이슈 #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())

# 문서 바이트까지 포함한 종단 간 검증
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 서명자 속성 검사. 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 응답에서 타임스탬프 블록을 파싱합니다. 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 클라이언트

시간 스탬프 기관(TSA)에 HTTP로 새 타임스탬프를 요청합니다. tsa-client Cargo 기능 뒤에 있으며 Python, C FFI, C#, Go에서 제공됩니다 (ureq가 wasm과 호환되지 않기 때문에 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는 nonce와 HTTP Basic 인증(username / password가 설정된 경우)을 포함한 RFC 3161 TimeStampReq를 HTTP POST로 전송합니다. 응답을 풀어 Timestamp::parse로 파싱합니다.

바인딩 커버리지 요약

영역 Rust Python Node C# Go WASM
Signature 열거 + 검증
Certificate 상세 확인
Timestamp 파싱 + 검증
TsaClient HTTP 요청

TsaClient × WASM 칸의 는 의도적이고 영구적인 선택입니다 — ureqwasm32 로 컴파일되지 않기 때문입니다. 브라우저에서 응답을 살펴봐야 한다면 서버 측 바인딩에서 TsaClient 를 호출한 뒤, 타임스탬프 원본 바이트를 WASM 쪽 Timestamp.parse() 로 넘기세요.

관련 페이지