PDF 서명하기: 디지털 서명과 PAdES
v0.3.50(issue #235)에서 추가되었습니다. PDF Oxide는 CMS/PKCS#7 분리형 서명을 /Contents에 임베드하고 /ByteRange를 채워 PDF에 서명하며, PAdES의 경우 증분 업데이트를 통해 RFC 3161 타임스탬프와 문서 보안 저장소(DSS)를 추가합니다. 이 페이지는 생성 측면을 다룹니다. 기존 서명을 검사하거나 검증하려면 디지털 서명(추출)을 참조하세요.
기능 게이트. 서명에는
signaturesCargo 기능이 필요합니다. 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-T와 B-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(×tamper),
&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 암호화 공급자 정책으로 인해 서명이 안전하게 실패할 수 있습니다.
관련 페이지
- 디지털 서명(추출) — 기존 서명 읽기 및 검증, RFC 3161 타임스탬프 파싱, TSA 타임스탬프 요청
- DocumentBuilder Fluent API — 서명할 PDF를 빌드하기
- API 레퍼런스 — Rust 측
signatures모듈의 인터페이스