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
@@ -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
|