Skip to content

PDF 서명하기: 디지털 서명과 PAdES

v0.3.50(issue #235)에서 추가되었습니다. PDF Oxide는 CMS/PKCS#7 분리형 서명을 /Contents에 임베드하고 /ByteRange를 채워 PDF에 서명하며, PAdES의 경우 증분 업데이트를 통해 RFC 3161 타임스탬프와 문서 보안 저장소(DSS)를 추가합니다. 이 페이지는 생성 측면을 다룹니다. 기존 서명을 검사하거나 검증하려면 디지털 서명(추출)을 참조하세요.

기능 게이트. 서명에는 signatures Cargo 기능이 필요합니다. PAdES-B-T와 B-LT에는 RFC 3161 타임스탬프 소스를 위해 추가로 tsa-client 기능이 필요합니다. 바인딩이 이러한 기능 없이 빌드된 경우, 서명 호출은 해당 바인딩의 “구현되지 않음” 오류를 발생시킵니다.

바인딩 지원 현황

서명은 C ABI의 pdf_sign_bytes*pdf_certificate_load_* 계열을 래핑하는 바인딩에서 제공됩니다: Rust, Python, Go(CGo), C#, Swift. Node와 WASM은 읽기/검증 측면만 제공합니다 — 페이지 빌더에 서명되지 않은 /Sig 플레이스홀더 필드를 제공하지만 CMS 서명자는 제공하지 않습니다. 서명된 바이트를 생성하려면 서버 측 바인딩을 호출하세요.

기능 Rust Python Go C# Swift Node WASM
자격 증명 불러오기(PEM / PKCS#12)
sign_pdf_bytes(CMS 분리형)
sign_pdf_bytes_pades(B-B / B-T / B-LT)
DSS(/DSS) 개수 읽기

는 의도적입니다: Node/WASM에는 CMS 서명자가 연결되어 있지 않습니다.

서명용 자격 증명은 어떻게 불러오나요?

서명자에게는 인증서 그에 맞는 개인 키가 필요합니다. PKCS#12(.p12 / .pfx) 블롭에서 또는 별도의 PEM 문자열에서 불러올 수 있습니다.

  • SigningCredentials::from_pkcs12(data: &[u8], password: &str) / from_pem(cert_pem: &str, key_pem: &str)(Rust)
  • Certificate.load_pkcs12(data: bytes, password: str) / Certificate.load_pem(cert_pem: str, key_pem: str)(Python)
  • LoadCertificate(data []byte, password string) / LoadCertificateFromPem(certPem, keyPem string)(Go)
  • Certificate.Load(byte[] data, string? password) / Certificate.LoadFromPem(string certPem, string keyPem)(C#)
  • Certificate.loadFromBytes(_ bytes: [UInt8], password: String) / Certificate.loadFromPem(certPem:keyPem:)(Swift)

불러오고 나면 동일한 핸들에서 임베드된 인증서의 subject, issuer, serial, validity, is_valid에 접근할 수 있습니다 — 추출 페이지의 Certificate 표를 참조하세요.

PDF에는 어떻게 서명하나요(CMS/PKCS#7)?

sign_pdf_bytes는 레거시 adbe.pkcs7.detached 서명을 생성합니다: 문서 바이트 범위에 대한 CMS 봉투이며, 선택적인 /Reason/Location 항목이 서명 딕셔너리에 기록됩니다.

Rust

use pdf_oxide::signatures::{sign_pdf_bytes, SignOptions, SigningCredentials};

let creds = SigningCredentials::from_pkcs12(&std::fs::read("id.p12")?, "testpass")?;
println!("signing as {}", creds.subject()?);

let pdf = std::fs::read("invoice.pdf")?;
let opts = SignOptions {
    reason: Some("Approved".into()),
    location: Some("HQ".into()),
    ..Default::default()
};
let signed = sign_pdf_bytes(&pdf, &creds, opts)?;
std::fs::write("invoice-signed.pdf", &signed)?;
assert!(signed.windows(10).any(|w| w == b"/ByteRange"));

Python

import pdf_oxide

with open("id.p12", "rb") as f:
    cert = pdf_oxide.Certificate.load_pkcs12(f.read(), "testpass")
print("signing as", cert.subject)

with open("invoice.pdf", "rb") as f:
    pdf_bytes = f.read()

signed = pdf_oxide.sign_pdf_bytes(pdf_bytes, cert, reason="Approved", location="HQ")
with open("invoice-signed.pdf", "wb") as f:
    f.write(signed)
assert b"/ByteRange" in signed

Go(CGo 전용)

import (
    "fmt"
    "os"

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

p12, _ := os.ReadFile("id.p12")
cert, err := pdfoxide.LoadCertificate(p12, "testpass")
if err != nil {
    panic(err)
}
defer cert.Close()

pdf, _ := os.ReadFile("invoice.pdf")
signed, err := pdfoxide.SignPdfBytes(pdf, cert, "Approved", "HQ")
if err != nil {
    panic(err)
}
os.WriteFile("invoice-signed.pdf", signed, 0o644)
fmt.Println("signed", len(signed), "bytes")

C#

using PdfOxide;

var cert = Certificate.Load(File.ReadAllBytes("id.p12"), "testpass");
Console.WriteLine($"signing as {cert.Subject}");

byte[] pdf = File.ReadAllBytes("invoice.pdf");
byte[] signed = cert.SignPdfBytes(pdf, reason: "Approved", location: "HQ");
File.WriteAllBytes("invoice-signed.pdf", signed);

Swift

import PdfOxide

let cert = try Certificate.loadFromBytes(Array(try Data(contentsOf: URL(fileURLWithPath: "id.p12"))), password: "testpass")
let pdf = Array(try Data(contentsOf: URL(fileURLWithPath: "invoice.pdf")))
let signed = try signBytes(pdf, certificate: cert, reason: "Approved", location: "HQ")
try Data(signed).write(to: URL(fileURLWithPath: "invoice-signed.pdf"))

Java

import fyi.oxide.pdf.PdfSigner;
import fyi.oxide.pdf.signature.SignOptions;
import fyi.oxide.pdf.signature.SignatureLevel;
import java.nio.file.*;

PdfSigner signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass");
byte[] pdf = Files.readAllBytes(Path.of("invoice.pdf"));

byte[] signed = signer.sign(pdf, SignOptions.builder()
        .withLevel(SignatureLevel.B_B)      // plain CMS / PKCS#7
        .withReason("Approved")
        .withLocation("HQ")
        .build());
Files.write(Path.of("invoice-signed.pdf"), signed);

PHP

use PdfOxide\PdfSigner;

$signer = PdfSigner::fromPkcs12('id.p12', 'testpass');
$pdf    = file_get_contents('invoice.pdf');

$signed = $signer->sign(
    $pdf,
    level:    PdfSigner::LEVEL_B_B,   // plain CMS / PKCS#7
    reason:   'Approved',
    location: 'HQ',
);
file_put_contents('invoice-signed.pdf', $signed);
$signer->close();

Ruby

require 'pdf_oxide'

# certificate_handle comes from your PKCS#12 / PEM credentials API
signer = PdfOxide::PdfSigner.new(certificate_handle)
pdf    = File.binread('invoice.pdf')

signed = signer.sign(pdf, level: :b, reason: 'Approved', location: 'HQ')
File.binwrite('invoice-signed.pdf', signed)

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto cert = pdf_oxide::Certificate::load_from_bytes(read_bytes("id.p12"), "testpass");
auto pdf  = read_bytes("invoice.pdf");

auto signed = pdf_oxide::sign_bytes(pdf, cert, "Approved", "HQ");
write_bytes("invoice-signed.pdf", signed);

Kotlin

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.SignOptions
import fyi.oxide.pdf.signature.SignatureLevel
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
    .withLevel(SignatureLevel.B_B)          // plain CMS / PKCS#7
    .withReason("Approved")
    .withLocation("HQ")
    .build())
Files.write(Path.of("invoice-signed.pdf"), signed)

Dart

import 'dart:io';
import 'package:pdf_oxide/pdf_oxide.dart';

final cert = Certificate.loadFromBytes(File('id.p12').readAsBytesSync(), 'testpass');
final pdf  = File('invoice.pdf').readAsBytesSync();

final signed = signBytes(pdf, cert, reason: 'Approved', location: 'HQ');
File('invoice-signed.pdf').writeAsBytesSync(signed);

R

library(pdfoxide)

cert <- pdf_certificate_load_from_bytes(readBin("id.p12", "raw", file.size("id.p12")), "testpass")
pdf  <- readBin("invoice.pdf", "raw", file.size("invoice.pdf"))

signed <- pdf_sign_bytes(pdf, cert, reason = "Approved", location = "HQ")
writeBin(signed, "invoice-signed.pdf")

Julia

using PdfOxide

cert = certificate_load_from_bytes(read("id.p12"), "testpass")
pdf  = read("invoice.pdf")

signed = sign_bytes(pdf, cert, "Approved", "HQ")
write("invoice-signed.pdf", signed)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

const cert = try pdf_oxide.Certificate.loadFromBytes(p12_bytes, "testpass");
const signed = try pdf_oxide.signBytes(a, pdf_bytes, cert, "Approved", "HQ");
defer a.free(signed);
try std.fs.cwd().writeFile(.{ .sub_path = "invoice-signed.pdf", .data = signed });

Scala

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.{SignOptions, SignatureLevel}
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
  .withLevel(SignatureLevel.B_B)            // plain CMS / PKCS#7
  .withReason("Approved")
  .withLocation("HQ")
  .build())
Files.write(Path.of("invoice-signed.pdf"), signed)

Clojure

(import '[fyi.oxide.pdf PdfSigner]
        '[fyi.oxide.pdf.signature SignOptions SignatureLevel])
(require '[clojure.java.io :as io])

(let [signer (PdfSigner/fromPkcs12 (java.nio.file.Path/of "id.p12" (make-array String 0)) "testpass")
      pdf    (java.nio.file.Files/readAllBytes (java.nio.file.Path/of "invoice.pdf" (make-array String 0)))
      opts   (-> (SignOptions/builder)
                 (.withLevel SignatureLevel/B_B)   ; plain CMS / PKCS#7
                 (.withReason "Approved")
                 (.withLocation "HQ")
                 (.build))
      signed (.sign signer pdf opts)]
  (java.nio.file.Files/write (java.nio.file.Path/of "invoice-signed.pdf" (make-array String 0)) signed
                             (make-array java.nio.file.OpenOption 0)))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXCertificate *cert = [POXCertificate loadFromBytes:p12Data password:@"testpass" error:&err];
NSData *pdf = [NSData dataWithContentsOfFile:@"invoice.pdf"];

NSData *signed = [POXSigning signBytes:pdf certificate:cert reason:@"Approved" location:@"HQ" error:&err];
[signed writeToFile:@"invoice-signed.pdf" atomically:YES];

Elixir

{:ok, cert} = PdfOxide.certificate_from_bytes(File.read!("id.p12"), "testpass")
pdf = File.read!("invoice.pdf")

{:ok, signed} = PdfOxide.sign_bytes(pdf, cert, "Approved", "HQ")
File.write!("invoice-signed.pdf", signed)

문서를 빌드하면서 한 번에 서명할 수도 있습니다 — DocumentBuilder().…​.build()는 서명되지 않은 바이트를 반환하며, 이를 그대로 sign_pdf_bytes에 전달합니다. DocumentBuilder를 참조하세요.

PAdES 베이스라인 레벨이란 무엇인가요?

PAdES(PDF Advanced Electronic Signatures, ETSI EN 319 142)는 CMS 서명 위에 장기 검증 계층을 쌓습니다. PDF Oxide는 ETSI.CAdES.detached 서명자를 제공하며, 그 레벨 매핑은 C ABI와 모든 바인딩에서 공유되는 고정된 값입니다:

레벨 코드 추가되는 것
B-B 0 CAdES 베이스라인: ESS signing-certificate-v2(RFC 5035)를 포함한 서명된 속성.
B-T 1 B-B + 서명 값에 대한 RFC 3161 signature-time-stamp 비서명 속성.
B-LT 2 B-T + 문서 보안 저장소(인증서 / CRL / OCSP + 서명별 VRI).
B-LTA 3 B-LT + 문서 단위 /DocTimeStamp. 서명용으로 예약됨 — 서명자에게 레벨 3을 전달하면 Unsupported를 반환합니다. 기존 B-LTA 파일은 아래의 문서 타임스탬프 리더 신호로 감지하세요.

B-T, B-LT, B-LTA에는 타임스탬프 기관이 필요합니다. B-TB-LT의 경우 TSA URL을 지정하면 바인딩이 RFC 3161 토큰을 가져옵니다. 타임스탬프 기관 없이 서명하면 Unsupported로 **안전하게 실패(fail closed)**합니다.

PAdES B-LT 서명은 어떻게 생성하나요?

sign_pdf_bytes_pades는 레벨, 선택적 TSA URL, 그리고 B-LT DSS가 되는 폐기 자료(DER 인코딩된 인증서, CRL, OCSP 응답)를 받습니다.

Rust

use pdf_oxide::signatures::{
    sign_pdf_bytes_pades, PadesLevel, RevocationMaterial, SignOptions, SigningCredentials,
};
use pdf_oxide::signatures::{TsaClient, TsaClientConfig};

let creds = SigningCredentials::from_pkcs12(&std::fs::read("id.p12")?, "testpass")?;
let pdf = std::fs::read("invoice.pdf")?;

// RFC 3161 token source for B-T / B-LT.
let tsa = TsaClient::new(TsaClientConfig::new("https://freetsa.org/tsr".into()));
let timestamper = |sig: &[u8]| tsa.request_timestamp(sig).map(|t| t.token_bytes().to_vec());

let material = RevocationMaterial {
    certificates: vec![std::fs::read("ca.der")?],
    ..Default::default()
};

let signed = sign_pdf_bytes_pades(
    &pdf,
    &creds,
    SignOptions { reason: Some("Approved".into()), ..Default::default() },
    PadesLevel::BLt,
    Some(&timestamper),
    &material,
)?;
std::fs::write("invoice-pades.pdf", &signed)?;

Python

import pdf_oxide
from pdf_oxide import PadesLevel, RevocationMaterial

with open("id.p12", "rb") as f:
    cert = pdf_oxide.Certificate.load_pkcs12(f.read(), "testpass")
with open("invoice.pdf", "rb") as f:
    pdf_bytes = f.read()
with open("ca.der", "rb") as f:
    ca_der = f.read()

signed = pdf_oxide.sign_pdf_bytes_pades(
    pdf_bytes,
    cert,
    PadesLevel.B_LT,
    tsa_url="https://freetsa.org/tsr",
    reason="Approved",
    revocation=RevocationMaterial(certs=[ca_der]),
)
with open("invoice-pades.pdf", "wb") as f:
    f.write(signed)

Go(CGo 전용)

ca, _ := os.ReadFile("ca.der")
opts := pdfoxide.PAdESOptions{
    Level:      pdfoxide.PAdESBLt,
    TSAURL:     "https://freetsa.org/tsr",
    Reason:     "Approved",
    Revocation: &pdfoxide.RevocationMaterial{Certs: [][]byte{ca}},
}
signed, err := pdfoxide.SignPdfBytesPAdES(pdf, cert, opts)
if err != nil {
    panic(err)
}
os.WriteFile("invoice-pades.pdf", signed, 0o644)

C#

var material = new RevocationMaterial();
material.Certificates.Add(File.ReadAllBytes("ca.der"));

byte[] signed = cert.SignPdfBytesPades(pdf, new PadesSignOptions
{
    Level = PadesLevel.BLt,
    TsaUrl = "https://freetsa.org/tsr",
    Reason = "Approved",
    Revocation = material,
});
File.WriteAllBytes("invoice-pades.pdf", signed);

Swift

let ca = Array(try Data(contentsOf: URL(fileURLWithPath: "ca.der")))
let signed = try signBytesPades(
    pdf,
    certificate: cert,
    level: 2,                              // 0=B-B 1=B-T 2=B-LT
    tsaUrl: "https://freetsa.org/tsr",
    reason: "Approved",
    certs: [ca]
)
try Data(signed).write(to: URL(fileURLWithPath: "invoice-pades.pdf"))

Java

import fyi.oxide.pdf.PdfSigner;
import fyi.oxide.pdf.signature.SignOptions;
import fyi.oxide.pdf.signature.SignatureLevel;
import java.nio.file.*;

PdfSigner signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass");
byte[] pdf = Files.readAllBytes(Path.of("invoice.pdf"));

byte[] signed = signer.sign(pdf, SignOptions.builder()
        .withLevel(SignatureLevel.B_LT)           // B-T / B-LT require a TSA
        .withTsaUrl("https://freetsa.org/tsr")
        .withReason("Approved")
        .build());
Files.write(Path.of("invoice-pades.pdf"), signed);

PHP

use PdfOxide\PdfSigner;

$signer = PdfSigner::fromPkcs12('id.p12', 'testpass');
$pdf    = file_get_contents('invoice.pdf');

$signed = $signer->sign(
    $pdf,
    level:  PdfSigner::LEVEL_B_LT,             // B-T / B-LT require a TSA
    tsaUrl: 'https://freetsa.org/tsr',
    reason: 'Approved',
);
file_put_contents('invoice-pades.pdf', $signed);
$signer->close();

Ruby

require 'pdf_oxide'

signer = PdfOxide::PdfSigner.new(certificate_handle)
pdf    = File.binread('invoice.pdf')

signed = signer.sign(
  pdf,
  level:   :lt,                               # B-T / B-LT require a TSA
  tsa_url: 'https://freetsa.org/tsr',
  reason:  'Approved',
)
File.binwrite('invoice-pades.pdf', signed)

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto cert = pdf_oxide::Certificate::load_from_bytes(read_bytes("id.p12"), "testpass");
auto pdf  = read_bytes("invoice.pdf");

pdf_oxide::RevocationMaterial material;
material.certs.push_back(read_bytes("ca.der"));

auto signed = pdf_oxide::sign_bytes_pades(
    pdf, cert,
    /*level=*/2,                              // 0=B-B 1=B-T 2=B-LT
    "https://freetsa.org/tsr",
    "Approved", /*location=*/"",
    material);
write_bytes("invoice-pades.pdf", signed);

Kotlin

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.SignOptions
import fyi.oxide.pdf.signature.SignatureLevel
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
    .withLevel(SignatureLevel.B_LT)               // B-T / B-LT require a TSA
    .withTsaUrl("https://freetsa.org/tsr")
    .withReason("Approved")
    .build())
Files.write(Path.of("invoice-pades.pdf"), signed)

Dart

import 'dart:io';
import 'package:pdf_oxide/pdf_oxide.dart';

final cert = Certificate.loadFromBytes(File('id.p12').readAsBytesSync(), 'testpass');
final pdf  = File('invoice.pdf').readAsBytesSync();
final ca   = File('ca.der').readAsBytesSync();

final signed = signBytesPades(
  pdf, cert,
  2,                                          // 0=B-B 1=B-T 2=B-LT
  tsaUrl: 'https://freetsa.org/tsr',
  reason: 'Approved',
  certs: [ca],
);
File('invoice-pades.pdf').writeAsBytesSync(signed);

R

library(pdfoxide)

cert <- pdf_certificate_load_from_bytes(readBin("id.p12", "raw", file.size("id.p12")), "testpass")
pdf  <- readBin("invoice.pdf", "raw", file.size("invoice.pdf"))
ca   <- readBin("ca.der", "raw", file.size("ca.der"))

signed <- pdf_sign_bytes_pades(
  pdf, cert,
  level = 2L,                                 # 0=B-B 1=B-T 2=B-LT
  tsa_url = "https://freetsa.org/tsr",
  reason = "Approved",
  certs = list(ca)
)
writeBin(signed, "invoice-pades.pdf")

Julia

using PdfOxide

cert = certificate_load_from_bytes(read("id.p12"), "testpass")
pdf  = read("invoice.pdf")
ca   = read("ca.der")

signed = sign_bytes_pades(
    pdf, cert,
    2,                                        # 0=B-B 1=B-T 2=B-LT
    "https://freetsa.org/tsr",
    "Approved", "";
    certs = [ca],
)
write("invoice-pades.pdf", signed)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

const cert = try pdf_oxide.Certificate.loadFromBytes(p12_bytes, "testpass");
const signed = try pdf_oxide.signBytesPades(
    a, pdf_bytes, cert,
    2,                                        // 0=B-B 1=B-T 2=B-LT
    "https://freetsa.org/tsr",
    "Approved", "",
    &[_][]const u8{ca_der}, &.{}, &.{},
);
defer a.free(signed);
try std.fs.cwd().writeFile(.{ .sub_path = "invoice-pades.pdf", .data = signed });

Scala

import fyi.oxide.pdf.PdfSigner
import fyi.oxide.pdf.signature.{SignOptions, SignatureLevel}
import java.nio.file.*

val signer = PdfSigner.fromPkcs12(Path.of("id.p12"), "testpass")
val pdf = Files.readAllBytes(Path.of("invoice.pdf"))

val signed = signer.sign(pdf, SignOptions.builder()
  .withLevel(SignatureLevel.B_LT)             // B-T / B-LT require a TSA
  .withTsaUrl("https://freetsa.org/tsr")
  .withReason("Approved")
  .build())
Files.write(Path.of("invoice-pades.pdf"), signed)

Clojure

(import '[fyi.oxide.pdf PdfSigner]
        '[fyi.oxide.pdf.signature SignOptions SignatureLevel])

(let [signer (PdfSigner/fromPkcs12 (java.nio.file.Path/of "id.p12" (make-array String 0)) "testpass")
      pdf    (java.nio.file.Files/readAllBytes (java.nio.file.Path/of "invoice.pdf" (make-array String 0)))
      opts   (-> (SignOptions/builder)
                 (.withLevel SignatureLevel/B_LT)    ; B-T / B-LT require a TSA
                 (.withTsaUrl "https://freetsa.org/tsr")
                 (.withReason "Approved")
                 (.build))
      signed (.sign signer pdf opts)]
  (java.nio.file.Files/write (java.nio.file.Path/of "invoice-pades.pdf" (make-array String 0)) signed
                             (make-array java.nio.file.OpenOption 0)))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXCertificate *cert = [POXCertificate loadFromBytes:p12Data password:@"testpass" error:&err];
NSData *pdf = [NSData dataWithContentsOfFile:@"invoice.pdf"];
NSData *ca  = [NSData dataWithContentsOfFile:@"ca.der"];

NSData *signed = [POXSigning signBytesPades:pdf
                               certificate:cert
                                     level:2           // 0=B-B 1=B-T 2=B-LT
                                    tsaUrl:@"https://freetsa.org/tsr"
                                    reason:@"Approved"
                                  location:nil
                                     certs:@[ca]
                                      crls:@[]
                                     ocsps:@[]
                                     error:&err];
[signed writeToFile:@"invoice-pades.pdf" atomically:YES];

Elixir

{:ok, cert} = PdfOxide.certificate_from_bytes(File.read!("id.p12"), "testpass")
pdf = File.read!("invoice.pdf")
ca  = File.read!("ca.der")

{:ok, signed} =
  PdfOxide.sign_bytes_pades(pdf, cert, 2, "https://freetsa.org/tsr",   # 0=B-B 1=B-T 2=B-LT
    reason: "Approved",
    certs: [ca]
  )
File.write!("invoice-pades.pdf", signed)

타임스탬프 없는 간단한 B-B 서명을 만들려면 레벨 0 / PadesLevel.B_B를 전달하고 TSA URL을 생략하세요.

생성한 문서 보안 저장소는 어떻게 검사하나요?

B-LT 서명 후, PDF를 다시 열어 그 /DSS를 읽어 검증 자료가 들어갔는지 확인하세요. 문서에 DSS가 없으면 dss()None/null을 반환합니다 — 이는 오류가 아닙니다.

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::signatures::read_dss;

let doc = PdfDocument::open("invoice-pades.pdf")?;
if let Some(dss) = read_dss(&doc)? {
    println!("certs={} crls={} ocsps={} vri={}",
        dss.certificates.len(), dss.crls.len(), dss.ocsp_responses.len(), dss.vri.len());
}

Python

import pdf_oxide

doc = pdf_oxide.PdfDocument("invoice-pades.pdf")
dss = doc.dss()
if dss is not None:
    print("certs", len(dss.certs), "crls", len(dss.crls),
          "ocsps", len(dss.ocsps), "vri", len(dss.vri))

Go(CGo 전용)

doc, _ := pdfoxide.Open("invoice-pades.pdf")
defer doc.Close()

dss, _ := doc.DSS()           // nil when the PDF has no /DSS
if dss != nil {
    fmt.Printf("certs=%d crls=%d ocsps=%d vri=%d\n",
        len(dss.Certs), len(dss.CRLs), len(dss.OCSPs), dss.VRICount)
}

C#

using var doc = PdfDocument.Open("invoice-pades.pdf");
var dss = doc.GetDss();        // null when the PDF has no /DSS
if (dss is not null)
{
    Console.WriteLine($"certs={dss.Certificates.Count} crls={dss.Crls.Count} " +
                      $"ocsps={dss.OcspResponses.Count} vri={dss.VriCount}");
}

Swift

let doc = try Document.open("invoice-pades.pdf")
if let dss = try doc.dss() {
    print("certs=\(try dss.certCount()) crls=\(try dss.crlCount()) " +
          "ocsps=\(try dss.ocspCount()) vri=\(try dss.vriCount())")
}

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("invoice-pades.pdf");
auto dss = doc.get_dss();          // throws if the PDF has no /DSS
std::cout << "certs=" << dss.cert_count() << " crls=" << dss.crl_count()
          << " ocsps=" << dss.ocsp_count() << " vri=" << dss.vri_count() << "\n";

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('invoice-pades.pdf');
final dss = doc.getDss();          // throws if the PDF has no /DSS
print('certs=${dss.certCount} crls=${dss.crlCount} '
      'ocsps=${dss.ocspCount} vri=${dss.vriCount}');

R

library(pdfoxide)

doc <- pdf_open("invoice-pades.pdf")
dss <- pdf_get_dss(doc)            # errors if the PDF has no /DSS
cat(sprintf("certs=%d crls=%d ocsps=%d vri=%d\n",
            pdf_dss_cert_count(dss), pdf_dss_crl_count(dss),
            pdf_dss_ocsp_count(dss), pdf_dss_vri_count(dss)))

Julia

using PdfOxide

doc = open_document("invoice-pades.pdf")
dss = document_get_dss(doc)        # errors if the PDF has no /DSS
println("certs=$(dss_cert_count(dss)) crls=$(dss_crl_count(dss)) ",
        "ocsps=$(dss_ocsp_count(dss)) vri=$(dss_vri_count(dss))")

Zig

const pdf_oxide = @import("pdf_oxide");

var doc = try pdf_oxide.Document.open("invoice-pades.pdf");
const dss = try doc.dss();         // errors if the PDF has no /DSS
std.debug.print("certs={d} crls={d} ocsps={d} vri={d}\n", .{
    try dss.certCount(), try dss.crlCount(), try dss.ocspCount(), try dss.vriCount(),
});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"invoice-pades.pdf" error:&err];
POXDss *dss = [doc dssWithError:&err];     // nil + error when the PDF has no /DSS
if (dss != nil) {
    NSLog(@"certs=%d crls=%d ocsps=%d vri=%d",
          [dss certCount], [dss crlCount], [dss ocspCount], [dss vriCount]);
}

Elixir

{:ok, doc} = PdfOxide.open("invoice-pades.pdf")

case PdfOxide.document_dss(doc) do      # {:error, _} when the PDF has no /DSS
  {:ok, dss} ->
    IO.puts("certs=#{PdfOxide.dss_cert_count(dss)} crls=#{PdfOxide.dss_crl_count(dss)} " <>
            "ocsps=#{PdfOxide.dss_ocsp_count(dss)} vri=#{PdfOxide.dss_vri_count(dss)}")
  _ -> :no_dss
end

DSS 접근자

기능(C ABI / Swift) Python / Go / C# 동등 표현 설명
dss.cert_count() len(dss.certs) / len(dss.Certs) / dss.Certificates.Count 문서 단위 DER 인증서(/Certs)
dss.crl_count() len(dss.crls) / len(dss.CRLs) / dss.Crls.Count 문서 단위 DER CRL(/CRLs)
dss.ocsp_count() len(dss.ocsps) / len(dss.OCSPs) / dss.OcspResponses.Count 문서 단위 DER OCSP 응답(/OCSPs)
dss.vri_count() len(dss.vri) / dss.VRICount / dss.VriCount 서명별 /VRI 항목(/Contents의 대문자 16진수 SHA-1)

C ABI / Swift는 개수 메서드와 함께 인덱스별 게터(pdf_dss_get_cert 등)를 제공합니다. Python과 Go는 DER 블롭을 리스트로 직접 반환합니다.

B-LTA 보관용 타임스탬프는 어떻게 감지하나요?

Signature.pades_level은 서명 범위이며 설계상 B-LT가 상한입니다. B-LTA는 문서 단위 /DocTimeStamp를 추가하는데, 이는 문서 전체의 신호이므로 문서 타임스탬프 검사로 읽습니다:

import pdf_oxide

with open("archive.pdf", "rb") as f:
    is_lta = pdf_oxide.has_document_timestamp(f.read())   # -> bool
  • Rust: pdf_oxide::signatures::has_document_timestamp(pdf_data: &[u8]) -> bool
  • Go: doc.HasDocumentTimestamp() (bool, error)
  • C#: doc.HasDocumentTimestamp() -> bool
  • Swift: try doc.hasTimestamp() -> Bool

성능

PDF Oxide의 추출 코어는 참조 코퍼스에서 **평균 0.8 ms / p99 9 ms, 통과율 100%**로 동작합니다(v0.3.8 벤치마크). 서명 비용은 CMS/RSA 연산이 지배적이며, B-T/B-LT의 경우 여기에 TSA로의 네트워크 왕복이 더해집니다 — PDF 바이트 범위 해싱과 증분 업데이트 기록이 그 위에 더하는 오버헤드는 미미합니다.

FAQ

서명하는 데 네트워크 연결이 필요한가요? TSA URL에서 가져온 RFC 3161 타임스탬프를 임베드하는 B-T와 B-LT(그리고 B-LTA)에서만 필요합니다. 일반 sign_pdf_bytes와 PAdES B-B 서명은 완전히 오프라인입니다.

왜 TSA URL 없이는 B-T/B-LT가 실패하나요? 설계에 의한 것입니다 — 이 레벨들은 타임스탬프 토큰을 요구하므로, 서명자는 레벨을 조용히 낮추는 대신 Unsupported 오류로 안전하게 실패합니다.

Node나 브라우저(WASM)에서 서명할 수 있나요? 아직 불가능합니다. Node와 WASM은 서명 읽기/검증과 서명되지 않은 /Sig 플레이스홀더 필드를 제공하지만, CMS 서명자는 Rust, Python, Go, C#, Swift에만 연결되어 있습니다. 서버 측 바인딩에서 서명한 뒤 서명된 바이트를 전달하세요.

서명자는 어떤 서명 알고리즘을 사용하나요? SHA-256 기반 RSA-PKCS#1 v1.5로, 사실상 모든 PDF 리더가 받아들이는 패딩입니다. 구성된 정책이 특정 알고리즘을 금지하면 FIPS 암호화 공급자 정책으로 인해 서명이 안전하게 실패할 수 있습니다.

관련 페이지