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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -10
  3. data/examples/024-digital-signatures.rb +23 -0
  4. data/lib/hexapdf/configuration.rb +12 -12
  5. data/lib/hexapdf/dictionary_fields.rb +6 -2
  6. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  7. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  8. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  9. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  10. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  11. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  12. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  13. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  14. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  15. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  16. data/lib/hexapdf/digital_signature.rb +56 -0
  17. data/lib/hexapdf/document.rb +21 -14
  18. data/lib/hexapdf/encryption/standard_security_handler.rb +2 -1
  19. data/lib/hexapdf/type.rb +0 -1
  20. data/lib/hexapdf/version.rb +1 -1
  21. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  22. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  23. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  24. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  25. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  26. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  27. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  28. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  29. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  30. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  31. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  32. data/test/hexapdf/test_dictionary_fields.rb +2 -1
  33. data/test/hexapdf/test_document.rb +1 -1
  34. data/test/hexapdf/test_writer.rb +3 -3
  35. metadata +25 -15
  36. data/lib/hexapdf/document/signatures.rb +0 -546
  37. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  38. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  39. data/lib/hexapdf/type/signature/handler.rb +0 -140
  40. 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 Type
41
- class Signature
38
+ module DigitalSignature
42
39
 
43
- # Holds the result information when verifying a signature.
44
- class VerificationResult
40
+ # Holds the result information when verifying a signature.
41
+ class VerificationResult
45
42
 
46
- # :nodoc:
47
- MESSAGE_SORT_MAP = {
48
- info: {warning: 1, error: 1, info: 0},
49
- warning: {info: -1, error: 1, warning: 0},
50
- error: {info: -1, warning: -1, error: 0},
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
- # This structure represents a single status message, containing the type (:info, :warning,
54
- # :error) and the content of the message.
55
- Message = Struct.new(:type, :content) do
56
- def <=>(other)
57
- MESSAGE_SORT_MAP[type][other.type]
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
- # An array with all result messages.
62
- attr_reader :messages
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
- # Returns +true+ if there are no error messages.
70
- def success?
71
- @messages.none? {|message| message.type == :error }
72
- end
61
+ # Creates an empty result object.
62
+ def initialize
63
+ @messages = []
64
+ end
73
65
 
74
- # Returns +true+ if there is at least one error message.
75
- def failure?
76
- !success?
77
- end
66
+ # Returns +true+ if there are no error messages.
67
+ def success?
68
+ @messages.none? {|message| message.type == :error }
69
+ end
78
70
 
79
- # Adds a new message of the given type to this result object.
80
- #
81
- # +type+:: One of :info, :warning or :error.
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