easy_code_sign 0.1.0 → 0.2.0
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/CHANGELOG.md +18 -19
- data/lib/easy_code_sign/pdf/cms_builder.rb +142 -0
- data/lib/easy_code_sign/pdf/native_signer.rb +275 -0
- data/lib/easy_code_sign/signable/pdf_file.rb +54 -424
- data/lib/easy_code_sign/version.rb +1 -1
- data/lib/easy_code_sign.rb +0 -2
- data/test/pdf_signable_test.rb +87 -463
- metadata +6 -4
|
@@ -1,253 +1,84 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "../pdf/native_signer"
|
|
4
4
|
|
|
5
5
|
module EasyCodeSign
|
|
6
6
|
module Signable
|
|
7
|
-
# Handler for signing PDF files
|
|
7
|
+
# Handler for signing PDF files using the native MIT-licensed backend.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Signs PDFs using ISO 32000 incremental-update signatures (adbe.pkcs7.detached).
|
|
10
|
+
# No HexaPDF (AGPL) dependency — signing is done via NativeSigner + CmsBuilder.
|
|
11
11
|
#
|
|
12
|
-
# @example Sign a PDF
|
|
13
|
-
# pdf = PdfFile.new("document.pdf"
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# pdf.apply_signature(
|
|
12
|
+
# @example Sign a PDF with a hardware-token provider
|
|
13
|
+
# pdf = PdfFile.new("document.pdf",
|
|
14
|
+
# output_path: "document_signed.pdf",
|
|
15
|
+
# signature_reason: "Approval",
|
|
16
|
+
# signature_location: "Athens")
|
|
17
|
+
# pdf.apply_signature(->(hash) { provider.sign_bytes(hash) }, [cert])
|
|
18
18
|
#
|
|
19
19
|
class PdfFile < Base
|
|
20
20
|
SUPPORTED_EXTENSIONS = %w[.pdf].freeze
|
|
21
21
|
|
|
22
|
-
# Signature appearance configuration
|
|
23
22
|
attr_reader :signature_config
|
|
24
23
|
|
|
25
24
|
def initialize(file_path, **options)
|
|
26
25
|
super
|
|
27
26
|
validate_pdf!
|
|
28
27
|
@signature_config = build_signature_config(options)
|
|
29
|
-
@document = nil
|
|
30
|
-
@signature_field = nil
|
|
31
|
-
@prepared_data = nil
|
|
32
28
|
end
|
|
33
29
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Set up the signing handler that will be called by HexaPDF
|
|
43
|
-
@prepared_data = {
|
|
44
|
-
document: @document,
|
|
45
|
-
signature_field: @signature_field
|
|
46
|
-
}
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Get content to sign (hash of ByteRange content)
|
|
50
|
-
# HexaPDF calculates this during the signing process
|
|
51
|
-
# @return [String] placeholder - actual content determined during apply_signature
|
|
52
|
-
def content_to_sign
|
|
53
|
-
prepare_for_signing if @prepared_data.nil?
|
|
54
|
-
|
|
55
|
-
# For PDF signing, the actual content to sign is determined by ByteRange
|
|
56
|
-
# during the signature embedding process. We return a placeholder here
|
|
57
|
-
# and handle the actual signing in apply_signature via a custom handler.
|
|
58
|
-
#
|
|
59
|
-
# The real signing happens through ExternalSigningHandler which receives
|
|
60
|
-
# the ByteRange content from HexaPDF.
|
|
61
|
-
"PDF_SIGNING_PLACEHOLDER"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Apply signature to PDF
|
|
65
|
-
# @param signature_or_callback [String, Proc] raw signature bytes or signing callback
|
|
66
|
-
# @param certificate_chain [Array<OpenSSL::X509::Certificate>] certificate chain
|
|
67
|
-
# @param timestamp_token [Timestamp::Response, Proc, nil] optional timestamp or lazy accessor
|
|
68
|
-
# @return [String] path to signed PDF
|
|
30
|
+
# Apply an ISO 32000 incremental-update signature to this PDF.
|
|
31
|
+
#
|
|
32
|
+
# @param signature_or_callback [String, Proc]
|
|
33
|
+
# - Proc: called with SHA256(signed_attrs_DER) → returns raw RSA signature bytes
|
|
34
|
+
# - String: pre-computed raw RSA signature bytes (used as-is)
|
|
35
|
+
# @param certificate_chain [Array<OpenSSL::X509::Certificate>]
|
|
36
|
+
# First element is the signing certificate; rest are chain certs.
|
|
37
|
+
# @return [String] path to the signed output file
|
|
69
38
|
def apply_signature(signature_or_callback, certificate_chain, timestamp_token: nil)
|
|
70
|
-
prepare_for_signing if @prepared_data.nil?
|
|
71
|
-
|
|
72
39
|
signing_certificate = certificate_chain.first
|
|
73
40
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Configure signature handler
|
|
87
|
-
handler = @document.signatures.handler_for_signing(
|
|
88
|
-
@signature_field,
|
|
89
|
-
certificate: signing_certificate,
|
|
90
|
-
key: signing_key,
|
|
41
|
+
sign_proc = if signature_or_callback.respond_to?(:call)
|
|
42
|
+
signature_or_callback
|
|
43
|
+
else
|
|
44
|
+
raw = signature_or_callback
|
|
45
|
+
->(_hash) { raw }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
signer = Pdf::NativeSigner.new(
|
|
49
|
+
pdf_path: file_path,
|
|
50
|
+
output_path: output_path,
|
|
51
|
+
certificate: signing_certificate,
|
|
91
52
|
certificate_chain: certificate_chain[1..] || [],
|
|
92
|
-
reason:
|
|
93
|
-
location:
|
|
94
|
-
contact_info:
|
|
95
|
-
signature_size: estimated_size,
|
|
96
|
-
timestamp_handler: timestamp_token ? Pdf::TimestampHandler.new(timestamp_token) : nil
|
|
53
|
+
reason: @signature_config[:reason],
|
|
54
|
+
location: @signature_config[:location],
|
|
55
|
+
contact_info: @signature_config[:contact_info]
|
|
97
56
|
)
|
|
98
|
-
|
|
99
|
-
# Build visible appearance if configured
|
|
100
|
-
if @signature_config[:visible]
|
|
101
|
-
build_visible_appearance(handler, signing_certificate)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Write signed PDF
|
|
105
|
-
out_path = output_path
|
|
106
|
-
@document.signatures.sign(@signature_field, handler, write_options: { output: out_path })
|
|
107
|
-
|
|
108
|
-
out_path
|
|
57
|
+
signer.sign { |hash| sign_proc.call(hash) }
|
|
109
58
|
end
|
|
110
59
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
60
|
+
# Extract the last signature from this PDF by scanning the raw bytes.
|
|
61
|
+
# Works for any PDF signed with the native backend (adbe.pkcs7.detached).
|
|
113
62
|
#
|
|
114
|
-
#
|
|
115
|
-
# The external_signing lambda receives (digest_algorithm, hash) where hash is the
|
|
116
|
-
# digest of the DER-encoded signed attributes — exactly what the external signer
|
|
117
|
-
# must sign. We capture it and return "" to leave the /Contents zero-filled.
|
|
118
|
-
#
|
|
119
|
-
# @param certificate [OpenSSL::X509::Certificate] signing certificate
|
|
120
|
-
# @param certificate_chain [Array<OpenSSL::X509::Certificate>] full chain
|
|
121
|
-
# @param digest_algorithm [String] "sha256", "sha384", or "sha512"
|
|
122
|
-
# @param timestamp_size [Integer] extra bytes to reserve for timestamp (0 if none)
|
|
123
|
-
# @return [DeferredSigningRequest]
|
|
124
|
-
def prepare_deferred(certificate, certificate_chain, digest_algorithm: "sha256", timestamp_size: 0)
|
|
125
|
-
captured_digest = nil
|
|
126
|
-
captured_algorithm = nil
|
|
127
|
-
signing_time = Time.now
|
|
128
|
-
|
|
129
|
-
external_signing = lambda do |algo, hash|
|
|
130
|
-
captured_algorithm = algo
|
|
131
|
-
captured_digest = hash
|
|
132
|
-
"" # Empty string signals async to HexaPDF
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
estimated_size = calculate_deferred_signature_size(certificate_chain, timestamp_size)
|
|
136
|
-
|
|
137
|
-
document = HexaPDF::Document.open(file_path)
|
|
138
|
-
handler = document.signatures.signing_handler(
|
|
139
|
-
certificate: certificate,
|
|
140
|
-
certificate_chain: certificate_chain[1..] || [],
|
|
141
|
-
external_signing: external_signing,
|
|
142
|
-
digest_algorithm: digest_algorithm,
|
|
143
|
-
signature_size: estimated_size,
|
|
144
|
-
signing_time: signing_time,
|
|
145
|
-
reason: @signature_config[:reason],
|
|
146
|
-
location: @signature_config[:location],
|
|
147
|
-
contact_info: @signature_config[:contact_info]
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
prepared_path = deferred_output_path
|
|
151
|
-
document.signatures.add(prepared_path, handler)
|
|
152
|
-
|
|
153
|
-
# Read back the ByteRange from the prepared PDF
|
|
154
|
-
byte_range = read_byte_range(prepared_path)
|
|
155
|
-
|
|
156
|
-
# Compute pre-hash signed attributes DER for WebCrypto compatibility
|
|
157
|
-
signed_attrs_der = compute_signed_attributes_data(
|
|
158
|
-
prepared_path, byte_range, certificate, certificate_chain,
|
|
159
|
-
digest_algorithm, signing_time
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
DeferredSigningRequest.new(
|
|
163
|
-
digest: captured_digest,
|
|
164
|
-
digest_algorithm: captured_algorithm,
|
|
165
|
-
prepared_pdf_path: prepared_path,
|
|
166
|
-
byte_range: byte_range,
|
|
167
|
-
certificate: certificate,
|
|
168
|
-
certificate_chain: certificate_chain,
|
|
169
|
-
estimated_size: estimated_size,
|
|
170
|
-
signing_time: signing_time,
|
|
171
|
-
signed_attributes_data: signed_attrs_der
|
|
172
|
-
)
|
|
173
|
-
rescue HexaPDF::Error => e
|
|
174
|
-
raise DeferredSigningError, "Failed to prepare PDF for deferred signing: #{e.message}"
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Phase 2 of deferred signing: rebuild CMS with the real signature and embed it.
|
|
178
|
-
#
|
|
179
|
-
# Re-reads the ByteRange content from the prepared PDF, invokes SignedDataCreator
|
|
180
|
-
# with the same parameters as Phase 1 (including signing_time for determinism),
|
|
181
|
-
# and the block returns the actual raw signature. The resulting CMS DER is embedded
|
|
182
|
-
# into the prepared PDF via Signing.embed_signature.
|
|
183
|
-
#
|
|
184
|
-
# @param deferred_request [DeferredSigningRequest] from Phase 1
|
|
185
|
-
# @param raw_signature [String] raw signature bytes from external signer
|
|
186
|
-
# @return [String] path to the finalized signed PDF
|
|
187
|
-
def finalize_deferred(deferred_request, raw_signature, timestamp_token: nil)
|
|
188
|
-
prepared_path = deferred_request.prepared_pdf_path
|
|
189
|
-
unless File.exist?(prepared_path)
|
|
190
|
-
raise DeferredSigningError, "Prepared PDF not found: #{prepared_path}"
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
byte_range = deferred_request.byte_range
|
|
194
|
-
|
|
195
|
-
# Read ByteRange content from the prepared PDF
|
|
196
|
-
data = File.open(prepared_path, "rb") do |f|
|
|
197
|
-
f.pos = byte_range[0]
|
|
198
|
-
content = f.read(byte_range[1])
|
|
199
|
-
f.pos = byte_range[2]
|
|
200
|
-
content << f.read(byte_range[3])
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Rebuild the CMS structure with the actual signature
|
|
204
|
-
signing_block = lambda do |_digest_algorithm, _hash|
|
|
205
|
-
raw_signature
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
creator = HexaPDF::DigitalSignature::Signing::SignedDataCreator.new
|
|
209
|
-
creator.certificate = deferred_request.certificate
|
|
210
|
-
creator.digest_algorithm = deferred_request.digest_algorithm.to_s
|
|
211
|
-
creator.signing_time = deferred_request.signing_time
|
|
212
|
-
creator.certificates = deferred_request.certificate_chain[1..] || []
|
|
213
|
-
creator.timestamp_handler = Pdf::TimestampHandler.new(timestamp_token) if timestamp_token
|
|
214
|
-
|
|
215
|
-
cms = creator.create(data, type: :cms, &signing_block)
|
|
216
|
-
|
|
217
|
-
cms_der = cms.to_der
|
|
218
|
-
|
|
219
|
-
# Embed the real signature into the prepared PDF
|
|
220
|
-
File.open(prepared_path, "rb+") do |io|
|
|
221
|
-
HexaPDF::DigitalSignature::Signing.embed_signature(io, cms_der)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
prepared_path
|
|
225
|
-
rescue HexaPDF::Error => e
|
|
226
|
-
raise DeferredSigningError, "Failed to finalize deferred signature: #{e.message}"
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Extract existing signature from PDF
|
|
230
|
-
# @return [Hash, nil] signature data or nil if unsigned
|
|
63
|
+
# @return [Hash, nil] :contents (binary DER), :byte_range, :sub_filter — or nil if unsigned
|
|
231
64
|
def extract_signature
|
|
232
|
-
|
|
65
|
+
raw = File.binread(file_path)
|
|
233
66
|
|
|
234
|
-
|
|
235
|
-
|
|
67
|
+
contents_hex = nil
|
|
68
|
+
raw.scan(%r{/Contents\s*<([0-9a-fA-F]*)>}) { |m| contents_hex = m[0] }
|
|
69
|
+
return nil unless contents_hex
|
|
236
70
|
|
|
237
|
-
|
|
238
|
-
|
|
71
|
+
br_str = nil
|
|
72
|
+
raw.scan(%r{/ByteRange\s*\[([^\]]+)\]}) { |m| br_str = m[0] }
|
|
73
|
+
return nil unless br_str
|
|
239
74
|
|
|
240
|
-
|
|
75
|
+
br_values = br_str.split.map(&:to_i)
|
|
76
|
+
return nil unless br_values.size == 4
|
|
241
77
|
|
|
242
78
|
{
|
|
243
|
-
contents:
|
|
244
|
-
byte_range:
|
|
245
|
-
sub_filter:
|
|
246
|
-
reason: sig_dict[:Reason],
|
|
247
|
-
location: sig_dict[:Location],
|
|
248
|
-
contact_info: sig_dict[:ContactInfo],
|
|
249
|
-
signing_time: sig_dict[:M],
|
|
250
|
-
name: sig_dict[:Name]
|
|
79
|
+
contents: [contents_hex].pack("H*"),
|
|
80
|
+
byte_range: br_values,
|
|
81
|
+
sub_filter: raw[%r{/SubFilter\s*/(\S+)}, 1]
|
|
251
82
|
}
|
|
252
83
|
rescue StandardError
|
|
253
84
|
nil
|
|
@@ -261,7 +92,6 @@ module EasyCodeSign
|
|
|
261
92
|
raise InvalidPdfError, "File must be a PDF: #{file_path}"
|
|
262
93
|
end
|
|
263
94
|
|
|
264
|
-
# Verify PDF header
|
|
265
95
|
File.open(file_path, "rb") do |f|
|
|
266
96
|
header = f.read(8)
|
|
267
97
|
unless header&.start_with?("%PDF-")
|
|
@@ -272,215 +102,15 @@ module EasyCodeSign
|
|
|
272
102
|
|
|
273
103
|
def build_signature_config(opts)
|
|
274
104
|
{
|
|
275
|
-
visible:
|
|
276
|
-
page:
|
|
277
|
-
position:
|
|
278
|
-
rect:
|
|
279
|
-
reason:
|
|
280
|
-
location:
|
|
105
|
+
visible: opts.fetch(:visible_signature, false),
|
|
106
|
+
page: opts.fetch(:signature_page, 1),
|
|
107
|
+
position: opts.fetch(:signature_position, :bottom_right),
|
|
108
|
+
rect: opts[:signature_rect],
|
|
109
|
+
reason: opts[:signature_reason],
|
|
110
|
+
location: opts[:signature_location],
|
|
281
111
|
contact_info: opts[:signature_contact]
|
|
282
112
|
}
|
|
283
113
|
end
|
|
284
|
-
|
|
285
|
-
def create_signature_field
|
|
286
|
-
page_index = [@signature_config[:page] - 1, 0].max
|
|
287
|
-
page = @document.pages[page_index] || @document.pages.add
|
|
288
|
-
|
|
289
|
-
# Create signature form field
|
|
290
|
-
form = @document.acro_form(create: true)
|
|
291
|
-
sig_field = form.create_signature_field("Signature1")
|
|
292
|
-
|
|
293
|
-
# Add visible appearance if requested
|
|
294
|
-
if @signature_config[:visible]
|
|
295
|
-
sig_field.create_widget(page, Rect: calculate_signature_rect(page))
|
|
296
|
-
# Appearance will be built during signing
|
|
297
|
-
else
|
|
298
|
-
# Invisible signature
|
|
299
|
-
sig_field.create_widget(page, Rect: [0, 0, 0, 0])
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
sig_field
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def calculate_signature_rect(page)
|
|
306
|
-
if @signature_config[:rect]
|
|
307
|
-
return @signature_config[:rect]
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Calculate position based on preset
|
|
311
|
-
box = page.box(:media)
|
|
312
|
-
width = 200
|
|
313
|
-
height = 50
|
|
314
|
-
margin = 36
|
|
315
|
-
|
|
316
|
-
case @signature_config[:position].to_sym
|
|
317
|
-
when :top_left
|
|
318
|
-
[margin, box.height - margin - height, margin + width, box.height - margin]
|
|
319
|
-
when :top_right
|
|
320
|
-
[box.width - margin - width, box.height - margin - height, box.width - margin, box.height - margin]
|
|
321
|
-
when :bottom_left
|
|
322
|
-
[margin, margin, margin + width, margin + height]
|
|
323
|
-
when :bottom_right
|
|
324
|
-
[box.width - margin - width, margin, box.width - margin, margin + height]
|
|
325
|
-
else
|
|
326
|
-
[box.width - margin - width, margin, box.width - margin, margin + height]
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def calculate_deferred_signature_size(certificate_chain, timestamp_size)
|
|
331
|
-
base_size = 8192
|
|
332
|
-
cert_size = certificate_chain.sum { |c| c.to_der.bytesize }
|
|
333
|
-
base_size + cert_size + timestamp_size
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
def deferred_output_path
|
|
337
|
-
dir = File.dirname(file_path)
|
|
338
|
-
base = File.basename(file_path, File.extname(file_path))
|
|
339
|
-
File.join(dir, "#{base}_prepared.pdf")
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def read_byte_range(pdf_path)
|
|
343
|
-
doc = HexaPDF::Document.open(pdf_path)
|
|
344
|
-
sig = doc.signatures.each.to_a.last
|
|
345
|
-
sig_dict = sig.is_a?(Hash) ? sig : sig
|
|
346
|
-
sig_dict[:ByteRange]&.value
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
# Reconstruct the DER-encoded signed attributes from the prepared PDF.
|
|
350
|
-
# This is the pre-hash data that WebCrypto can hash-and-sign in one step.
|
|
351
|
-
# Invariant: SHA256(result) == captured_digest
|
|
352
|
-
def compute_signed_attributes_data(prepared_path, byte_range, certificate, certificate_chain,
|
|
353
|
-
digest_algorithm, signing_time)
|
|
354
|
-
# Read ByteRange content (same as finalize_deferred does)
|
|
355
|
-
data = File.open(prepared_path, "rb") do |f|
|
|
356
|
-
f.pos = byte_range[0]
|
|
357
|
-
content = f.read(byte_range[1])
|
|
358
|
-
f.pos = byte_range[2]
|
|
359
|
-
content << f.read(byte_range[3])
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# Build a SignedDataCreator with the same params used in Phase 1
|
|
363
|
-
creator = HexaPDF::DigitalSignature::Signing::SignedDataCreator.new
|
|
364
|
-
creator.certificate = certificate
|
|
365
|
-
creator.digest_algorithm = digest_algorithm.to_s
|
|
366
|
-
creator.signing_time = signing_time
|
|
367
|
-
creator.certificates = certificate_chain[1..] || []
|
|
368
|
-
|
|
369
|
-
# Access the private method to get the ASN.1 signed attributes SET
|
|
370
|
-
signed_attrs = creator.send(:create_signed_attrs, data, signing_time: true)
|
|
371
|
-
|
|
372
|
-
# DER-encode the SET (mirrors line 113 of signed_data_creator.rb)
|
|
373
|
-
OpenSSL::ASN1::Set.new(signed_attrs.value).to_der
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
def calculate_signature_size(certificate_chain, timestamp_token)
|
|
377
|
-
# Estimate PKCS#7 signature size
|
|
378
|
-
# Base size + certificates + timestamp
|
|
379
|
-
base_size = 8192
|
|
380
|
-
cert_size = certificate_chain.sum { |c| c.to_der.bytesize }
|
|
381
|
-
timestamp_size = timestamp_token ? 4096 : 0
|
|
382
|
-
|
|
383
|
-
base_size + cert_size + timestamp_size
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def build_visible_appearance(handler, certificate)
|
|
387
|
-
# Get the widget annotation for the signature field
|
|
388
|
-
widget = @signature_field.each_widget.first
|
|
389
|
-
return unless widget
|
|
390
|
-
|
|
391
|
-
rect = widget[:Rect].value
|
|
392
|
-
width = rect[2] - rect[0]
|
|
393
|
-
height = rect[3] - rect[1]
|
|
394
|
-
|
|
395
|
-
# Create appearance form XObject
|
|
396
|
-
form = @document.add({ Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height] })
|
|
397
|
-
canvas = form.canvas
|
|
398
|
-
|
|
399
|
-
# Draw border
|
|
400
|
-
canvas.stroke_color(0, 0, 0)
|
|
401
|
-
canvas.line_width(0.5)
|
|
402
|
-
canvas.rectangle(0.5, 0.5, width - 1, height - 1)
|
|
403
|
-
canvas.stroke
|
|
404
|
-
|
|
405
|
-
# Draw signature text
|
|
406
|
-
canvas.font("Helvetica", size: 8)
|
|
407
|
-
canvas.fill_color(0, 0, 0)
|
|
408
|
-
|
|
409
|
-
y_pos = height - 12
|
|
410
|
-
x_pos = 5
|
|
411
|
-
|
|
412
|
-
# Signer name
|
|
413
|
-
signer_name = extract_cn_from_certificate(certificate)
|
|
414
|
-
canvas.text("Digitally signed by:", at: [x_pos, y_pos])
|
|
415
|
-
y_pos -= 10
|
|
416
|
-
canvas.text(signer_name, at: [x_pos, y_pos])
|
|
417
|
-
y_pos -= 12
|
|
418
|
-
|
|
419
|
-
# Reason
|
|
420
|
-
if @signature_config[:reason]
|
|
421
|
-
canvas.text("Reason: #{@signature_config[:reason]}", at: [x_pos, y_pos])
|
|
422
|
-
y_pos -= 10
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
# Location
|
|
426
|
-
if @signature_config[:location]
|
|
427
|
-
canvas.text("Location: #{@signature_config[:location]}", at: [x_pos, y_pos])
|
|
428
|
-
y_pos -= 10
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
# Date
|
|
432
|
-
canvas.text("Date: #{Time.now.strftime('%Y-%m-%d %H:%M')}", at: [x_pos, y_pos])
|
|
433
|
-
|
|
434
|
-
# Set the appearance
|
|
435
|
-
widget[:AP] = { N: form }
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def extract_cn_from_certificate(certificate)
|
|
439
|
-
subject = certificate.subject.to_a
|
|
440
|
-
cn = subject.find { |name, _, _| name == "CN" }
|
|
441
|
-
cn ? cn[1] : certificate.subject.to_s
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
# Callback-based signing for HSM integration
|
|
446
|
-
# HexaPDF calls #sign with the actual data to sign (ByteRange content)
|
|
447
|
-
class ExternalSigningCallback
|
|
448
|
-
def initialize(signing_proc)
|
|
449
|
-
@signing_proc = signing_proc
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
# Called by HexaPDF during signing with the ByteRange content
|
|
453
|
-
def sign(data, _digest_algorithm)
|
|
454
|
-
@signing_proc.call(data)
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
def private?
|
|
458
|
-
true
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Proxy object that provides pre-computed signature to HexaPDF
|
|
463
|
-
# HexaPDF expects a key object that responds to #sign, but we've already
|
|
464
|
-
# signed with the HSM, so we return the pre-computed signature
|
|
465
|
-
class ExternalSigningProxy
|
|
466
|
-
attr_reader :certificate
|
|
467
|
-
|
|
468
|
-
def initialize(signature, certificate, certificate_chain)
|
|
469
|
-
@signature = signature
|
|
470
|
-
@certificate = certificate
|
|
471
|
-
@certificate_chain = certificate_chain
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# Called by HexaPDF's DefaultHandler during signing
|
|
475
|
-
# Returns pre-computed signature from HSM
|
|
476
|
-
def sign(data, digest_algorithm)
|
|
477
|
-
@signature
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
# HexaPDF checks this for RSA keys
|
|
481
|
-
def private?
|
|
482
|
-
true
|
|
483
|
-
end
|
|
484
114
|
end
|
|
485
115
|
end
|
|
486
116
|
end
|
data/lib/easy_code_sign.rb
CHANGED
|
@@ -10,8 +10,6 @@ require_relative "easy_code_sign/signable/base"
|
|
|
10
10
|
require_relative "easy_code_sign/signable/gem_file"
|
|
11
11
|
require_relative "easy_code_sign/signable/zip_file"
|
|
12
12
|
require_relative "easy_code_sign/signable/pdf_file"
|
|
13
|
-
require_relative "easy_code_sign/pdf/timestamp_handler"
|
|
14
|
-
require_relative "easy_code_sign/pdf/appearance_builder"
|
|
15
13
|
require_relative "easy_code_sign/timestamp/request"
|
|
16
14
|
require_relative "easy_code_sign/timestamp/response"
|
|
17
15
|
require_relative "easy_code_sign/timestamp/client"
|