Підпис PDF: цифрові підписи та PAdES
Додано у v0.3.50 (issue #235). PDF Oxide підписує PDF, вбудовуючи відокремлений підпис CMS/PKCS#7 у /Contents, заповнює /ByteRange і — для PAdES — додає мітку часу RFC 3161 та сховище безпеки документа (DSS) через інкрементальне оновлення. Ця сторінка охоплює бік створення. Щоб перевірити чи верифікувати наявний підпис, див. Цифрові підписи (вилучення).
Шлюз функцій. Підписання потребує Cargo-фічу
signatures. Для PAdES-B-T та B-LT додатково потрібна фічаtsa-clientяк джерело мітки часу RFC 3161. Якщо привʼязку зібрано без цих фіч, виклики підписання спричиняють помилку «не реалізовано» відповідної привʼязки.
Підтримка у привʼязках
Підписання надається привʼязками, які обгортають родини pdf_sign_bytes* та pdf_certificate_load_* з C ABI: 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 + непідписаний атрибут signature-time-stamp (RFC 3161) поверх значення підпису. |
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; підписання без неї завершується з відмовою (fails closed) і повертає Unsupported.
Як створити підпис PAdES B-LT?
sign_pdf_bytes_pades приймає рівень, необовʼязковий TSA URL і матеріал відкликання (сертифікати, CRL та відповіді OCSP у кодуванні DER), який стає DSS рівня B-LT.
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() повертає None/null, коли в документі немає DSS — це не помилка.
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 для кожного підпису (SHA-1 від /Contents у hex верхнього регістру) |
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 мс / p99 9 мс при 100% проходженні на еталонному корпусі (бенчмарк v0.3.8). Вартість підписання визначається переважно операцією CMS/RSA плюс — для B-T/B-LT — мережевим зверненням до TSA; гешування діапазону байтів PDF та запис інкрементального оновлення додають незначні накладні витрати поверх цього.
Часті запитання
Чи потрібне мережеве зʼєднання для підписання?
Лише для B-T та B-LT (і B-LTA), які вбудовують мітку часу RFC 3161, отриману з TSA URL. Простий sign_pdf_bytes і підпис PAdES B-B повністю працюють офлайн.
Чому B-T/B-LT завершується помилкою без TSA URL?
За задумом — ці рівні потребують токен мітки часу, тож підписувач завершується з відмовою і помилкою Unsupported, а не мовчки знижує рівень.
Чи можу я підписувати з Node або браузера (WASM)?
Поки що ні. Node та WASM надають читання/перевірку підпису і непідписане поле-заповнювач /Sig, але CMS-підписувач підключено лише в Rust, Python, Go, C# та Swift. Підписуйте на серверній привʼязці й надсилайте підписані байти.
Який алгоритм підпису використовує підписувач? RSA-PKCS#1 v1.5 поверх SHA-256 — доповнення, яке приймає практично кожен PDF-рідер. Політика крипто-провайдера FIPS може спричинити відмову під час підписання, якщо налаштована політика забороняє алгоритм.
Повʼязані сторінки
- Цифрові підписи (вилучення) — читання та перевірка наявних підписів, розбір міток часу RFC 3161, запит мітки часу в TSA
- Плинний API DocumentBuilder — побудуйте PDF, який ви хочете підписати
- Довідник API — поверхня модуля
signaturesна боці Rust