hexapdf 0.27.0 → 0.29.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 +100 -11
- data/examples/019-acro_form.rb +14 -3
- data/examples/023-images.rb +30 -0
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +2 -2
- data/lib/hexapdf/configuration.rb +13 -14
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +1 -5
- 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 +27 -24
- data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +32 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +13 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +4 -0
- data/lib/hexapdf/tokenizer.rb +14 -8
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +33 -7
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +1 -1
- data/lib/hexapdf/type/page.rb +56 -46
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +2 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +34 -4
- 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 +2 -2
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_dictionary_fields.rb +2 -1
- data/test/hexapdf/test_document.rb +3 -9
- data/test/hexapdf/test_importer.rb +13 -6
- data/test/hexapdf/test_parser.rb +17 -0
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +43 -0
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +3 -4
- data/test/hexapdf/test_writer.rb +3 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +18 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +3 -0
- data/test/hexapdf/type/test_page.rb +67 -30
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- metadata +69 -16
- 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
@@ -0,0 +1,317 @@
|
|
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/error'
|
39
|
+
require 'hexapdf/version'
|
40
|
+
require 'stringio'
|
41
|
+
|
42
|
+
module HexaPDF
|
43
|
+
module DigitalSignature
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
# This is the default signing handler which provides the ability to sign a document with the
|
47
|
+
# adbe.pkcs7.detached or ETSI.CAdES.detached algorithms. It is registered under the :default
|
48
|
+
# name.
|
49
|
+
#
|
50
|
+
# == Usage
|
51
|
+
#
|
52
|
+
# The signing handler is used by default by all methods that need a signing handler. Therefore
|
53
|
+
# it is usually only necessary to provide the actual attribute values.
|
54
|
+
#
|
55
|
+
# *Note*: Currently only RSA is supported, DSA and ECDSA are not. See the examples below for
|
56
|
+
# how to handle them using external signing.
|
57
|
+
#
|
58
|
+
#
|
59
|
+
# == CMS and PAdES Signatures
|
60
|
+
#
|
61
|
+
# The handler supports the older standard of CMS signatures as well as the newer PAdES
|
62
|
+
# signatures specified in PDF 2.0. By default, CMS signatures are created but this can be
|
63
|
+
# changed by setting #signature_type to :pades.
|
64
|
+
#
|
65
|
+
# When creating PAdES signatures the following two PAdES baseline signatures are supported:
|
66
|
+
# B-B and B-T. The difference between those two is that a timestamp handler was defined for
|
67
|
+
# B-T compatibility.
|
68
|
+
#
|
69
|
+
#
|
70
|
+
# == Signing Modes - Internal, External, External/Asynchronous
|
71
|
+
#
|
72
|
+
# This handler provides two ways to create the CMS signed-data structure required by
|
73
|
+
# Signatures#add:
|
74
|
+
#
|
75
|
+
# * By providing the signing certificate together with the signing key and the certificate
|
76
|
+
# chain, HexaPDF itself does the signing *internally*. It is the preferred way if all the
|
77
|
+
# needed information is available.
|
78
|
+
#
|
79
|
+
# Assign the respective data to the #certificate, #key and #certificate_chain attributes.
|
80
|
+
#
|
81
|
+
# * By using an *external signing mechanism*, a callable object assigned to #external_signing.
|
82
|
+
# Here the actual signing happens "outside" of HexaPDF, for example, in custom code or even
|
83
|
+
# asynchronously. This is needed in case the signing key is not directly available but only
|
84
|
+
# an interface to it (e.g. when dealing with a HSM).
|
85
|
+
#
|
86
|
+
# Depending on whether #certificate is set the signing happens differently:
|
87
|
+
#
|
88
|
+
# * If #certificate is not set, the callable object is used instead of #sign, so it needs to
|
89
|
+
# accept the same arguments as #sign and needs to return a complete, DER-serialized CMS
|
90
|
+
# signed data object.
|
91
|
+
#
|
92
|
+
# * If #certificate is set, the CMS signed data object is created by HexaPDF. The
|
93
|
+
# callable #external_signing object is called with the used digest algorithm and the
|
94
|
+
# already digested data which needs to be signed (but *not* digested) and the signature
|
95
|
+
# returned.
|
96
|
+
#
|
97
|
+
# If the signing process needs to be *asynchronous*, make sure to set the #signature_size
|
98
|
+
# appropriately, return an empty string during signing and later use
|
99
|
+
# Signatures.embed_signature to embed the actual signature.
|
100
|
+
#
|
101
|
+
#
|
102
|
+
# == Optional Data
|
103
|
+
#
|
104
|
+
# Besides the required data, some optional attributes can also be specified:
|
105
|
+
#
|
106
|
+
# * Reason, location and contact information
|
107
|
+
# * Making the signature a certification signature by applying the DocMDP transform method and
|
108
|
+
# a DoCMDP permission
|
109
|
+
#
|
110
|
+
#
|
111
|
+
# == Examples
|
112
|
+
#
|
113
|
+
# # Signing using certificate + key
|
114
|
+
# document.sign("output.pdf", certificate: my_cert, key: my_key,
|
115
|
+
# certificate_chain: my_chain)
|
116
|
+
#
|
117
|
+
# # Signing using an external mechanism without certificate set
|
118
|
+
# signing_proc = lambda do |io, byte_range|
|
119
|
+
# io.pos = byte_range[0]
|
120
|
+
# data = io.read(byte_range[1])
|
121
|
+
# io.pos = byte_range[2]
|
122
|
+
# data << io.read(byte_range[3])
|
123
|
+
# signing_service.pkcs7_sign(data).to_der
|
124
|
+
# end
|
125
|
+
# document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc)
|
126
|
+
#
|
127
|
+
# # Signing using external mechanism with certificate set
|
128
|
+
# signing_proc = lambda do |digest_method, hash|
|
129
|
+
# signing_service.sign_raw(digest_method, hash)
|
130
|
+
# end
|
131
|
+
# document.sign("output.pdf", certificate: my_cert, certificate_chain: my_chain,
|
132
|
+
# external_signing: signing_proc)
|
133
|
+
#
|
134
|
+
# # Signing with DSA or ECDSA certificate/keys
|
135
|
+
# signing_proc = lambda do |io, byte_range|
|
136
|
+
# io.pos = byte_range[0]
|
137
|
+
# data = io.read(byte_range[1])
|
138
|
+
# io.pos = byte_range[2]
|
139
|
+
# data << io.read(byte_range[3])
|
140
|
+
# OpenSSL::PKCS7.sign(certificate, key, data, certificate_chain,
|
141
|
+
# OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
|
142
|
+
# end
|
143
|
+
# document.sign("output.pdf", signature_size: 10_000, external_signing: signing_proc)
|
144
|
+
#
|
145
|
+
#
|
146
|
+
# == Implementing a Signing Handler
|
147
|
+
#
|
148
|
+
# This class also serves as an example on how to create a custom handler: The public methods
|
149
|
+
# #signature_size, #finalize_objects and #sign are used by the digital signature algorithm.
|
150
|
+
# See their descriptions for details.
|
151
|
+
#
|
152
|
+
# Once a custom signing handler has been created, it can be registered under the
|
153
|
+
# 'signature.signing_handler' configuration option for easy use. It has to take keyword
|
154
|
+
# arguments in its initialize method to be compatible with the Signatures#handler method.
|
155
|
+
class DefaultHandler
|
156
|
+
|
157
|
+
# The certificate with which to sign the PDF.
|
158
|
+
#
|
159
|
+
# If the certificate is provided, HexaPDF creates the signature object. Otherwise the
|
160
|
+
# #external_signing callable object has to create it.
|
161
|
+
attr_accessor :certificate
|
162
|
+
|
163
|
+
# The private key for the #certificate.
|
164
|
+
#
|
165
|
+
# If the key is provided, HexaPDF does the signing. Otherwise the #external_signing callable
|
166
|
+
# object has to sign the data.
|
167
|
+
attr_accessor :key
|
168
|
+
|
169
|
+
# The certificate chain that should be embedded in the PDF; usually contains all
|
170
|
+
# certificates up to the root certificate.
|
171
|
+
attr_accessor :certificate_chain
|
172
|
+
|
173
|
+
# The digest algorithm that should be used when creating the signature.
|
174
|
+
#
|
175
|
+
# See SignedDataCreator#digest_algorithm for the default value (if nothing is set) and for
|
176
|
+
# the allowed values.
|
177
|
+
attr_accessor :digest_algorithm
|
178
|
+
|
179
|
+
# The timestamp handler that should be used for timestamping the signature.
|
180
|
+
#
|
181
|
+
# If this attribute is set, a timestamp token is embedded into the CMS object.
|
182
|
+
attr_accessor :timestamp_handler
|
183
|
+
|
184
|
+
# A callable object for custom signing mechanisms.
|
185
|
+
#
|
186
|
+
# The callable object has two different uses depending on whether #certificate is set:
|
187
|
+
#
|
188
|
+
# * If #certificate is not set, it fulfills the same role as the #sign method and needs to
|
189
|
+
# conform to that interface.
|
190
|
+
#
|
191
|
+
# * If #certificate is set and #key is not, it is just used for signing. Here it needs to
|
192
|
+
# accept the used digest algorithm and the already digested data as arguments and return
|
193
|
+
# the signature.
|
194
|
+
attr_accessor :external_signing
|
195
|
+
|
196
|
+
# The reason for signing. If used, will be set on the signature object.
|
197
|
+
attr_accessor :reason
|
198
|
+
|
199
|
+
# The signing location. If used, will be set on the signature object.
|
200
|
+
attr_accessor :location
|
201
|
+
|
202
|
+
# The contact information. If used, will be set on the signature object.
|
203
|
+
attr_accessor :contact_info
|
204
|
+
|
205
|
+
# The size of the serialized signature that should be reserved.
|
206
|
+
#
|
207
|
+
# If this attribute has not been set, an empty string will be signed using #sign to
|
208
|
+
# determine the signature size.
|
209
|
+
#
|
210
|
+
# The size needs to be at least as big as the final signature, otherwise signing results in
|
211
|
+
# an error.
|
212
|
+
attr_writer :signature_size
|
213
|
+
|
214
|
+
# The type of signature to be written (i.e. the value of the /SubFilter key).
|
215
|
+
#
|
216
|
+
# The value can either be :cms (the default; uses a detached CMS signature) or :pades
|
217
|
+
# (uses an ETSI CAdES compatible signature).
|
218
|
+
attr_accessor :signature_type
|
219
|
+
|
220
|
+
# The DocMDP permissions that should be set on the document.
|
221
|
+
#
|
222
|
+
# See #doc_mdp_permissions=
|
223
|
+
attr_reader :doc_mdp_permissions
|
224
|
+
|
225
|
+
# Creates a new DefaultHandler instance with the given attributes.
|
226
|
+
def initialize(**arguments)
|
227
|
+
@signature_size = nil
|
228
|
+
@signature_type = :cms
|
229
|
+
arguments.each {|name, value| send("#{name}=", value) }
|
230
|
+
end
|
231
|
+
|
232
|
+
# Sets the DocMDP permissions that should be applied to the document.
|
233
|
+
#
|
234
|
+
# Valid values for +permissions+ are:
|
235
|
+
#
|
236
|
+
# +nil+::
|
237
|
+
# Don't set any DocMDP permissions (default).
|
238
|
+
#
|
239
|
+
# +:no_changes+ or 1::
|
240
|
+
# No changes whatsoever are allowed.
|
241
|
+
#
|
242
|
+
# +:form_filling+ or 2::
|
243
|
+
# Only filling in forms and signing are allowed.
|
244
|
+
#
|
245
|
+
# +:form_filling_and_annotations+ or 3::
|
246
|
+
# Only filling in forms, signing and annotation creation/deletion/modification are
|
247
|
+
# allowed.
|
248
|
+
def doc_mdp_permissions=(permissions)
|
249
|
+
case permissions
|
250
|
+
when :no_changes, 1 then @doc_mdp_permissions = 1
|
251
|
+
when :form_filling, 2 then @doc_mdp_permissions = 2
|
252
|
+
when :form_filling_and_annotations, 3 then @doc_mdp_permissions = 3
|
253
|
+
when nil then @doc_mdp_permissions = nil
|
254
|
+
else
|
255
|
+
raise ArgumentError, "Invalid permissions value '#{permissions.inspect}'"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Returns the size of the serialized signature that should be reserved.
|
260
|
+
#
|
261
|
+
# If a custom size is set using #signature_size=, it used. Otherwise the size is determined
|
262
|
+
# by using #sign to sign an empty string.
|
263
|
+
def signature_size
|
264
|
+
@signature_size || sign(StringIO.new, [0, 0, 0, 0]).size
|
265
|
+
end
|
266
|
+
|
267
|
+
# Finalizes the signature field as well as the signature dictionary before writing.
|
268
|
+
def finalize_objects(_signature_field, signature)
|
269
|
+
signature[:Filter] = :'Adobe.PPKLite'
|
270
|
+
signature[:SubFilter] = (signature_type == :pades ? :'ETSI.CAdES.detached' : :'adbe.pkcs7.detached')
|
271
|
+
signature[:M] = Time.now
|
272
|
+
signature[:Reason] = reason if reason
|
273
|
+
signature[:Location] = location if location
|
274
|
+
signature[:ContactInfo] = contact_info if contact_info
|
275
|
+
signature[:Prop_Build] = {App: {Name: :HexaPDF, REx: HexaPDF::VERSION}}
|
276
|
+
|
277
|
+
if doc_mdp_permissions
|
278
|
+
doc = signature.document
|
279
|
+
if doc.signatures.count > 1
|
280
|
+
raise HexaPDF::Error, "Can set DocMDP access permissions only on first signature"
|
281
|
+
end
|
282
|
+
params = doc.add({Type: :TransformParams, V: :'1.2', P: doc_mdp_permissions})
|
283
|
+
sigref = doc.add({Type: :SigRef, TransformMethod: :DocMDP, DigestMethod: :SHA256,
|
284
|
+
TransformParams: params})
|
285
|
+
signature[:Reference] = [sigref]
|
286
|
+
(doc.catalog[:Perms] ||= {})[:DocMDP] = signature
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Returns the DER serialized CMS signed data object containing the signature for the given
|
291
|
+
# IO byte ranges.
|
292
|
+
#
|
293
|
+
# The +byte_range+ argument is an array containing four numbers [offset1, length1, offset2,
|
294
|
+
# length2]. The offset numbers are byte positions in the +io+ argument and the to-be-signed
|
295
|
+
# data can be determined by reading length bytes at the offsets.
|
296
|
+
def sign(io, byte_range)
|
297
|
+
if certificate
|
298
|
+
io.pos = byte_range[0]
|
299
|
+
data = io.read(byte_range[1])
|
300
|
+
io.pos = byte_range[2]
|
301
|
+
data << io.read(byte_range[3])
|
302
|
+
SignedDataCreator.create(data,
|
303
|
+
type: signature_type,
|
304
|
+
certificate: certificate, key: key,
|
305
|
+
digest_algorithm: digest_algorithm,
|
306
|
+
timestamp_handler: timestamp_handler,
|
307
|
+
certificates: certificate_chain, &external_signing).to_der
|
308
|
+
else
|
309
|
+
external_signing.call(io, byte_range)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,308 @@
|
|
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 'stringio'
|
39
|
+
require 'hexapdf/error'
|
40
|
+
|
41
|
+
module HexaPDF
|
42
|
+
module DigitalSignature
|
43
|
+
module Signing
|
44
|
+
|
45
|
+
# This class is used for creating a CMS SignedData binary data object, as needed for PDF
|
46
|
+
# signing.
|
47
|
+
#
|
48
|
+
# OpenSSL already provides the ability to access, sign and create such CMS objects but is
|
49
|
+
# limited in what it offers in terms of data added to it. Since HexaPDF needs to follow the
|
50
|
+
# PDF standard, it needs control over the created structure so as to make it compatible with
|
51
|
+
# the various requirements.
|
52
|
+
#
|
53
|
+
# As the created CMS object is only meant to be used in the context of PDF signing, it also
|
54
|
+
# restricts certain things, like allowing only a single signer.
|
55
|
+
#
|
56
|
+
# Additionally, only RSA signatures are currently supported!
|
57
|
+
#
|
58
|
+
# See: PDF1.7/2.0 s12.8.3.3, PDF2.0 s12.8.3.4, RFC5652, ETSI TS 102 778 Parts 1-4
|
59
|
+
class SignedDataCreator
|
60
|
+
|
61
|
+
# Creates a SignedDataCreator, sets the given attributes if they are not nil and then calls
|
62
|
+
# #create with the given data, type and block.
|
63
|
+
def self.create(data, type: :cms, **attributes, &block)
|
64
|
+
instance = new
|
65
|
+
attributes.each {|key, value| instance.send("#{key}=", value) unless value.nil? }
|
66
|
+
instance.create(data, type: type, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# The OpenSSL certificate object which is used to sign the data.
|
70
|
+
attr_accessor :certificate
|
71
|
+
|
72
|
+
# The OpenSSL key object which is used for signing. Needs to correspond to #certificate.
|
73
|
+
#
|
74
|
+
# If the key is not set, a block for signing will need to be provided to #sign.
|
75
|
+
attr_accessor :key
|
76
|
+
|
77
|
+
# Array of additional OpenSSL certificate objects that should be included.
|
78
|
+
#
|
79
|
+
# Should include all certificates of the hierarchy of the signing certificate.
|
80
|
+
attr_accessor :certificates
|
81
|
+
|
82
|
+
# The digest algorithm that should be used. Defaults to 'sha256'.
|
83
|
+
#
|
84
|
+
# Allowed values: sha256, sha384, sha512.
|
85
|
+
attr_accessor :digest_algorithm
|
86
|
+
|
87
|
+
# The timestamp handler instance that should be used for timestamping.
|
88
|
+
attr_accessor :timestamp_handler
|
89
|
+
|
90
|
+
# Creates a new SignedData object.
|
91
|
+
#
|
92
|
+
# Use the attribute accessor methods to set the required attributes.
|
93
|
+
def initialize
|
94
|
+
@certificate = nil
|
95
|
+
@key = nil
|
96
|
+
@certificates = []
|
97
|
+
@digest_algorithm = 'sha256'
|
98
|
+
@timestamp_handler = nil
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates a CMS SignedData binary data object for the given data using the set attributes
|
102
|
+
# and returns it in DER-serialized form.
|
103
|
+
#
|
104
|
+
# If the #key attribute is not set, the digest algorithm and the already digested data to be
|
105
|
+
# signed is yielded and the block needs to return the signature.
|
106
|
+
#
|
107
|
+
# +type+::
|
108
|
+
# The type can either be :cms when creating standard PDF CMS signatures or :pades when
|
109
|
+
# creating PAdES compatible signatures. PAdES signatures are part of PDF 2.0.
|
110
|
+
def create(data, type: :cms, &block) # :yield: digested_data
|
111
|
+
signed_attrs = create_signed_attrs(data, signing_time: (type == :cms))
|
112
|
+
signature = digest_and_sign_data(set(*signed_attrs.value).to_der, &block)
|
113
|
+
unsigned_attrs = create_unsigned_attrs(signature)
|
114
|
+
|
115
|
+
signer_info = create_signer_info(signature, signed_attrs, unsigned_attrs)
|
116
|
+
signed_data = create_signed_data(signer_info)
|
117
|
+
create_content_info(signed_data)
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# Creates the set of signed attributes for the signer information structure.
|
123
|
+
def create_signed_attrs(data, signing_time: true)
|
124
|
+
set(
|
125
|
+
attribute('content-type', oid('id-data')),
|
126
|
+
(attribute('id-signingTime', utc_time(Time.now.utc)) if signing_time),
|
127
|
+
attribute(
|
128
|
+
'message-digest',
|
129
|
+
binary(OpenSSL::Digest.digest(@digest_algorithm, data))
|
130
|
+
),
|
131
|
+
attribute(
|
132
|
+
'id-aa-signingCertificateV2',
|
133
|
+
sequence( # SigningCertificateV2
|
134
|
+
sequence( # Seq of ESSCertIDv2
|
135
|
+
sequence( # ESSCertIDv2
|
136
|
+
#TODO: Does not validate on ETSI checker if used, doesn't matter if SHA256 or 512
|
137
|
+
#oid('sha512'),
|
138
|
+
binary(OpenSSL::Digest.digest('sha256', @certificate.to_der)), # certHash
|
139
|
+
sequence( # issuerSerial
|
140
|
+
sequence( # issuer
|
141
|
+
implicit(4, sequence(@certificate.issuer)) # choice 4 directoryName
|
142
|
+
),
|
143
|
+
integer(@certificate.serial) # serial
|
144
|
+
)
|
145
|
+
)
|
146
|
+
)
|
147
|
+
)
|
148
|
+
)
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Creates the set of unsigned attributes for the signer information structure.
|
153
|
+
def create_unsigned_attrs(signature)
|
154
|
+
attrs = set
|
155
|
+
if @timestamp_handler
|
156
|
+
time_stamp_token = @timestamp_handler.sign(StringIO.new(signature),
|
157
|
+
[0, signature.size, 0, 0])
|
158
|
+
attrs.value << attribute('id-aa-timeStampToken', time_stamp_token)
|
159
|
+
end
|
160
|
+
attrs.value.empty? ? nil : attrs
|
161
|
+
end
|
162
|
+
|
163
|
+
# Creates a single attribute for use in the (un)signed attributes set.
|
164
|
+
def attribute(name, value)
|
165
|
+
sequence(
|
166
|
+
oid(name), # attrType
|
167
|
+
set(value) # attrValues
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Digests the data and then signs it using the assigned key, or if the key is not available,
|
172
|
+
# by yielding to the caller.
|
173
|
+
def digest_and_sign_data(data)
|
174
|
+
hash = OpenSSL::Digest.digest(@digest_algorithm, data)
|
175
|
+
if @key
|
176
|
+
@key.sign_raw(@digest_algorithm, hash)
|
177
|
+
else
|
178
|
+
yield(@digest_algorithm, hash)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Creates a signer information structure containing the actual meat of the whole CMS object.
|
183
|
+
def create_signer_info(signature, signed_attrs, unsigned_attrs = nil)
|
184
|
+
certificate_pkey_algorithm = @certificate.public_key.oid
|
185
|
+
signature_algorithm = if certificate_pkey_algorithm == 'rsaEncryption'
|
186
|
+
sequence( # signatureAlgorithm
|
187
|
+
oid('rsaEncryption'), # algorithmID
|
188
|
+
null # params
|
189
|
+
)
|
190
|
+
else
|
191
|
+
raise HexaPDF::Error, "Unsupported key type/signature algorithm"
|
192
|
+
end
|
193
|
+
|
194
|
+
sequence(
|
195
|
+
integer(1), # version
|
196
|
+
sequence( # sid (choice: issuerAndSerialNumber)
|
197
|
+
@certificate.issuer, # issuer
|
198
|
+
integer(@certificate.serial) # serial
|
199
|
+
),
|
200
|
+
sequence( # digestAlgorithm
|
201
|
+
oid(@digest_algorithm), # algorithmID
|
202
|
+
null # params
|
203
|
+
),
|
204
|
+
implicit(0, signed_attrs), # signedAttrs 0 implicit
|
205
|
+
signature_algorithm, # signatureAlgorithm
|
206
|
+
binary(signature), # signature
|
207
|
+
(implicit(1, unsigned_attrs) if unsigned_attrs) # unsignedAttrs 1 implicit
|
208
|
+
)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Creates the signed data structure which is the actual content of the CMS object.
|
212
|
+
def create_signed_data(signer_info)
|
213
|
+
certificates = set(*[@certificate, @certificates].flatten)
|
214
|
+
|
215
|
+
sequence(
|
216
|
+
integer(1), # version
|
217
|
+
set( # digestAlgorithms
|
218
|
+
sequence( # digestAlgorithm
|
219
|
+
oid(@digest_algorithm), # algorithmID
|
220
|
+
null # params
|
221
|
+
)
|
222
|
+
),
|
223
|
+
sequence( # encapContentInfo (detached signature)
|
224
|
+
oid('id-data') # eContentType
|
225
|
+
),
|
226
|
+
implicit(0, certificates), # certificates 0 implicit
|
227
|
+
set( # signerInfos
|
228
|
+
signer_info # signerInfo
|
229
|
+
)
|
230
|
+
)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Creates the content info structure which is the main structure containing everything else.
|
234
|
+
def create_content_info(signed_data)
|
235
|
+
signed_data.tag = 0
|
236
|
+
signed_data.tagging = :EXPLICIT
|
237
|
+
signed_data.tag_class = :CONTEXT_SPECIFIC
|
238
|
+
sequence(
|
239
|
+
oid('id-signedData'), # contentType
|
240
|
+
signed_data # content 0 explicit
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Changes the given ASN1Data object to use implicit tagging with the given +tag+ and a tag
|
245
|
+
# class of :CONTEXT_SPECIFIC.
|
246
|
+
def implicit(tag, data)
|
247
|
+
data.tag = tag
|
248
|
+
data.tagging = :IMPLICIT
|
249
|
+
data.tag_class = :CONTEXT_SPECIFIC
|
250
|
+
data
|
251
|
+
end
|
252
|
+
|
253
|
+
# Creates an ASN.1 set instance.
|
254
|
+
def set(*contents, tag: nil, tagging: nil)
|
255
|
+
OpenSSL::ASN1::Set.new(contents.compact, *tag, *tagging)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Creates an ASN.1 sequence instance.
|
259
|
+
def sequence(*contents, tag: nil, tagging: nil)
|
260
|
+
OpenSSL::ASN1::Sequence.new(contents.compact, *tag, *tagging)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Mapping of ASN.1 object ID names to object ID strings.
|
264
|
+
OIDS = {
|
265
|
+
'content-type' => '1.2.840.113549.1.9.3',
|
266
|
+
'message-digest' => '1.2.840.113549.1.9.4',
|
267
|
+
'id-data' => '1.2.840.113549.1.7.1',
|
268
|
+
'id-signedData' => '1.2.840.113549.1.7.2',
|
269
|
+
'id-signingTime' => '1.2.840.113549.1.9.5',
|
270
|
+
'sha256' => '2.16.840.1.101.3.4.2.1',
|
271
|
+
'sha384' => '2.16.840.1.101.3.4.2.2',
|
272
|
+
'sha512' => '2.16.840.1.101.3.4.2.3',
|
273
|
+
'rsaEncryption' => '1.2.840.113549.1.1.1',
|
274
|
+
'id-aa-signingCertificate' => '1.2.840.113549.1.9.16.2.12',
|
275
|
+
'id-aa-timeStampToken' => '1.2.840.113549.1.9.16.2.14',
|
276
|
+
'id-aa-signingCertificateV2' => '1.2.840.113549.1.9.16.2.47',
|
277
|
+
}
|
278
|
+
|
279
|
+
# Creates an ASN.1 object ID instance for the given object ID name.
|
280
|
+
def oid(name)
|
281
|
+
OpenSSL::ASN1::ObjectId.new(OIDS[name])
|
282
|
+
end
|
283
|
+
|
284
|
+
# Creates an ASN.1 octet string instance.
|
285
|
+
def binary(str)
|
286
|
+
OpenSSL::ASN1::OctetString.new(str)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Creates an ASN.1 integer instance.
|
290
|
+
def integer(int)
|
291
|
+
OpenSSL::ASN1::Integer.new(int)
|
292
|
+
end
|
293
|
+
|
294
|
+
# Creates an ASN.1 UTC time instance.
|
295
|
+
def utc_time(value)
|
296
|
+
OpenSSL::ASN1::UTCTime.new(value)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Creates an ASN.1 null instance.
|
300
|
+
def null
|
301
|
+
OpenSSL::ASN1::Null.new(nil)
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|