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
@@ -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
|
@@ -0,0 +1,148 @@
|
|
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
|
+
module DigitalSignature
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
# This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
|
47
|
+
# document. It is registered under the :timestamp name.
|
48
|
+
#
|
49
|
+
# The timestamp is provided by a timestamp authority and establishes the document contents at
|
50
|
+
# the time indicated in the timestamp. Timestamping a PDF document is usually done in context
|
51
|
+
# of long term validation but can also be done standalone.
|
52
|
+
#
|
53
|
+
# == Usage
|
54
|
+
#
|
55
|
+
# It is necessary to provide at least the URL of the timestamp authority server (TSA) via
|
56
|
+
# #tsa_url, everything else is optional and uses default values. The TSA server must not use
|
57
|
+
# authentication to be usable.
|
58
|
+
#
|
59
|
+
# Example:
|
60
|
+
#
|
61
|
+
# document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
|
62
|
+
class TimestampHandler
|
63
|
+
|
64
|
+
# The URL of the timestamp authority server.
|
65
|
+
#
|
66
|
+
# This value is required.
|
67
|
+
attr_accessor :tsa_url
|
68
|
+
|
69
|
+
# The hash algorithm to use for timestamping. Defaults to SHA512.
|
70
|
+
attr_accessor :tsa_hash_algorithm
|
71
|
+
|
72
|
+
# The policy OID to use for timestamping. Defaults to +nil+.
|
73
|
+
attr_accessor :tsa_policy_id
|
74
|
+
|
75
|
+
# The size of the serialized signature that should be reserved.
|
76
|
+
#
|
77
|
+
# If this attribute has not been set, an empty string will be signed using #sign to
|
78
|
+
# determine the signature size which will contact the TSA server
|
79
|
+
#
|
80
|
+
# The size needs to be at least as big as the final signature, otherwise signing results in
|
81
|
+
# an error.
|
82
|
+
attr_writer :signature_size
|
83
|
+
|
84
|
+
# The reason for timestamping. If used, will be set on the signature object.
|
85
|
+
attr_accessor :reason
|
86
|
+
|
87
|
+
# The timestamping location. If used, will be set on the signature object.
|
88
|
+
attr_accessor :location
|
89
|
+
|
90
|
+
# The contact information. If used, will be set on the signature object.
|
91
|
+
attr_accessor :contact_info
|
92
|
+
|
93
|
+
# Creates a new TimestampHandler with the given attributes.
|
94
|
+
def initialize(**arguments)
|
95
|
+
@signature_size = nil
|
96
|
+
arguments.each {|name, value| send("#{name}=", value) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the size of the serialized signature that should be reserved.
|
100
|
+
def signature_size
|
101
|
+
@signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
|
102
|
+
end
|
103
|
+
|
104
|
+
# Finalizes the signature field as well as the signature dictionary before writing.
|
105
|
+
def finalize_objects(_signature_field, signature)
|
106
|
+
signature.document.version = '2.0'
|
107
|
+
signature[:Type] = :DocTimeStamp
|
108
|
+
signature[:Filter] = :'Adobe.PPKLite'
|
109
|
+
signature[:SubFilter] = :'ETSI.RFC3161'
|
110
|
+
signature[:Reason] = reason if reason
|
111
|
+
signature[:Location] = location if location
|
112
|
+
signature[:ContactInfo] = contact_info if contact_info
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
|
116
|
+
# given IO byte ranges.
|
117
|
+
def sign(io, byte_range)
|
118
|
+
hash_algorithm = tsa_hash_algorithm || 'SHA512'
|
119
|
+
digest = OpenSSL::Digest.new(hash_algorithm)
|
120
|
+
io.pos = byte_range[0]
|
121
|
+
digest << io.read(byte_range[1])
|
122
|
+
io.pos = byte_range[2]
|
123
|
+
digest << io.read(byte_range[3])
|
124
|
+
|
125
|
+
req = OpenSSL::Timestamp::Request.new
|
126
|
+
req.algorithm = hash_algorithm
|
127
|
+
req.message_imprint = digest.digest
|
128
|
+
req.policy_id = tsa_policy_id if tsa_policy_id
|
129
|
+
|
130
|
+
http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
|
131
|
+
'content-type' => 'application/timestamp-query')
|
132
|
+
if http_response.kind_of?(Net::HTTPOK)
|
133
|
+
response = OpenSSL::Timestamp::Response.new(http_response.body)
|
134
|
+
if response.status == 0
|
135
|
+
response.token.to_der
|
136
|
+
else
|
137
|
+
raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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 'hexapdf/document'
|
38
|
+
|
39
|
+
module HexaPDF
|
40
|
+
module DigitalSignature
|
41
|
+
|
42
|
+
# This module contains everything related to the signing of a PDF document, i.e. signing
|
43
|
+
# handlers and the actual code for signing.
|
44
|
+
module Signing
|
45
|
+
|
46
|
+
autoload(:DefaultHandler, 'hexapdf/digital_signature/signing/default_handler')
|
47
|
+
autoload(:TimestampHandler, 'hexapdf/digital_signature/signing/timestamp_handler')
|
48
|
+
autoload(:SignedDataCreator, 'hexapdf/digital_signature/signing/signed_data_creator')
|
49
|
+
|
50
|
+
# Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
|
51
|
+
# the PDF document given by the +io+ argument.
|
52
|
+
#
|
53
|
+
# This functionality can be used together with the support for external signing (see
|
54
|
+
# DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
|
55
|
+
#
|
56
|
+
# Note: This will, most probably, only work on documents prepared for external signing by
|
57
|
+
# HexaPDF and not by other libraries.
|
58
|
+
def self.embed_signature(io, signature)
|
59
|
+
doc = HexaPDF::Document.new(io: io)
|
60
|
+
signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
|
61
|
+
signature_dict_offset, signature_dict_length = locate_signature_dict(
|
62
|
+
doc.revisions.current.xref_section,
|
63
|
+
doc.revisions.parser.startxref_offset,
|
64
|
+
signature_dict.oid
|
65
|
+
)
|
66
|
+
io.pos = signature_dict_offset
|
67
|
+
signature_data = io.read(signature_dict_length)
|
68
|
+
replace_signature_contents(signature_data, signature)
|
69
|
+
io.pos = signature_dict_offset
|
70
|
+
io.write(signature_data)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Uses the information in the given cross-reference section as well as the byte offset of the
|
74
|
+
# cross-reference section to calculate the offset and length of the signature dictionary with
|
75
|
+
# the given object id.
|
76
|
+
def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
|
77
|
+
data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
|
78
|
+
[start_xref_position, nil]
|
79
|
+
index = data.index {|_pos, oid| oid == signature_oid }
|
80
|
+
[data[index][0], data[index + 1][0] - data[index][0]]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
|
84
|
+
# +contents+.
|
85
|
+
def self.replace_signature_contents(signature_data, contents)
|
86
|
+
signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
|
87
|
+
length = match.size
|
88
|
+
result = "Contents<#{contents.unpack1('H*')}"
|
89
|
+
if length < result.size
|
90
|
+
raise HexaPDF::Error, "The reserved space for the signature was too small " \
|
91
|
+
"(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
|
92
|
+
"#signature_size method to increase the reserved space"
|
93
|
+
end
|
94
|
+
"#{result.ljust(length - 1, '0')}>"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -34,59 +34,55 @@
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
35
|
#++
|
36
36
|
|
37
|
-
require 'hexapdf/type/signature'
|
38
|
-
|
39
37
|
module HexaPDF
|
40
|
-
module
|
41
|
-
class Signature
|
38
|
+
module DigitalSignature
|
42
39
|
|
43
|
-
|
44
|
-
|
40
|
+
# Holds the result information when verifying a signature.
|
41
|
+
class VerificationResult
|
45
42
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
# :nodoc:
|
44
|
+
MESSAGE_SORT_MAP = {
|
45
|
+
info: {warning: 1, error: 1, info: 0},
|
46
|
+
warning: {info: -1, error: 1, warning: 0},
|
47
|
+
error: {info: -1, warning: -1, error: 0},
|
48
|
+
}
|
52
49
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
50
|
+
# This structure represents a single status message, containing the type (:info, :warning,
|
51
|
+
# :error) and the content of the message.
|
52
|
+
Message = Struct.new(:type, :content) do
|
53
|
+
def <=>(other)
|
54
|
+
MESSAGE_SORT_MAP[type][other.type]
|
59
55
|
end
|
56
|
+
end
|
60
57
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# Creates an empty result object.
|
65
|
-
def initialize
|
66
|
-
@messages = []
|
67
|
-
end
|
58
|
+
# An array with all result messages.
|
59
|
+
attr_reader :messages
|
68
60
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
61
|
+
# Creates an empty result object.
|
62
|
+
def initialize
|
63
|
+
@messages = []
|
64
|
+
end
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
66
|
+
# Returns +true+ if there are no error messages.
|
67
|
+
def success?
|
68
|
+
@messages.none? {|message| message.type == :error }
|
69
|
+
end
|
78
70
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# +content+:: The log message.
|
84
|
-
def log(type, content)
|
85
|
-
@messages << Message.new(type, content)
|
86
|
-
end
|
71
|
+
# Returns +true+ if there is at least one error message.
|
72
|
+
def failure?
|
73
|
+
!success?
|
74
|
+
end
|
87
75
|
|
76
|
+
# Adds a new message of the given type to this result object.
|
77
|
+
#
|
78
|
+
# +type+:: One of :info, :warning or :error.
|
79
|
+
#
|
80
|
+
# +content+:: The log message.
|
81
|
+
def log(type, content)
|
82
|
+
@messages << Message.new(type, content)
|
88
83
|
end
|
89
84
|
|
90
85
|
end
|
86
|
+
|
91
87
|
end
|
92
88
|
end
|