hexapdf 0.28.0 → 0.31.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -10
  3. data/examples/024-digital-signatures.rb +23 -0
  4. data/lib/hexapdf/cli/command.rb +16 -1
  5. data/lib/hexapdf/cli/info.rb +9 -1
  6. data/lib/hexapdf/cli/inspect.rb +2 -2
  7. data/lib/hexapdf/composer.rb +76 -28
  8. data/lib/hexapdf/configuration.rb +29 -16
  9. data/lib/hexapdf/dictionary_fields.rb +13 -4
  10. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  11. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  12. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  13. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  14. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  15. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  16. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  17. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  18. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  19. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  20. data/lib/hexapdf/digital_signature.rb +56 -0
  21. data/lib/hexapdf/document/pages.rb +31 -18
  22. data/lib/hexapdf/document.rb +29 -15
  23. data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
  24. data/lib/hexapdf/filter/flate_decode.rb +20 -8
  25. data/lib/hexapdf/layout/page_style.rb +144 -0
  26. data/lib/hexapdf/layout.rb +1 -0
  27. data/lib/hexapdf/task/optimize.rb +8 -6
  28. data/lib/hexapdf/type/font_simple.rb +14 -2
  29. data/lib/hexapdf/type/object_stream.rb +7 -2
  30. data/lib/hexapdf/type/outline.rb +1 -1
  31. data/lib/hexapdf/type/outline_item.rb +1 -1
  32. data/lib/hexapdf/type/page.rb +29 -8
  33. data/lib/hexapdf/type/xref_stream.rb +11 -4
  34. data/lib/hexapdf/type.rb +0 -1
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +1 -1
  37. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  38. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  39. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  40. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  41. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  42. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  43. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  44. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  45. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  46. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  47. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  48. data/test/hexapdf/document/test_pages.rb +25 -0
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
  50. data/test/hexapdf/filter/test_flate_decode.rb +19 -5
  51. data/test/hexapdf/layout/test_page_style.rb +70 -0
  52. data/test/hexapdf/task/test_optimize.rb +11 -9
  53. data/test/hexapdf/test_composer.rb +35 -10
  54. data/test/hexapdf/test_dictionary_fields.rb +9 -3
  55. data/test/hexapdf/test_document.rb +1 -1
  56. data/test/hexapdf/test_writer.rb +8 -8
  57. data/test/hexapdf/type/test_font_simple.rb +18 -6
  58. data/test/hexapdf/type/test_object_stream.rb +16 -7
  59. data/test/hexapdf/type/test_outline.rb +3 -1
  60. data/test/hexapdf/type/test_outline_item.rb +3 -1
  61. data/test/hexapdf/type/test_page.rb +42 -11
  62. data/test/hexapdf/type/test_xref_stream.rb +6 -1
  63. metadata +27 -15
  64. data/lib/hexapdf/document/signatures.rb +0 -546
  65. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  66. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  67. data/lib/hexapdf/type/signature/handler.rb +0 -140
  68. data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -1,352 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
