rustpdf 0.4.2-aarch64-linux → 0.4.4-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +32 -0
- data/lib/rustpdf/editable_doc.rb +35 -6
- data/lib/rustpdf/native.rb +21 -2
- data/lib/rustpdf.rb +317 -9
- data/vendor/aarch64-linux/libpdf_ffi.so +0 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a9f8748dc44df1f9a62da646c68d0c72277cdc5be78b1d45e8a07e371936668
|
|
4
|
+
data.tar.gz: 94a7fe602d4e77a601f6f4edd74f745ec0a20bb6b2706a6d1376c5e10409e09e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58b4668a5ce6344d838a6b5c4da2ffdd0676d4b8890d4a088deb2d7c5ac4e0506d0116991584f505c5c542ebe6eb0dab42039531c079ca62f444c592e8fed6b5
|
|
7
|
+
data.tar.gz: 70bf9dbca7a482309342e8cbcd838f9545925829bfa0eff7d1135674333ca601518e2a7ea0b993ce0630020de8574afd48f3c0014d745ca23343ee1004e46004
|
data/README.md
CHANGED
|
@@ -49,6 +49,38 @@ signed = RustPdf.sign(data, key_der, cert_der, pades: true)
|
|
|
49
49
|
Corporate features (PDF/A, signing, encryption, accessibility, page rendering — a **Pro** feature) require a license;
|
|
50
50
|
without one they raise `RustPdf::Error`. See [`docs/LICENSING.md`](../../docs/LICENSING.md).
|
|
51
51
|
|
|
52
|
+
## Deferred / HSM signing
|
|
53
|
+
|
|
54
|
+
Sign without the private key ever entering this library — the key stays in an
|
|
55
|
+
HSM, cloud KMS, smartcard or PKI token. Works with any PKI (eIDAS, AATL, or
|
|
56
|
+
your own CA). The library builds the CMS and asks your code only for the raw
|
|
57
|
+
RSA signature over the bytes it hands you.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "rustpdf"
|
|
61
|
+
|
|
62
|
+
pdf = File.binread("contract.pdf")
|
|
63
|
+
cert_der = File.binread("signing-cert.der") # X.509 signer certificate (DER)
|
|
64
|
+
|
|
65
|
+
# Model A — your block returns the raw RSA PKCS#1 v1.5 signature; the key
|
|
66
|
+
# never reaches the library. Call your HSM / cloud KMS / token here.
|
|
67
|
+
signed = RustPdf.sign_with(pdf, cert_der,
|
|
68
|
+
chain: [issuer_der],
|
|
69
|
+
options: RustPdf::SigningOptions.new(reason: "Approved")) do |to_sign|
|
|
70
|
+
hsm.sign_rsa_sha256(to_sign) # bytes in, raw signature out
|
|
71
|
+
end
|
|
72
|
+
File.binwrite("contract.signed.pdf", signed)
|
|
73
|
+
|
|
74
|
+
# Model B — two-phase: prepare, sign the hash out of band, then complete.
|
|
75
|
+
session = RustPdf.begin_signing(pdf)
|
|
76
|
+
container = remote.build_cms(session.hash) # send #hash to a remote signer
|
|
77
|
+
final = session.complete(container) # embed the DER CMS / PKCS#7
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`RustPdf.list_signatures(pdf)` inventories existing signature fields before you
|
|
81
|
+
sign. See [`docs/ruby.html`](../../site/public/docs/ruby.html#sign) for the full
|
|
82
|
+
deferred-signing API (`SigningOptions`, `Certify`, `SignaturePolicy`).
|
|
83
|
+
|
|
52
84
|
## Test
|
|
53
85
|
|
|
54
86
|
```sh
|
data/lib/rustpdf/editable_doc.rb
CHANGED
|
@@ -127,16 +127,22 @@ module RustPdf
|
|
|
127
127
|
text.split("\n").reject(&:empty?)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
# Stamp a diagonal text watermark across every page.
|
|
131
|
-
|
|
130
|
+
# Stamp a diagonal text watermark across every page. When
|
|
131
|
+
# +opaque_background+ is true, the text is drawn over an opaque white box
|
|
132
|
+
# (otherwise it is blended into the page content).
|
|
133
|
+
def watermark_text(text, size: 64.0, color: [0.5, 0.5, 0.5], opacity: 0.30,
|
|
134
|
+
rotation_deg: 45.0, opaque_background: false)
|
|
132
135
|
r, g, b = color
|
|
133
|
-
RustPdf.check(Native.call("pdf_editable_watermark_text", ptr, text, size, r, g, b,
|
|
136
|
+
RustPdf.check(Native.call("pdf_editable_watermark_text", ptr, text, size, r, g, b,
|
|
137
|
+
opacity, rotation_deg, opaque_background ? 1 : 0))
|
|
134
138
|
self
|
|
135
139
|
end
|
|
136
140
|
|
|
137
|
-
# Stamp an image watermark (from a file) across every page
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
# Stamp an image watermark (from a file) across every page, rotated
|
|
142
|
+
# +rotation_deg+ degrees counter-clockwise.
|
|
143
|
+
def watermark_image_file(path, width, height, opacity: 0.30, rotation_deg: 0.0)
|
|
144
|
+
RustPdf.check(Native.call("pdf_editable_watermark_image_file", ptr, path, width, height,
|
|
145
|
+
opacity, rotation_deg))
|
|
140
146
|
self
|
|
141
147
|
end
|
|
142
148
|
|
|
@@ -156,6 +162,29 @@ module RustPdf
|
|
|
156
162
|
self
|
|
157
163
|
end
|
|
158
164
|
|
|
165
|
+
# ---- version normalization (issue #41 P1) -------------------------------
|
|
166
|
+
|
|
167
|
+
# Set the output PDF version (downgrade/normalize). +version+ uses the
|
|
168
|
+
# RustPdf::Version codes (V1_4=0, V1_5=1, V1_7=2, V2_0=3).
|
|
169
|
+
def set_version(version)
|
|
170
|
+
RustPdf.check(Native.call("pdf_editable_set_version", ptr, version))
|
|
171
|
+
self
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Strip PDF/A conformance (OutputIntents, XMP pdfaid, /Version) so the file
|
|
175
|
+
# is a plain PDF.
|
|
176
|
+
def strip_pdfa
|
|
177
|
+
RustPdf.check(Native.call("pdf_editable_strip_pdfa", ptr))
|
|
178
|
+
self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Normalize to a plain PDF at +version+ (strip PDF/A + set version). Codes as
|
|
182
|
+
# in #set_version.
|
|
183
|
+
def normalize(version = Version::V1_7)
|
|
184
|
+
RustPdf.check(Native.call("pdf_editable_normalize", ptr, version))
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
159
188
|
def optimize
|
|
160
189
|
RustPdf.check(Native.call("pdf_editable_optimize", ptr))
|
|
161
190
|
self
|
data/lib/rustpdf/native.rb
CHANGED
|
@@ -97,13 +97,32 @@ module RustPdf
|
|
|
97
97
|
"pdf_editable_set_choice" => [[VP, VP, VP, VP], I],
|
|
98
98
|
"pdf_editable_flatten_forms" => [[VP], I],
|
|
99
99
|
"pdf_editable_field_names" => [[VP, VP, VP], I],
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
# watermark_text gained a trailing int opaque_background; watermark_image
|
|
101
|
+
# gained a trailing double rotation_deg (issue #41 P1).
|
|
102
|
+
"pdf_editable_watermark_text" => [[VP, VP, D, D, D, D, D, D, I], I],
|
|
103
|
+
"pdf_editable_watermark_image_file" => [[VP, VP, D, D, D, D], I],
|
|
102
104
|
# Tier 2: redaction + PDF/A conversion (EditableDoc)
|
|
103
105
|
"pdf_editable_redact" => [[VP, SZ, VP, SZ, VP], I],
|
|
104
106
|
"pdf_editable_convert_to_pdfa" => [[VP, I], I],
|
|
105
107
|
# Tier 2: signature validation (module-level)
|
|
106
108
|
"pdf_verify_signatures_json" => [[VP, SZ, VP, VP], I],
|
|
109
|
+
|
|
110
|
+
# Deferred / external (HSM) signing — issue #41 P0. The PdfSigningOptions
|
|
111
|
+
# struct crosses as an opaque pointer (VP); the Model-A callback is a
|
|
112
|
+
# Fiddle::Closure passed as a function pointer (VP).
|
|
113
|
+
"pdf_sign_begin" => [[VP, SZ, VP, VP, VP, VP, VP], I],
|
|
114
|
+
"pdf_sign_complete" => [[VP, SZ, VP, SZ, VP, VP], I],
|
|
115
|
+
"pdf_sign_with" => [[VP, SZ, VP, SZ, VP, VP, SZ, VP, VP, VP, VP, VP], I],
|
|
116
|
+
"pdf_list_signatures" => [[VP, SZ, VP, VP], I],
|
|
117
|
+
|
|
118
|
+
# Issue #41 P1: positional text search, version normalization, network TSA.
|
|
119
|
+
"pdf_find_text_json" => [[VP, SZ, VP, I, VP, VP], I],
|
|
120
|
+
"pdf_editable_set_version" => [[VP, I], I],
|
|
121
|
+
"pdf_editable_strip_pdfa" => [[VP], I],
|
|
122
|
+
"pdf_editable_normalize" => [[VP, I], I],
|
|
123
|
+
"pdf_timestamp_begin" => [[VP, SZ, VP, VP, VP, VP], I],
|
|
124
|
+
"pdf_timestamp_request" => [[VP, SZ, VP, SZ, I, VP, VP], I],
|
|
125
|
+
"pdf_timestamp_token_from_response" => [[VP, SZ, VP, VP], I],
|
|
107
126
|
}.freeze
|
|
108
127
|
|
|
109
128
|
def lib
|
data/lib/rustpdf.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "fiddle"
|
|
2
2
|
require "json"
|
|
3
|
+
require "digest"
|
|
3
4
|
require_relative "rustpdf/native"
|
|
4
5
|
|
|
5
6
|
# Idiomatic Ruby binding for the rust-pdf core over its C ABI (libpdf_ffi),
|
|
@@ -55,6 +56,14 @@ module RustPdf
|
|
|
55
56
|
AES256 = 2
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
# Output PDF version (for set_version / normalize).
|
|
60
|
+
module Version
|
|
61
|
+
V1_4 = 0
|
|
62
|
+
V1_5 = 1
|
|
63
|
+
V1_7 = 2
|
|
64
|
+
V2_0 = 3
|
|
65
|
+
end
|
|
66
|
+
|
|
58
67
|
# ZUGFeRD / Factur-X conformance profiles.
|
|
59
68
|
module FacturxProfile
|
|
60
69
|
MINIMUM = 0
|
|
@@ -91,6 +100,89 @@ module RustPdf
|
|
|
91
100
|
end
|
|
92
101
|
end
|
|
93
102
|
|
|
103
|
+
# DocMDP certification level applied by the first (certifying) signature.
|
|
104
|
+
module Certify
|
|
105
|
+
NONE = 0 # not a certifying signature
|
|
106
|
+
LOCKED = 1 # /P 1 — no changes permitted after signing
|
|
107
|
+
FORMS = 2 # /P 2 — form-filling and signing permitted
|
|
108
|
+
FORMS_AND_ANNOTATIONS = 3 # /P 3 — also annotations permitted
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A signature-policy identifier (PAdES-EPES / ICP-Brasil AD-RB).
|
|
112
|
+
class SignaturePolicy
|
|
113
|
+
attr_accessor :oid, :hash, :hash_algorithm_oid, :uri
|
|
114
|
+
|
|
115
|
+
# +oid+: dotted-decimal policy OID; +hash+: policy document hash bytes
|
|
116
|
+
# (under +hash_algorithm_oid+, nil = SHA-256); +uri+: optional SPURI.
|
|
117
|
+
def initialize(oid:, hash:, hash_algorithm_oid: nil, uri: nil)
|
|
118
|
+
@oid = oid
|
|
119
|
+
@hash = hash
|
|
120
|
+
@hash_algorithm_oid = hash_algorithm_oid
|
|
121
|
+
@uri = uri
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Options for deferred / external signing (issue #41). The +visible_*+ fields
|
|
126
|
+
# request a visible signature appearance: +visible_rect+ is [x0,y0,x1,y1] in
|
|
127
|
+
# points, +visible_text+ is newline-separated appearance lines, and
|
|
128
|
+
# +visible_image+ is PNG/JPEG bytes of a handwritten-signature image.
|
|
129
|
+
class SigningOptions
|
|
130
|
+
attr_accessor :reason, :location, :name, :pades, :certify, :container_size, :policy,
|
|
131
|
+
:visible, :visible_page, :visible_rect, :visible_text, :visible_image
|
|
132
|
+
|
|
133
|
+
def initialize(reason: nil, location: nil, name: nil, pades: false,
|
|
134
|
+
certify: Certify::NONE, container_size: 0, policy: nil,
|
|
135
|
+
visible: false, visible_page: 0, visible_rect: nil,
|
|
136
|
+
visible_text: nil, visible_image: nil)
|
|
137
|
+
@reason = reason
|
|
138
|
+
@location = location
|
|
139
|
+
@name = name
|
|
140
|
+
@pades = pades
|
|
141
|
+
@certify = certify
|
|
142
|
+
@container_size = container_size
|
|
143
|
+
@policy = policy
|
|
144
|
+
@visible = visible
|
|
145
|
+
@visible_page = visible_page
|
|
146
|
+
@visible_rect = visible_rect
|
|
147
|
+
@visible_text = visible_text
|
|
148
|
+
@visible_image = visible_image
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A signature field discovered in a PDF (pre-signing inventory). +signed+ is
|
|
153
|
+
# true when the field already carries a signature.
|
|
154
|
+
SignatureField = Struct.new(:name, :signed)
|
|
155
|
+
|
|
156
|
+
# One positional text match from #find_text. Coordinates are in PDF points,
|
|
157
|
+
# origin lower-left; +x+/+y+ is the lower-left corner of the box.
|
|
158
|
+
TextHit = Struct.new(:page, :text, :x, :y, :width, :height)
|
|
159
|
+
|
|
160
|
+
# An in-progress two-phase signature: #document holds the placeholder PDF and
|
|
161
|
+
# #bytes the exact bytes the signature covers. Hand #hash to a remote signer,
|
|
162
|
+
# build the CMS container, then call #complete.
|
|
163
|
+
class SigningSession
|
|
164
|
+
# The prepared PDF (with a zero-filled /Contents placeholder).
|
|
165
|
+
attr_reader :document
|
|
166
|
+
# The exact bytes covered by the signature (the two ByteRange segments).
|
|
167
|
+
attr_reader :bytes
|
|
168
|
+
|
|
169
|
+
def initialize(document, bytes)
|
|
170
|
+
@document = document
|
|
171
|
+
@bytes = bytes
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# SHA-256 of #bytes — the 32-byte value an HSM signs.
|
|
175
|
+
def hash
|
|
176
|
+
Digest::SHA256.digest(@bytes)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Phase 2: complete the signature by embedding a finished DER CMS / PKCS#7
|
|
180
|
+
# +container+, returning the final signed PDF.
|
|
181
|
+
def complete(container)
|
|
182
|
+
RustPdf.complete_signature(@document, container)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
94
186
|
module_function
|
|
95
187
|
|
|
96
188
|
# Native library version string.
|
|
@@ -133,10 +225,27 @@ module RustPdf
|
|
|
133
225
|
count[0, Native::SIZEOF_SZ].unpack1("J")
|
|
134
226
|
end
|
|
135
227
|
|
|
228
|
+
# Find every occurrence of +query+ in +pdf+, returning positional boxes.
|
|
229
|
+
# Returns an Array of TextHit (page, text, x, y, width, height) in PDF points
|
|
230
|
+
# (origin lower-left). +case_sensitive+ defaults to false. An empty Array
|
|
231
|
+
# means no match.
|
|
232
|
+
def find_text(pdf, query, case_sensitive: false)
|
|
233
|
+
js = take_bytes do |pp, pn|
|
|
234
|
+
Native.call("pdf_find_text_json", pdf, pdf.bytesize, query, case_sensitive ? 1 : 0, pp, pn)
|
|
235
|
+
end.force_encoding(Encoding::UTF_8)
|
|
236
|
+
return [] if js.empty?
|
|
237
|
+
|
|
238
|
+
JSON.parse(js).map do |h|
|
|
239
|
+
TextHit.new(h["page"], h["text"], h["x"], h["y"], h["width"], h["height"])
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
136
243
|
# Validate every signature in +pdf+. Returns one Hash per signature with keys
|
|
137
244
|
# "field_name", "sub_filter", "signer", "covers_whole_document",
|
|
138
|
-
# "digest_valid", "signature_valid", "is_valid" and "byte_range"
|
|
139
|
-
#
|
|
245
|
+
# "digest_valid", "signature_valid", "is_valid" and "byte_range", plus the
|
|
246
|
+
# richer certificate fields (any may be null): "issuer", "serial_number",
|
|
247
|
+
# "valid_from", "valid_to", "algorithm", "signing_time", "cert_count" and
|
|
248
|
+
# "has_timestamp". An empty array means the document is unsigned.
|
|
140
249
|
def verify_signatures(pdf)
|
|
141
250
|
js = take_bytes { |pp, pn| Native.call("pdf_verify_signatures_json", pdf, pdf.bytesize, pp, pn) }
|
|
142
251
|
.force_encoding(Encoding::UTF_8)
|
|
@@ -177,8 +286,213 @@ module RustPdf
|
|
|
177
286
|
result
|
|
178
287
|
end
|
|
179
288
|
|
|
289
|
+
# ---- deferred / external (HSM) signing — issue #41 ------------------------
|
|
290
|
+
|
|
291
|
+
# Model A — remote signer. Sign +pdf+ without handing this library a key: it
|
|
292
|
+
# builds the CMS signed attributes and calls the given block for the raw RSA
|
|
293
|
+
# PKCS#1 v1.5 signature (over SHA-256 of the block's argument), then assembles
|
|
294
|
+
# and embeds the CMS. +cert_der+ is the signer certificate (DER); +chain+ are
|
|
295
|
+
# intermediate certificates (DER), supplied independently of the key. The
|
|
296
|
+
# private key never reaches this library. Returns the signed PDF bytes.
|
|
297
|
+
def sign_with(pdf, cert_der, chain: [], options: nil, &block)
|
|
298
|
+
raise Error, "sign_with requires a block that produces the signature" unless block
|
|
299
|
+
|
|
300
|
+
opts_bytes, keep = build_signing_options(options)
|
|
301
|
+
cptrs = chain.map { |c| Fiddle::Pointer[c] }
|
|
302
|
+
cptr_buf = cptrs.map(&:to_i).pack("J*")
|
|
303
|
+
clen_buf = chain.map(&:bytesize).pack("J*")
|
|
304
|
+
|
|
305
|
+
signer_error = nil
|
|
306
|
+
closure = Fiddle::Closure::BlockCaller.new(
|
|
307
|
+
Fiddle::TYPE_INT,
|
|
308
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T,
|
|
309
|
+
Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_VOIDP]
|
|
310
|
+
) do |_ctx, data, data_len, sig_buf, sig_cap, sig_len|
|
|
311
|
+
begin
|
|
312
|
+
sig = block.call(data[0, data_len]).to_s
|
|
313
|
+
if sig.bytesize > sig_cap
|
|
314
|
+
signer_error = Error.new("signature (#{sig.bytesize} bytes) exceeds buffer capacity #{sig_cap}")
|
|
315
|
+
next 2
|
|
316
|
+
end
|
|
317
|
+
sig_buf[0, sig.bytesize] = sig
|
|
318
|
+
sig_len[0, Native::SIZEOF_SZ] = [sig.bytesize].pack("J")
|
|
319
|
+
0
|
|
320
|
+
rescue StandardError => e
|
|
321
|
+
signer_error = e
|
|
322
|
+
1
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
begin
|
|
327
|
+
result = take_bytes do |pp, pn|
|
|
328
|
+
Native.call("pdf_sign_with", pdf, pdf.bytesize, cert_der, cert_der.bytesize,
|
|
329
|
+
cptr_buf, clen_buf, chain.size, opts_bytes, closure, Fiddle::NULL, pp, pn)
|
|
330
|
+
end
|
|
331
|
+
rescue Error
|
|
332
|
+
raise signer_error if signer_error
|
|
333
|
+
|
|
334
|
+
raise
|
|
335
|
+
end
|
|
336
|
+
# keep the callback, struct and per-cert pointers alive until the call returned
|
|
337
|
+
cptrs.clear
|
|
338
|
+
keep.clear
|
|
339
|
+
closure.to_i # touch to keep it referenced past the native call
|
|
340
|
+
result
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Model B — two-phase signing, phase 1. Prepare +pdf+ for deferred signing:
|
|
344
|
+
# returns a SigningSession whose #hash you send to a remote HSM. Build the CMS
|
|
345
|
+
# container, then call SigningSession#complete (or .complete_signature). The
|
|
346
|
+
# key never reaches this library.
|
|
347
|
+
def begin_signing(pdf, options: nil)
|
|
348
|
+
opts_bytes, keep = build_signing_options(options)
|
|
349
|
+
doc_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
350
|
+
doc_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
351
|
+
tbs_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
352
|
+
tbs_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
353
|
+
check(Native.call("pdf_sign_begin", pdf, pdf.bytesize, opts_bytes, doc_p, doc_n, tbs_p, tbs_n))
|
|
354
|
+
keep.clear
|
|
355
|
+
SigningSession.new(read_buffer(doc_p, doc_n), read_buffer(tbs_p, tbs_n))
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Model B — phase 2. Embed a complete DER CMS / PKCS#7 +container+ into a
|
|
359
|
+
# prepared +document+ (from #begin_signing), returning the final signed PDF.
|
|
360
|
+
def complete_signature(document, container)
|
|
361
|
+
take_bytes do |pp, pn|
|
|
362
|
+
Native.call("pdf_sign_complete", document, document.bytesize, container, container.bytesize, pp, pn)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# ---- network TSA (AD-RT) document timestamp — issue #41 -------------------
|
|
367
|
+
|
|
368
|
+
# Phase 1 of a network-TSA document timestamp (/DocTimeStamp). Prepares +pdf+
|
|
369
|
+
# and returns [document_bytes, tbs_bytes]: +tbs_bytes+ are the bytes whose
|
|
370
|
+
# SHA-256 forms the RFC 3161 message imprint. Build a TimeStampReq with
|
|
371
|
+
# #timestamp_request, POST it to the TSA, extract the token with
|
|
372
|
+
# #timestamp_token_from_response, then embed it via #complete_signature.
|
|
373
|
+
def begin_timestamp(pdf)
|
|
374
|
+
doc_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
375
|
+
doc_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
376
|
+
tbs_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
377
|
+
tbs_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
378
|
+
check(Native.call("pdf_timestamp_begin", pdf, pdf.bytesize, doc_p, doc_n, tbs_p, tbs_n))
|
|
379
|
+
[read_buffer(doc_p, doc_n), read_buffer(tbs_p, tbs_n)]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Build an RFC 3161 TimeStampReq (DER) for +imprint+ (the SHA-256 of the bytes
|
|
383
|
+
# to timestamp, e.g. the +tbs_bytes+ from #begin_timestamp). +nonce+ is
|
|
384
|
+
# optional; +cert_req+ asks the TSA to embed its certificate. Returns the
|
|
385
|
+
# request bytes to POST to the TSA.
|
|
386
|
+
def timestamp_request(imprint, nonce: nil, cert_req: true)
|
|
387
|
+
take_bytes do |pp, pn|
|
|
388
|
+
Native.call("pdf_timestamp_request", imprint, imprint.bytesize,
|
|
389
|
+
nonce, nonce ? nonce.bytesize : 0, cert_req ? 1 : 0, pp, pn)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Extract the TimeStampToken (a CMS ContentInfo) from a TSA's RFC 3161
|
|
394
|
+
# TimeStampResp +response+ bytes. The returned token is embedded via
|
|
395
|
+
# #complete_signature(document, token).
|
|
396
|
+
def timestamp_token_from_response(response)
|
|
397
|
+
take_bytes do |pp, pn|
|
|
398
|
+
Native.call("pdf_timestamp_token_from_response", response, response.bytesize, pp, pn)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# List the signature fields in +pdf+ (detect existing signatures before
|
|
403
|
+
# signing). Returns an Array of SignatureField; an empty Array means there are
|
|
404
|
+
# no signature fields.
|
|
405
|
+
def list_signatures(pdf)
|
|
406
|
+
text = take_bytes { |pp, pn| Native.call("pdf_list_signatures", pdf, pdf.bytesize, pp, pn) }
|
|
407
|
+
.force_encoding(Encoding::UTF_8)
|
|
408
|
+
fields = []
|
|
409
|
+
text.each_line do |line|
|
|
410
|
+
line = line.chomp
|
|
411
|
+
tab = line.index("\t")
|
|
412
|
+
next unless tab
|
|
413
|
+
|
|
414
|
+
fields << SignatureField.new(line[(tab + 1)..-1], line[0...tab] == "1")
|
|
415
|
+
end
|
|
416
|
+
fields
|
|
417
|
+
end
|
|
418
|
+
|
|
180
419
|
# ---- internal helpers (used by Document/EditableDoc) ----------------------
|
|
181
420
|
|
|
421
|
+
# Marshal a SigningOptions (or nil) into the C PdfSigningOptions struct bytes,
|
|
422
|
+
# returning [packed_struct, keepalive] where +keepalive+ holds the Fiddle
|
|
423
|
+
# pointers backing the struct's string/byte fields (keep it referenced until
|
|
424
|
+
# the native call returns). 64-bit layout: 3 ptr, 2 int (8 bytes together),
|
|
425
|
+
# size_t, ptr, ptr, size_t, ptr, ptr, then the visible-signature tail:
|
|
426
|
+
# int (+ 4 pad), size_t, double[4], ptr, ptr, size_t.
|
|
427
|
+
def build_signing_options(options)
|
|
428
|
+
keep = []
|
|
429
|
+
cstr = lambda do |s|
|
|
430
|
+
return 0 if s.nil?
|
|
431
|
+
|
|
432
|
+
p = Fiddle::Pointer[s.to_s]
|
|
433
|
+
keep << p
|
|
434
|
+
p.to_i
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
reason = cstr.call(options&.reason)
|
|
438
|
+
location = cstr.call(options&.location)
|
|
439
|
+
name = cstr.call(options&.name)
|
|
440
|
+
pades = options&.pades ? 1 : 0
|
|
441
|
+
cert = (options&.certify || Certify::NONE).to_i
|
|
442
|
+
est = (options&.container_size || 0).to_i
|
|
443
|
+
|
|
444
|
+
policy_oid = 0
|
|
445
|
+
policy_hash = 0
|
|
446
|
+
policy_hash_len = 0
|
|
447
|
+
policy_alg = 0
|
|
448
|
+
policy_uri = 0
|
|
449
|
+
if (pol = options&.policy)
|
|
450
|
+
policy_oid = cstr.call(pol.oid)
|
|
451
|
+
if pol.hash && !pol.hash.empty?
|
|
452
|
+
hp = Fiddle::Pointer[pol.hash]
|
|
453
|
+
keep << hp
|
|
454
|
+
policy_hash = hp.to_i
|
|
455
|
+
policy_hash_len = pol.hash.bytesize
|
|
456
|
+
end
|
|
457
|
+
policy_alg = cstr.call(pol.hash_algorithm_oid)
|
|
458
|
+
policy_uri = cstr.call(pol.uri)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
visible = options&.visible ? 1 : 0
|
|
462
|
+
vis_page = (options&.visible_page || 0).to_i
|
|
463
|
+
vis_rect = Array(options&.visible_rect || [0.0, 0.0, 0.0, 0.0]).map(&:to_f)[0, 4]
|
|
464
|
+
vis_rect += [0.0] * (4 - vis_rect.size)
|
|
465
|
+
vis_text = cstr.call(options&.visible_text)
|
|
466
|
+
vis_image = 0
|
|
467
|
+
vis_image_len = 0
|
|
468
|
+
if (img = options&.visible_image) && !img.empty?
|
|
469
|
+
ip = Fiddle::Pointer[img]
|
|
470
|
+
keep << ip
|
|
471
|
+
vis_image = ip.to_i
|
|
472
|
+
vis_image_len = img.bytesize
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
bytes = [reason, location, name].pack("J3") +
|
|
476
|
+
[pades, cert].pack("l2") +
|
|
477
|
+
[est, policy_oid, policy_hash, policy_hash_len, policy_alg, policy_uri].pack("J6") +
|
|
478
|
+
[visible].pack("l") + "\x00\x00\x00\x00".b + # int visible + 4-byte pad
|
|
479
|
+
[vis_page].pack("J") + vis_rect.pack("d4") +
|
|
480
|
+
[vis_text, vis_image, vis_image_len].pack("J3")
|
|
481
|
+
[bytes, keep]
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Read an out-buffer (pointer-to-pointer +pp+, pointer-to-len +pn+) into a
|
|
485
|
+
# binary String, freeing the native buffer.
|
|
486
|
+
def read_buffer(pp, pn)
|
|
487
|
+
len = pn[0, Native::SIZEOF_SZ].unpack1("J")
|
|
488
|
+
return "".b if len.zero?
|
|
489
|
+
|
|
490
|
+
dptr = pp.ptr
|
|
491
|
+
bytes = dptr[0, len]
|
|
492
|
+
Native.call("pdf_buffer_free", dptr, len)
|
|
493
|
+
bytes
|
|
494
|
+
end
|
|
495
|
+
|
|
182
496
|
def last_error
|
|
183
497
|
p = Native.call("pdf_last_error_message")
|
|
184
498
|
p.null? ? "unknown error" : p.to_s
|
|
@@ -194,13 +508,7 @@ module RustPdf
|
|
|
194
508
|
pp = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
195
509
|
pn = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
196
510
|
check(yield(pp, pn))
|
|
197
|
-
|
|
198
|
-
return "".b if len.zero?
|
|
199
|
-
|
|
200
|
-
dptr = pp.ptr
|
|
201
|
-
bytes = dptr[0, len]
|
|
202
|
-
Native.call("pdf_buffer_free", dptr, len)
|
|
203
|
-
bytes
|
|
511
|
+
read_buffer(pp, pn)
|
|
204
512
|
end
|
|
205
513
|
|
|
206
514
|
# Run a producer { |out_int| status } and return the written int.
|
|
Binary file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rustpdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- rust-pdf
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Idiomatic Ruby binding over the rust-pdf C ABI (libpdf_ffi) using the
|
|
14
14
|
built-in Fiddle stdlib.
|