hexapdf 0.28.0 → 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/configuration.rb +12 -12
- data/lib/hexapdf/dictionary_fields.rb +13 -4
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document/pages.rb +35 -18
- data/lib/hexapdf/document.rb +21 -14
- data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
- data/lib/hexapdf/type/font_simple.rb +14 -2
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- data/test/hexapdf/document/test_pages.rb +25 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
- data/test/hexapdf/test_dictionary_fields.rb +9 -3
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +6 -6
- data/test/hexapdf/type/test_font_simple.rb +18 -6
- metadata +25 -15
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -1,546 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
2
|
-
#
|
3
|
-
#--
|
4
|
-
# This file is part of HexaPDF.
|
5
|
-
#
|
6
|
-
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
-
# Copyright (C) 2014-2022 Thomas Leitner
|
8
|
-
#
|
9
|
-
# HexaPDF is free software: you can redistribute it and/or modify it
|
10
|
-
# under the terms of the GNU Affero General Public License version 3 as
|
11
|
-
# published by the Free Software Foundation with the addition of the
|
12
|
-
# following permission added to Section 15 as permitted in Section 7(a):
|
13
|
-
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
14
|
-
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
15
|
-
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
16
|
-
#
|
17
|
-
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
18
|
-
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
19
|
-
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
20
|
-
# License for more details.
|
21
|
-
#
|
22
|
-
# You should have received a copy of the GNU Affero General Public License
|
23
|
-
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
24
|
-
#
|
25
|
-
# The interactive user interfaces in modified source and object code
|
26
|
-
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
27
|
-
# under Section 5 of the GNU Affero General Public License version 3.
|
28
|
-
#
|
29
|
-
# In accordance with Section 7(b) of the GNU Affero General Public
|
30
|
-
# License, a covered work must retain the producer line in every PDF that
|
31
|
-
# is created or manipulated using HexaPDF.
|
32
|
-
#
|
33
|
-
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
-
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
-
#++
|
36
|
-
|
37
|
-
require 'openssl'
|
38
|
-
require 'net/http'
|
39
|
-
require 'hexapdf/error'
|
40
|
-
require 'stringio'
|
41
|
-
|
42
|
-
module HexaPDF
|
43
|
-
class Document
|
44
|
-
|
45
|
-
# This class provides methods for interacting with digital signatures of a PDF file.
|
46
|
-
class Signatures
|
47
|
-
|
48
|
-
# This is the default signing handler which provides the ability to sign a document with the
|
49
|
-
# adbe.pkcs7.detached or ETSI.CAdES.detached algorithms. It is registered under the :default
|
50
|
-
# name.
|
51
|
-
#
|
52
|
-
# == Usage
|
53
|
-
#
|
54
|
-
# The signing handler is used by default by all methods that need a signing handler. Therefore
|
55
|
-
# it is usually only necessary to provide the actual attribute values.
|
56
|
-
#
|
57
|
-
# This handler provides two ways to create the PKCS#7 signed-data structure required by
|
58
|
-
# Signatures#add:
|
59
|
-
#
|
60
|
-
# * By providing the signing certificate together with the signing key and the certificate
|
61
|
-
# chain. This way HexaPDF itself does the signing. It is the preferred way if all the needed
|
62
|
-
# information is available.
|
63
|
-
#
|
64
|
-
# Assign the respective data to the #certificate, #key and #certificate_chain attributes.
|
65
|
-
#
|
66
|
-
# * By using an external signing mechanism. Here the actual signing happens "outside" of
|
67
|
-
# HexaPDF, for example, in custom code or even asynchronously. This is needed in case the
|
68
|
-
# signing certificate plus key are not directly available but only an interface to them
|
69
|
-
# (e.g. when dealing with a HSM).
|
70
|
-
#
|
71
|
-
# Assign a callable object to #external_signing. If the signing process needs to be
|
72
|
-
# asynchronous, make sure to set the #signature_size appropriately, return an empty string
|
73
|
-
# during signing and later use Signatures.embed_signature to embed the actual signature.
|
74
|
-
#
|
75
|
-
# Additional functionality:
|
76
|
-
#
|
77
|
-
# * Optionally setting the reason, location and contact information.
|
78
|
-
# * Making the signature a certification signature by applying the DocMDP transform method.
|
79
|
-
#
|
80
|
-
# Example:
|
81
|
-
#
|
82
|
-
# # Signing using certificate + key
|
83
|
-
# document.sign("output.pdf", certificate: my_cert, key: my_key,
|
84
|
-
# certificate_chain: my_chain)
|
85
|
-
#
|
86
|
-
# # Signing using an external mechanism:
|
87
|
-
# signing_proc = lambda do |io, byte_range|
|
88
|
-
# io.pos = byte_range[0]
|
89
|
-
# data = io.read(byte_range[1])
|
90
|
-
# io.pos = byte_range[2]
|
91
|
-
# data << io.read(byte_range[3])
|
92
|
-
# signing_service.pkcs7_sign(data)
|
93
|
-
# end
|
94
|
-
# document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc)
|
95
|
-
#
|
96
|
-
# == Implementing a Signing Handler
|
97
|
-
#
|
98
|
-
# This class also serves as an example on how to create a custom handler: The public methods
|
99
|
-
# #signature_size, #finalize_objects and #sign are used by the digital signature algorithm.
|
100
|
-
# See their descriptions for details.
|
101
|
-
#
|
102
|
-
# Once a custom signing handler has been created, it can be registered under the
|
103
|
-
# 'signature.signing_handler' configuration option for easy use. It has to take keyword
|
104
|
-
# arguments in its initialize method to be compatible with the Signatures#handler method.
|
105
|
-
class DefaultHandler
|
106
|
-
|
107
|
-
# The certificate with which to sign the PDF.
|
108
|
-
attr_accessor :certificate
|
109
|
-
|
110
|
-
# The private key for the #certificate.
|
111
|
-
attr_accessor :key
|
112
|
-
|
113
|
-
# The certificate chain that should be embedded in the PDF; normally contains all
|
114
|
-
# certificates up to the root certificate.
|
115
|
-
attr_accessor :certificate_chain
|
116
|
-
|
117
|
-
# A callable object fulfilling the same role as the #sign method that is used instead of the
|
118
|
-
# default mechanism for signing.
|
119
|
-
#
|
120
|
-
# If this attribute is set, the attributes #certificate, #key and #certificate_chain are not
|
121
|
-
# used.
|
122
|
-
attr_accessor :external_signing
|
123
|
-
|
124
|
-
# The reason for signing. If used, will be set on the signature object.
|
125
|
-
attr_accessor :reason
|
126
|
-
|
127
|
-
# The signing location. If used, will be set on the signature object.
|
128
|
-
attr_accessor :location
|
129
|
-
|
130
|
-
# The contact information. If used, will be set on the signature object.
|
131
|
-
attr_accessor :contact_info
|
132
|
-
|
133
|
-
# The size of the serialized signature that should be reserved.
|
134
|
-
#
|
135
|
-
# If this attribute has not been set, an empty string will be signed using #sign to
|
136
|
-
# determine the signature size.
|
137
|
-
#
|
138
|
-
# The size needs to be at least as big as the final signature, otherwise signing results in
|
139
|
-
# an error.
|
140
|
-
attr_writer :signature_size
|
141
|
-
|
142
|
-
# The type of signature to be written (i.e. the value of the /SubFilter key).
|
143
|
-
#
|
144
|
-
# The value can either be :adobe (the default; uses a detached PKCS7 signature) or :etsi
|
145
|
-
# (uses an ETSI CAdES compatible signature).
|
146
|
-
attr_accessor :signature_type
|
147
|
-
|
148
|
-
# The DocMDP permissions that should be set on the document.
|
149
|
-
#
|
150
|
-
# See #doc_mdp_permissions=
|
151
|
-
attr_reader :doc_mdp_permissions
|
152
|
-
|
153
|
-
# Creates a new DefaultHandler with the given attributes.
|
154
|
-
def initialize(**arguments)
|
155
|
-
@signature_size = nil
|
156
|
-
arguments.each {|name, value| send("#{name}=", value) }
|
157
|
-
end
|
158
|
-
|
159
|
-
# Sets the DocMDP permissions that should be applied to the document.
|
160
|
-
#
|
161
|
-
# Valid values for +permissions+ are:
|
162
|
-
#
|
163
|
-
# +nil+::
|
164
|
-
# Don't set any DocMDP permissions (default).
|
165
|
-
#
|
166
|
-
# +:no_changes+ or 1::
|
167
|
-
# No changes whatsoever are allowed.
|
168
|
-
#
|
169
|
-
# +:form_filling+ or 2::
|
170
|
-
# Only filling in forms and signing are allowed.
|
171
|
-
#
|
172
|
-
# +:form_filling_and_annotations+ or 3::
|
173
|
-
# Only filling in forms, signing and annotation creation/deletion/modification are
|
174
|
-
# allowed.
|
175
|
-
def doc_mdp_permissions=(permissions)
|
176
|
-
case permissions
|
177
|
-
when :no_changes, 1 then @doc_mdp_permissions = 1
|
178
|
-
when :form_filling, 2 then @doc_mdp_permissions = 2
|
179
|
-
when :form_filling_and_annotations, 3 then @doc_mdp_permissions = 3
|
180
|
-
when nil then @doc_mdp_permissions = nil
|
181
|
-
else
|
182
|
-
raise ArgumentError, "Invalid permissions value '#{permissions.inspect}'"
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Returns the size of the serialized signature that should be reserved.
|
187
|
-
#
|
188
|
-
# If a custom size is set using #signature_size=, it used. Otherwise the size is determined
|
189
|
-
# by using #sign to sign an empty string.
|
190
|
-
def signature_size
|
191
|
-
@signature_size || sign(StringIO.new, [0, 0, 0, 0]).size
|
192
|
-
end
|
193
|
-
|
194
|
-
# Finalizes the signature field as well as the signature dictionary before writing.
|
195
|
-
def finalize_objects(_signature_field, signature)
|
196
|
-
signature[:SubFilter] = :'ETSI.CAdES.detached' if signature_type == :etsi
|
197
|
-
signature[:Reason] = reason if reason
|
198
|
-
signature[:Location] = location if location
|
199
|
-
signature[:ContactInfo] = contact_info if contact_info
|
200
|
-
|
201
|
-
if doc_mdp_permissions
|
202
|
-
doc = signature.document
|
203
|
-
if doc.signatures.count > 1
|
204
|
-
raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature"
|
205
|
-
end
|
206
|
-
params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions})
|
207
|
-
sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA1,
|
208
|
-
TransformParams: params})
|
209
|
-
signature[:Reference] = [sigref]
|
210
|
-
(doc.catalog[:Perms] ||= {})[:DocMDP] = signature
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
# Returns the DER serialized OpenSSL::PKCS7 structure containing the signature for the given
|
215
|
-
# IO byte ranges.
|
216
|
-
#
|
217
|
-
# The +byte_range+ argument is an array containing four numbers [offset1, length1, offset2,
|
218
|
-
# length2]. The offset numbers are byte positions in the +io+ argument and the to-be-signed
|
219
|
-
# data can be determined by reading length bytes at the offsets.
|
220
|
-
def sign(io, byte_range)
|
221
|
-
if external_signing
|
222
|
-
external_signing.call(io, byte_range)
|
223
|
-
else
|
224
|
-
io.pos = byte_range[0]
|
225
|
-
data = io.read(byte_range[1])
|
226
|
-
io.pos = byte_range[2]
|
227
|
-
data << io.read(byte_range[3])
|
228
|
-
OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
|
229
|
-
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
end
|
234
|
-
|
235
|
-
# This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
|
236
|
-
# document. It is registered under the :timestamp name.
|
237
|
-
#
|
238
|
-
# The timestamp is provided by a timestamp authority and establishes the document contents at
|
239
|
-
# the time indicated in the timestamp. Timestamping a PDF document is usually done in context
|
240
|
-
# of long term validation but can also be done standalone.
|
241
|
-
#
|
242
|
-
# == Usage
|
243
|
-
#
|
244
|
-
# It is necessary to provide at least the URL of the timestamp authority server (TSA) via
|
245
|
-
# #tsa_url, everything else is optional and uses default values. The TSA server must not use
|
246
|
-
# authentication to be usable.
|
247
|
-
#
|
248
|
-
# Example:
|
249
|
-
#
|
250
|
-
# document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
|
251
|
-
class TimestampHandler
|
252
|
-
|
253
|
-
# The URL of the timestamp authority server.
|
254
|
-
#
|
255
|
-
# This value is required.
|
256
|
-
attr_accessor :tsa_url
|
257
|
-
|
258
|
-
# The hash algorithm to use for timestamping. Defaults to SHA512.
|
259
|
-
attr_accessor :tsa_hash_algorithm
|
260
|
-
|
261
|
-
# The policy OID to use for timestamping. Defaults to +nil+.
|
262
|
-
attr_accessor :tsa_policy_id
|
263
|
-
|
264
|
-
# The size of the serialized signature that should be reserved.
|
265
|
-
#
|
266
|
-
# If this attribute has not been set, an empty string will be signed using #sign to
|
267
|
-
# determine the signature size which will contact the TSA server
|
268
|
-
#
|
269
|
-
# The size needs to be at least as big as the final signature, otherwise signing results in
|
270
|
-
# an error.
|
271
|
-
attr_writer :signature_size
|
272
|
-
|
273
|
-
# The reason for timestamping. If used, will be set on the signature object.
|
274
|
-
attr_accessor :reason
|
275
|
-
|
276
|
-
# The timestamping location. If used, will be set on the signature object.
|
277
|
-
attr_accessor :location
|
278
|
-
|
279
|
-
# The contact information. If used, will be set on the signature object.
|
280
|
-
attr_accessor :contact_info
|
281
|
-
|
282
|
-
# Creates a new TimestampHandler with the given attributes.
|
283
|
-
def initialize(**arguments)
|
284
|
-
@signature_size = nil
|
285
|
-
arguments.each {|name, value| send("#{name}=", value) }
|
286
|
-
end
|
287
|
-
|
288
|
-
# Returns the size of the serialized signature that should be reserved.
|
289
|
-
def signature_size
|
290
|
-
@signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
|
291
|
-
end
|
292
|
-
|
293
|
-
# Finalizes the signature field as well as the signature dictionary before writing.
|
294
|
-
def finalize_objects(_signature_field, signature)
|
295
|
-
signature.document.version = '2.0'
|
296
|
-
signature[:Type] = :DocTimeStamp
|
297
|
-
signature[:SubFilter] = :'ETSI.RFC3161'
|
298
|
-
signature[:Reason] = reason if reason
|
299
|
-
signature[:Location] = location if location
|
300
|
-
signature[:ContactInfo] = contact_info if contact_info
|
301
|
-
end
|
302
|
-
|
303
|
-
# Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
|
304
|
-
# given IO byte ranges.
|
305
|
-
def sign(io, byte_range)
|
306
|
-
hash_algorithm = tsa_hash_algorithm || 'SHA512'
|
307
|
-
digest = OpenSSL::Digest.new(hash_algorithm)
|
308
|
-
io.pos = byte_range[0]
|
309
|
-
digest << io.read(byte_range[1])
|
310
|
-
io.pos = byte_range[2]
|
311
|
-
digest << io.read(byte_range[3])
|
312
|
-
|
313
|
-
req = OpenSSL::Timestamp::Request.new
|
314
|
-
req.algorithm = hash_algorithm
|
315
|
-
req.message_imprint = digest.digest
|
316
|
-
req.policy_id = tsa_policy_id if tsa_policy_id
|
317
|
-
|
318
|
-
http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
|
319
|
-
'content-type' => 'application/timestamp-query')
|
320
|
-
if http_response.kind_of?(Net::HTTPOK)
|
321
|
-
response = OpenSSL::Timestamp::Response.new(http_response.body)
|
322
|
-
if response.status == 0
|
323
|
-
response.token.to_der
|
324
|
-
else
|
325
|
-
raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
|
326
|
-
end
|
327
|
-
else
|
328
|
-
raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
end
|
333
|
-
|
334
|
-
# Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
|
335
|
-
# the PDF document given by the +io+ argument.
|
336
|
-
#
|
337
|
-
# This functionality can be used together with the support for external signing (see
|
338
|
-
# DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
|
339
|
-
#
|
340
|
-
# Note: This will, most probably, only work on documents prepared for external signing by
|
341
|
-
# HexaPDF and not by other libraries.
|
342
|
-
def self.embed_signature(io, signature)
|
343
|
-
doc = HexaPDF::Document.new(io: io)
|
344
|
-
signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
|
345
|
-
signature_dict_offset, signature_dict_length = locate_signature_dict(
|
346
|
-
doc.revisions.current.xref_section,
|
347
|
-
doc.revisions.parser.startxref_offset,
|
348
|
-
signature_dict.oid
|
349
|
-
)
|
350
|
-
io.pos = signature_dict_offset
|
351
|
-
signature_data = io.read(signature_dict_length)
|
352
|
-
replace_signature_contents(signature_data, signature)
|
353
|
-
io.pos = signature_dict_offset
|
354
|
-
io.write(signature_data)
|
355
|
-
end
|
356
|
-
|
357
|
-
# Uses the information in the given cross-reference section as well as the byte offset of the
|
358
|
-
# cross-reference section to calculate the offset and length of the signature dictionary with
|
359
|
-
# the given object id.
|
360
|
-
def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
|
361
|
-
data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
|
362
|
-
[start_xref_position, nil]
|
363
|
-
index = data.index {|_pos, oid| oid == signature_oid }
|
364
|
-
[data[index][0], data[index + 1][0] - data[index][0]]
|
365
|
-
end
|
366
|
-
|
367
|
-
# Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
|
368
|
-
# +contents+.
|
369
|
-
def self.replace_signature_contents(signature_data, contents)
|
370
|
-
signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
|
371
|
-
length = match.size
|
372
|
-
result = "Contents<#{contents.unpack1('H*')}"
|
373
|
-
if length < result.size
|
374
|
-
raise HexaPDF::Error, "The reserved space for the signature was too small " \
|
375
|
-
"(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
|
376
|
-
"#signature_size method to increase the reserved space"
|
377
|
-
end
|
378
|
-
"#{result.ljust(length - 1, '0')}>"
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
include Enumerable
|
383
|
-
|
384
|
-
# Creates a new Signatures object for the given PDF document.
|
385
|
-
def initialize(document)
|
386
|
-
@document = document
|
387
|
-
end
|
388
|
-
|
389
|
-
# Creates a signing handler with the given attributes and returns it.
|
390
|
-
#
|
391
|
-
# A signing handler name is mapped to a class via the 'signature.signing_handler'
|
392
|
-
# configuration option. The default signing handler is DefaultHandler.
|
393
|
-
def handler(name: :default, **attributes)
|
394
|
-
handler = @document.config.constantize('signature.signing_handler', name) do
|
395
|
-
raise HexaPDF::Error, "No signing handler named '#{name}' is available"
|
396
|
-
end
|
397
|
-
handler.new(**attributes)
|
398
|
-
end
|
399
|
-
|
400
|
-
# Adds a signature to the document and returns the corresponding signature object.
|
401
|
-
#
|
402
|
-
# This method will add a new signature to the document and write the updated document to the
|
403
|
-
# given file or IO stream. Afterwards the document can't be modified anymore and still retain
|
404
|
-
# a correct digital signature. To modify the signed document (e.g. for adding another
|
405
|
-
# signature) create a new document based on the given file or IO stream instead.
|
406
|
-
#
|
407
|
-
# +signature+::
|
408
|
-
# Can either be a signature object (determined via the /Type key), a signature field or
|
409
|
-
# +nil+. Providing a signature object or signature field provides for more control, e.g.:
|
410
|
-
#
|
411
|
-
# * Setting values for optional signature object fields like /Reason and /Location.
|
412
|
-
# * (In)directly specifying which signature field should be used.
|
413
|
-
#
|
414
|
-
# If a signature object is provided and it is not associated with an AcroForm signature
|
415
|
-
# field, a new signature field is created and added to the main AcroForm object, creating
|
416
|
-
# that if necessary.
|
417
|
-
#
|
418
|
-
# If a signature field is provided and it already has a signature object as field value,
|
419
|
-
# that signature object is discarded.
|
420
|
-
#
|
421
|
-
# If the signature field doesn't have a widget, a non-visible one is created on the first
|
422
|
-
# page.
|
423
|
-
#
|
424
|
-
# +handler+::
|
425
|
-
# The signing handler that provides the necessary methods for signing and adjusting the
|
426
|
-
# signature and signature field objects to one's liking, see #handler and DefaultHandler.
|
427
|
-
#
|
428
|
-
# +write_options+::
|
429
|
-
# The key-value pairs of this hash will be passed on to the HexaPDF::Document#write
|
430
|
-
# method. Note that +incremental+ will be automatically set to ensure proper behaviour.
|
431
|
-
#
|
432
|
-
# The used signature object will have the following default values set:
|
433
|
-
#
|
434
|
-
# /Filter:: /Adobe.PPKLite
|
435
|
-
# /SubFilter:: /adbe.pkcs7.detached
|
436
|
-
# /M:: The current time.
|
437
|
-
#
|
438
|
-
# These values can be overridden in the #finalize_objects method of the signature handler.
|
439
|
-
def add(file_or_io, handler, signature: nil, write_options: {})
|
440
|
-
if signature && signature.type != :Sig
|
441
|
-
signature_field = signature
|
442
|
-
signature = signature_field.field_value
|
443
|
-
end
|
444
|
-
signature ||= @document.add({Type: :Sig})
|
445
|
-
|
446
|
-
# Prepare AcroForm
|
447
|
-
form = @document.acro_form(create: true)
|
448
|
-
form.signature_flag(:signatures_exist, :append_only)
|
449
|
-
|
450
|
-
# Prepare signature field
|
451
|
-
signature_field ||= form.each_field.find {|field| field.field_value == signature } ||
|
452
|
-
form.create_signature_field(generate_field_name)
|
453
|
-
signature_field.field_value = signature
|
454
|
-
|
455
|
-
if signature_field.each_widget.to_a.empty?
|
456
|
-
signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
|
457
|
-
end
|
458
|
-
|
459
|
-
# Prepare signature object
|
460
|
-
signature[:Filter] = :'Adobe.PPKLite'
|
461
|
-
signature[:SubFilter] = :'adbe.pkcs7.detached'
|
462
|
-
signature[:M] = Time.now
|
463
|
-
handler.finalize_objects(signature_field, signature)
|
464
|
-
signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
|
465
|
-
signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
|
466
|
-
|
467
|
-
io = if file_or_io.kind_of?(String)
|
468
|
-
File.open(file_or_io, 'wb+')
|
469
|
-
else
|
470
|
-
file_or_io
|
471
|
-
end
|
472
|
-
|
473
|
-
# Save the current state so that we can determine the correct /ByteRange value and set the
|
474
|
-
# values
|
475
|
-
start_xref, section = @document.write(io, incremental: true, **write_options)
|
476
|
-
signature_offset, signature_length = self.class.locate_signature_dict(section, start_xref,
|
477
|
-
signature.oid)
|
478
|
-
io.pos = signature_offset
|
479
|
-
signature_data = io.read(signature_length)
|
480
|
-
|
481
|
-
io.seek(0, IO::SEEK_END)
|
482
|
-
file_size = io.pos
|
483
|
-
|
484
|
-
# Calculate the offsets for the /ByteRange
|
485
|
-
contents_offset = signature_offset + signature_data.index('Contents(') + 8
|
486
|
-
offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
|
487
|
-
length2 = file_size - offset2
|
488
|
-
signature[:ByteRange] = [0, contents_offset, offset2, length2]
|
489
|
-
|
490
|
-
# Set the correct /ByteRange value
|
491
|
-
signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
|
492
|
-
length = match.size
|
493
|
-
result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
|
494
|
-
result.ljust(length)
|
495
|
-
end
|
496
|
-
|
497
|
-
# Now everything besides the /Contents value is correct, so we can read the contents for
|
498
|
-
# signing
|
499
|
-
io.pos = signature_offset
|
500
|
-
io.write(signature_data)
|
501
|
-
signature[:Contents] = handler.sign(io, signature[:ByteRange].value)
|
502
|
-
|
503
|
-
# And now replace the /Contents value
|
504
|
-
self.class.replace_signature_contents(signature_data, signature[:Contents])
|
505
|
-
io.pos = signature_offset
|
506
|
-
io.write(signature_data)
|
507
|
-
|
508
|
-
signature
|
509
|
-
ensure
|
510
|
-
io.close if io && io != file_or_io
|
511
|
-
end
|
512
|
-
|
513
|
-
# :call-seq:
|
514
|
-
# signatures.each {|signature| block } -> signatures
|
515
|
-
# signatures.each -> Enumerator
|
516
|
-
#
|
517
|
-
# Iterates over all signatures in the order they are found.
|
518
|
-
def each
|
519
|
-
return to_enum(__method__) unless block_given?
|
520
|
-
|
521
|
-
return [] unless (form = @document.acro_form)
|
522
|
-
form.each_field do |field|
|
523
|
-
yield(field.field_value) if field.field_type == :Sig && field.field_value
|
524
|
-
end
|
525
|
-
end
|
526
|
-
|
527
|
-
# Returns the number of signatures in the PDF document. May be zero if the document has no
|
528
|
-
# signatures.
|
529
|
-
def count
|
530
|
-
each.to_a.size
|
531
|
-
end
|
532
|
-
|
533
|
-
private
|
534
|
-
|
535
|
-
# Generates a field name for a signature field.
|
536
|
-
def generate_field_name
|
537
|
-
index = (@document.acro_form.each_field.
|
538
|
-
map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
|
539
|
-
max || 0) + 1
|
540
|
-
"Signature#{index}"
|
541
|
-
end
|
542
|
-
|
543
|
-
end
|
544
|
-
|
545
|
-
end
|
546
|
-
end
|
@@ -1,135 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
2
|
-
#
|
3
|
-
#--
|
4
|
-
# This file is part of HexaPDF.
|
5
|
-
#
|
6
|
-
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
-
# Copyright (C) 2014-2022 Thomas Leitner
|
8
|
-
#
|
9
|
-
# HexaPDF is free software: you can redistribute it and/or modify it
|
10
|
-
# under the terms of the GNU Affero General Public License version 3 as
|
11
|
-
# published by the Free Software Foundation with the addition of the
|
12
|
-
# following permission added to Section 15 as permitted in Section 7(a):
|
13
|
-
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
14
|
-
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
15
|
-
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
16
|
-
#
|
17
|
-
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
18
|
-
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
19
|
-
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
20
|
-
# License for more details.
|
21
|
-
#
|
22
|
-
# You should have received a copy of the GNU Affero General Public License
|
23
|
-
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
24
|
-
#
|
25
|
-
# The interactive user interfaces in modified source and object code
|
26
|
-
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
27
|
-
# under Section 5 of the GNU Affero General Public License version 3.
|
28
|
-
#
|
29
|
-
# In accordance with Section 7(b) of the GNU Affero General Public
|
30
|
-
# License, a covered work must retain the producer line in every PDF that
|
31
|
-
# is created or manipulated using HexaPDF.
|
32
|
-
#
|
33
|
-
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
-
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
-
#++
|
36
|
-
|
37
|
-
require 'openssl'
|
38
|
-
require 'hexapdf/type/signature'
|
39
|
-
|
40
|
-
module HexaPDF
|
41
|
-
module Type
|
42
|
-
class Signature
|
43
|
-
|
44
|
-
# The signature handler for the adbe.pkcs7.detached sub-filter.
|
45
|
-
class AdbePkcs7Detached < Handler
|
46
|
-
|
47
|
-
# Creates a new signature handler for the given signature dictionary.
|
48
|
-
def initialize(signature_dict)
|
49
|
-
super
|
50
|
-
@pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns the common name of the signer.
|
54
|
-
def signer_name
|
55
|
-
signer_certificate.subject.to_a.assoc("CN")&.[](1) || super
|
56
|
-
end
|
57
|
-
|
58
|
-
# Returns the time of signing.
|
59
|
-
def signing_time
|
60
|
-
signer_info.signed_time rescue super
|
61
|
-
end
|
62
|
-
|
63
|
-
# Returns the certificate chain.
|
64
|
-
def certificate_chain
|
65
|
-
@pkcs7.certificates
|
66
|
-
end
|
67
|
-
|
68
|
-
# Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
|
69
|
-
def signer_certificate
|
70
|
-
info = signer_info
|
71
|
-
certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial }
|
72
|
-
end
|
73
|
-
|
74
|
-
# Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo).
|
75
|
-
def signer_info
|
76
|
-
@pkcs7.signers.first
|
77
|
-
end
|
78
|
-
|
79
|
-
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
80
|
-
def verify(store, allow_self_signed: false)
|
81
|
-
result = super
|
82
|
-
|
83
|
-
signer_info = self.signer_info
|
84
|
-
signer_certificate = self.signer_certificate
|
85
|
-
certificate_chain = self.certificate_chain
|
86
|
-
|
87
|
-
if certificate_chain.empty?
|
88
|
-
result.log(:error, "No certificates found in signature")
|
89
|
-
return result
|
90
|
-
end
|
91
|
-
|
92
|
-
if @pkcs7.signers.size != 1
|
93
|
-
result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}")
|
94
|
-
end
|
95
|
-
|
96
|
-
unless signer_certificate
|
97
|
-
result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \
|
98
|
-
"not found in certificates stored in PKCS7 object")
|
99
|
-
return result
|
100
|
-
end
|
101
|
-
|
102
|
-
key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
|
103
|
-
unless key_usage && key_usage.value.split(', ').include?("Digital Signature")
|
104
|
-
result.log(:error, "Certificate key usage is missing 'Digital Signature'")
|
105
|
-
end
|
106
|
-
|
107
|
-
if signature_dict.signature_type == 'ETSI.RFC3161'
|
108
|
-
# Getting the needed values is not directly supported by Ruby OpenSSL
|
109
|
-
p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
|
110
|
-
signed_data = p7.value[1].value[0]
|
111
|
-
content_info = signed_data.value[2]
|
112
|
-
content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
|
113
|
-
digest_algorithm = content.value[2].value[0].value[0].value
|
114
|
-
original_hash = content.value[2].value[1].value
|
115
|
-
recomputed_hash = OpenSSL::Digest.digest(digest_algorithm, signature_dict.signed_data)
|
116
|
-
hash_valid = (original_hash == recomputed_hash)
|
117
|
-
else
|
118
|
-
data = signature_dict.signed_data
|
119
|
-
hash_valid = true # hash will be checked by @pkcs7.verify
|
120
|
-
end
|
121
|
-
if hash_valid && @pkcs7.verify(certificate_chain, store, data,
|
122
|
-
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
123
|
-
result.log(:info, "Signature valid")
|
124
|
-
else
|
125
|
-
result.log(:error, "Signature verification failed")
|
126
|
-
end
|
127
|
-
|
128
|
-
result
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
132
|
-
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|