-
3
- require 'test_helper'
4
- require 'stringio'
5
- require 'tempfile'
6
- require 'hexapdf/document'
7
- require_relative '../type/signature/common'
8
-
9
- describe HexaPDF::Document::Signatures do
10
- before do
11
- @doc = HexaPDF::Document.new
12
- @form = @doc.acro_form(create: true)
13
- @sig1 = @form.create_signature_field("test1")
14
- @sig2 = @form.create_signature_field("test2")
15
- @handler = HexaPDF::Document::Signatures::DefaultHandler.new(
16
- certificate: CERTIFICATES.signer_certificate,
17
- key: CERTIFICATES.signer_key,
18
- certificate_chain: [CERTIFICATES.ca_certificate]
19
- )
20
- end
21
-
22
- it "allows embedding an external signature value" do
23
- doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
24
- io = StringIO.new(''.b)
25
- doc.signatures.add(io, @handler)
26
- doc = HexaPDF::Document.new(io: io)
27
- io = StringIO.new(''.b)
28
-
29
- byte_range = nil
30
- @handler.signature_size = 5000
31
- @handler.external_signing = proc {|_, br| byte_range = br; "" }
32
- doc.signatures.add(io, @handler)
33
-
34
- io.pos = byte_range[0]
35
- data = io.read(byte_range[1])
36
- io.pos = byte_range[2]
37
- data << io.read(byte_range[3])
38
- contents = OpenSSL::PKCS7.sign(@handler.certificate, @handler.key, data, @handler.certificate_chain,
39
- OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
40
- HexaPDF::Document::Signatures.embed_signature(io, contents)
41
- doc = HexaPDF::Document.new(io: io)
42
- assert_equal(2, doc.signatures.each.count)
43
- doc.signatures.each do |signature|
44
- assert(signature.verify(allow_self_signed: true).messages.find {|m| m.content == 'Signature valid' })
45
- end
46
- end
47
-
48
- describe "DefaultHandler" do
49
- it "returns the size of serialized signature" do
50
- assert_equal(1310, @handler.signature_size)
51
- @handler.signature_size = 100
52
- assert_equal(100, @handler.signature_size)
53
- end
54
-
55
- it "allows setting the DocMDP permissions" do
56
- assert_nil(@handler.doc_mdp_permissions)
57
-
58
- @handler.doc_mdp_permissions = :no_changes
59
- assert_equal(1, @handler.doc_mdp_permissions)
60
- @handler.doc_mdp_permissions = 1
61
- assert_equal(1, @handler.doc_mdp_permissions)
62
-
63
- @handler.doc_mdp_permissions = :form_filling
64
- assert_equal(2, @handler.doc_mdp_permissions)
65
- @handler.doc_mdp_permissions = 2
66
- assert_equal(2, @handler.doc_mdp_permissions)
67
-
68
- @handler.doc_mdp_permissions = :form_filling_and_annotations
69
- assert_equal(3, @handler.doc_mdp_permissions)
70
- @handler.doc_mdp_permissions = 3
71
- assert_equal(3, @handler.doc_mdp_permissions)
72
-
73
- @handler.doc_mdp_permissions = nil
74
- assert_nil(@handler.doc_mdp_permissions)
75
-
76
- assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
77
- end
78
-
79
- describe "sign" do
80
- it "can sign the data using PKCS7" do
81
- data = StringIO.new("data")
82
- store = OpenSSL::X509::Store.new
83
- store.add_cert(CERTIFICATES.ca_certificate)
84
-
85
- pkcs7 = OpenSSL::PKCS7.new(@handler.sign(data, [0, 4, 0, 0]))
86
- assert(pkcs7.detached?)
87
- assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
88
- pkcs7.certificates)
89
- assert(pkcs7.verify([], store, data.string, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
90
- end
91
-
92
- it "can use external signing" do
93
- @handler.external_signing = proc { "hallo" }
94
- assert_equal("hallo", @handler.sign(StringIO.new, [0, 0, 0, 0]))
95
- end
96
- end
97
-
98
- describe "finalize_objects" do
99
- before do
100
- @field = @doc.wrap({})
101
- @obj = @doc.wrap({})
102
- end
103
-
104
- it "does nothing if no finalization tasks need to be done" do
105
- @handler.finalize_objects(@field, @obj)
106
- assert(@field.empty?)
107
- assert(@obj.empty?)
108
- end
109
-
110
- it "adjust the /SubFilter if signature type is etsi" do
111
- @handler.signature_type = :etsi
112
- @handler.finalize_objects(@field, @obj)
113
- assert_equal(:'ETSI.CAdES.detached', @obj[:SubFilter])
114
- end
115
-
116
- it "sets the reason, location and contact info fields" do
117
- @handler.reason = 'Reason'
118
- @handler.location = 'Location'
119
- @handler.contact_info = 'Contact'
120
- @handler.finalize_objects(@field, @obj)
121
- assert(@field.empty?)
122
- assert_equal({Reason: 'Reason', Location: 'Location', ContactInfo: 'Contact'}, @obj.value)
123
- end
124
-
125
- it "applies the specified DocMDP permissions" do
126
- @handler.doc_mdp_permissions = :no_changes
127
- @handler.finalize_objects(@field, @obj)
128
- ref = @obj[:Reference][0]
129
- assert_equal(:DocMDP, ref[:TransformMethod])
130
- assert_equal(:SHA1, ref[:DigestMethod])
131
- assert_equal(1, ref[:TransformParams][:P])
132
- assert_equal(:'1.2', ref[:TransformParams][:V])
133
- assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
134
- end
135
-
136
- it "fails if DocMDP should be set but there is already a signature" do
137
- @handler.doc_mdp_permissions = :no_changes
138
- 2.times do
139
- field = @doc.acro_form(create: true).create_signature_field('test')
140
- field.field_value = :something
141
- end
142
- assert_raises(HexaPDF::Error) { @handler.finalize_objects(@field, @obj) }
143
- end
144
- end
145
- end
146
-
147
- describe "TimestampHandler" do
148
- before do
149
- @handler = HexaPDF::Document::Signatures::TimestampHandler.new
150
- end
151
-
152
- it "allows setting the attributes in the constructor" do
153
- handler = HexaPDF::Document::Signatures::TimestampHandler.new(
154
- tsa_url: "url", tsa_hash_algorithm: "MD5", tsa_policy_id: "5",
155
- reason: "Reason", location: "Location", contact_info: "Contact",
156
- signature_size: 1_000
157
- )
158
- assert_equal("url", handler.tsa_url)
159
- assert_equal("MD5", handler.tsa_hash_algorithm)
160
- assert_equal("5", handler.tsa_policy_id)
161
- assert_equal("Reason", handler.reason)
162
- assert_equal("Location", handler.location)
163
- assert_equal("Contact", handler.contact_info)
164
- assert_equal(1_000, handler.signature_size)
165
- end
166
-
167
- it "finalizes the signature field and signature objects" do
168
- @field = @doc.wrap({})
169
- @sig = @doc.wrap({})
170
- @handler.reason = 'Reason'
171
- @handler.location = 'Location'
172
- @handler.contact_info = 'Contact'
173
-
174
- @handler.finalize_objects(@field, @sig)
175
- assert_equal('2.0', @doc.version)
176
- assert_equal(:DocTimeStamp, @sig[:Type])
177
- assert_equal(:'ETSI.RFC3161', @sig[:SubFilter])
178
- assert_equal('Reason', @sig[:Reason])
179
- assert_equal('Location', @sig[:Location])
180
- assert_equal('Contact', @sig[:ContactInfo])
181
- end
182
-
183
- it "returns the size of serialized signature" do
184
- @handler.tsa_url = "http://127.0.0.1:34567"
185
- CERTIFICATES.start_tsa_server
186
- assert_equal(1420, @handler.signature_size)
187
- end
188
-
189
- describe "sign" do
190
- before do
191
- @data = StringIO.new("data")
192
- @range = [0, 4, 0, 0]
193
- @handler.tsa_url = "http://127.0.0.1:34567"
194
- CERTIFICATES.start_tsa_server
195
- end
196
-
197
- it "respects the set hash algorithm and policy id" do
198
- @handler.tsa_hash_algorithm = 'SHA256'
199
- @handler.tsa_policy_id = '1.2.3.4.2'
200
- token = OpenSSL::ASN1.decode(@handler.sign(@data, @range))
201
- content = OpenSSL::ASN1.decode(token.value[1].value[0].value[2].value[1].value[0].value)
202
- policy_id = content.value[1].value
203
- digest_algorithm = content.value[2].value[0].value[0].value
204
- assert_equal('SHA256', digest_algorithm)
205
- assert_equal("1.2.3.4.2", policy_id)
206
- end
207
-
208
- it "returns the serialized timestamp token" do
209
- token = OpenSSL::PKCS7.new(@handler.sign(@data, @range))
210
- assert_equal(CERTIFICATES.ca_certificate.subject, token.signers[0].issuer)
211
- assert_equal(CERTIFICATES.timestamp_certificate.serial, token.signers[0].serial)
212
- end
213
-
214
- it "fails if the timestamp token could not be created" do
215
- @handler.tsa_hash_algorithm = 'SHA1'
216
- msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
217
- assert_match(/BAD_ALG/, msg.message)
218
- end
219
-
220
- it "fails if the timestamp server couldn't process the request" do
221
- @handler.tsa_policy_id = '1.2.3.4.1'
222
- msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
223
- assert_match(/Invalid TSA server response/, msg.message)
224
- end
225
- end
226
- end
227
-
228
- it "iterates over all signature dictionaries" do
229
- assert_equal([], @doc.signatures.to_a)
230
- @sig1.field_value = :sig1
231
- @sig2.field_value = :sig2
232
- assert_equal([:sig1, :sig2], @doc.signatures.to_a)
233
- end
234
-
235
- it "returns the number of signature dictionaries" do
236
- @sig1.field_value = :sig1
237
- assert_equal(1, @doc.signatures.count)
238
- end
239
-
240
- describe "handler" do
241
- it "return the initialized handler" do
242
- handler = @doc.signatures.handler(certificate: 'cert', reason: 'reason')
243
- assert_equal('cert', handler.certificate)
244
- assert_equal('reason', handler.reason)
245
- end
246
-
247
- it "fails if the given task is not available" do
248
- assert_raises(HexaPDF::Error) { @doc.signatures.handler(name: :unknown) }
249
- end
250
- end
251
-
252
- describe "add" do
253
- before do
254
- @doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
255
- @io = StringIO.new(''.b)
256
- end
257
-
258
- it "uses the provided signature dictionary" do
259
- sig = @doc.add({Type: :Sig, Key: :value})
260
- @doc.signatures.add(@io, @handler, signature: sig)
261
- assert_equal(1, @doc.signatures.to_a.compact.size)
262
- assert_equal(:value, @doc.signatures.to_a[0][:Key])
263
- refute_equal(:value, @doc.acro_form.each_field.first[:Key])
264
- end
265
-
266
- it "creates the signature dictionary if none is provided" do
267
- @doc.signatures.add(@io, @handler)
268
- assert_equal(1, @doc.signatures.to_a.compact.size)
269
- refute(@doc.acro_form.each_field.first.key?(:Contents))
270
- end
271
-
272
- it "sets the needed information on the signature dictionary" do
273
- def @handler.finalize_objects(sigfield, sig)
274
- sig[:key] = :sig
275
- sigfield[:key] = :sig_field
276
- end
277
- @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
278
- sig = @doc.signatures.first
279
- assert_equal(:'Adobe.PPKLite', sig[:Filter])
280
- assert_equal(:'adbe.pkcs7.detached', sig[:SubFilter])
281
- assert_equal([0, 996, 3618, 2501], sig[:ByteRange].value)
282
- assert_equal(:sig, sig[:key])
283
- assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
284
- assert(sig.key?(:Contents))
285
- assert(sig.key?(:M))
286
- end
287
-
288
- it "creates the main form dictionary if necessary" do
289
- @doc.signatures.add(@io, @handler)
290
- assert(@doc.acro_form)
291
- assert_equal([:signatures_exist, :append_only], @doc.acro_form.signature_flags)
292
- end
293
-
294
- it "uses the provided signature field" do
295
- field = @doc.acro_form(create: true).create_signature_field('Signature2')
296
- @doc.signatures.add(@io, @handler, signature: field)
297
- assert_nil(@doc.acro_form.field_by_name("Signature3"))
298
- refute_nil(field.field_value)
299
- assert_nil(@doc.signatures.first[:T])
300
- end
301
-
302
- it "uses an existing signature field if possible" do
303
- field = @doc.acro_form(create: true).create_signature_field('Signature2')
304
- field.field_value = sig = @doc.add({Type: :Sig, key: :value})
305
- @doc.signatures.add(@io, @handler, signature: sig)
306
- assert_nil(@doc.acro_form.field_by_name("Signature3"))
307
- assert_same(sig, @doc.signatures.first)
308
- end
309
-
310
- it "creates the signature field if necessary" do
311
- @doc.acro_form(create: true).create_text_field('Signature2')
312
- @doc.signatures.add(@io, @handler)
313
- field = @doc.acro_form.field_by_name("Signature3")
314
- assert_equal(:Sig, field.field_type)
315
- refute_nil(field.field_value)
316
- assert_equal(1, field.each_widget.count)
317
- end
318
-
319
- it "handles different xref section types correctly when determing the offsets" do
320
- @doc.delete(7)
321
- sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
322
- assert_equal([0, 988, 3610, 2483], sig[:ByteRange].value)
323
- end
324
-
325
- it "works if the signature object is the last object of the xref section" do
326
- field = @doc.acro_form(create: true).create_signature_field('Signature2')
327
- field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
328
- sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
329
- assert_equal([0, 3095, 5717, 380], sig[:ByteRange].value)
330
- end
331
-
332
- it "allows writing to a file in addition to writing to an IO" do
333
- tempfile = Tempfile.new('hexapdf-signature')
334
- tempfile.close
335
- @doc.signatures.add(tempfile.path, @handler)
336
- doc = HexaPDF::Document.open(tempfile.path)
337
- assert(doc.signatures.first.verify(allow_self_signed: true).success?)
338
- end
339
-
340
- it "adds a new revision with the signature" do
341
- @doc.signatures.add(@io, @handler)
342
- signed_doc = HexaPDF::Document.new(io: @io)
343
- assert(signed_doc.signatures.first.verify)
344
- end
345
-
346
- it "fails if the reserved signature space is too small" do
347
- def @handler.signature_size; 200; end
348
- msg = assert_raises(HexaPDF::Error) { @doc.signatures.add(@io, @handler) }
349
- assert_match(/space.*too small.*200 vs/, msg.message)
350
- end
351
- end
352
- end