hexapdf 0.28.0 → 0.29.0

Sign up to get free protection for your applications and to get access to all the features.
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