hexapdf 0.28.0 → 0.29.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 +41 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/configuration.rb +12 -12
- data/lib/hexapdf/dictionary_fields.rb +6 -2
- 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.rb +21 -14
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- 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/test_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +3 -3
- 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
|