hexapdf 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/data/hexapdf/cert/demo_cert.rb +22 -0
  4. data/data/hexapdf/cert/root-ca.crt +119 -0
  5. data/data/hexapdf/cert/signing.crt +125 -0
  6. data/data/hexapdf/cert/signing.key +52 -0
  7. data/data/hexapdf/cert/sub-ca.crt +125 -0
  8. data/lib/hexapdf/cli/info.rb +21 -1
  9. data/lib/hexapdf/configuration.rb +26 -0
  10. data/lib/hexapdf/content/graphics_state.rb +24 -5
  11. data/lib/hexapdf/content/processor.rb +1 -1
  12. data/lib/hexapdf/document/signatures.rb +327 -0
  13. data/lib/hexapdf/document.rb +26 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
  15. data/lib/hexapdf/importer.rb +1 -1
  16. data/lib/hexapdf/layout/style.rb +2 -1
  17. data/lib/hexapdf/object.rb +5 -3
  18. data/lib/hexapdf/parser.rb +21 -9
  19. data/lib/hexapdf/rectangle.rb +0 -6
  20. data/lib/hexapdf/revision.rb +13 -6
  21. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  22. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  23. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  24. data/lib/hexapdf/type/annotation.rb +36 -3
  25. data/lib/hexapdf/type/font.rb +5 -0
  26. data/lib/hexapdf/type/font_simple.rb +1 -1
  27. data/lib/hexapdf/type/font_type3.rb +20 -0
  28. data/lib/hexapdf/type/object_stream.rb +3 -1
  29. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
  30. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +99 -0
  31. data/lib/hexapdf/type/signature/handler.rb +112 -0
  32. data/lib/hexapdf/type/signature/verification_result.rb +92 -0
  33. data/lib/hexapdf/type/signature.rb +236 -0
  34. data/lib/hexapdf/type.rb +1 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +24 -10
  37. data/test/hexapdf/content/test_graphics_state.rb +9 -1
  38. data/test/hexapdf/content/test_operator.rb +8 -3
  39. data/test/hexapdf/content/test_processor.rb +1 -1
  40. data/test/hexapdf/document/test_signatures.rb +225 -0
  41. data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
  42. data/test/hexapdf/layout/test_style.rb +11 -0
  43. data/test/hexapdf/test_document.rb +28 -0
  44. data/test/hexapdf/test_object.rb +7 -2
  45. data/test/hexapdf/test_parser.rb +14 -0
  46. data/test/hexapdf/test_rectangle.rb +0 -7
  47. data/test/hexapdf/test_revision.rb +44 -14
  48. data/test/hexapdf/test_writer.rb +44 -14
  49. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  50. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  51. data/test/hexapdf/type/signature/common.rb +71 -0
  52. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  53. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  54. data/test/hexapdf/type/signature/test_handler.rb +76 -0
  55. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  56. data/test/hexapdf/type/test_annotation.rb +40 -2
  57. data/test/hexapdf/type/test_font.rb +4 -0
  58. data/test/hexapdf/type/test_font_simple.rb +5 -5
  59. data/test/hexapdf/type/test_font_type3.rb +16 -1
  60. data/test/hexapdf/type/test_object_stream.rb +9 -0
  61. data/test/hexapdf/type/test_signature.rb +131 -0
  62. metadata +21 -33
  63. data/test/data/cert/create.sh +0 -171
  64. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
  65. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
  66. data/test/data/cert/root-ca/db/crlnumber +0 -1
  67. data/test/data/cert/root-ca/db/index +0 -2
  68. data/test/data/cert/root-ca/db/index.attr +0 -1
  69. data/test/data/cert/root-ca/db/index.attr.old +0 -1
  70. data/test/data/cert/root-ca/db/index.old +0 -1
  71. data/test/data/cert/root-ca/db/serial +0 -1
  72. data/test/data/cert/root-ca/db/serial.old +0 -1
  73. data/test/data/cert/root-ca/private/root-ca.key +0 -52
  74. data/test/data/cert/root-ca/root-ca.conf +0 -65
  75. data/test/data/cert/root-ca/root-ca.crt +0 -119
  76. data/test/data/cert/root-ca/root-ca.csr +0 -28
  77. data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
  78. data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
  79. data/test/data/cert/sub-ca/db/crlnumber +0 -1
  80. data/test/data/cert/sub-ca/db/index +0 -1
  81. data/test/data/cert/sub-ca/db/index.attr +0 -1
  82. data/test/data/cert/sub-ca/db/index.old +0 -0
  83. data/test/data/cert/sub-ca/db/serial +0 -1
  84. data/test/data/cert/sub-ca/db/serial.old +0 -1
  85. data/test/data/cert/sub-ca/private/signing.key +0 -52
  86. data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
  87. data/test/data/cert/sub-ca/signing.crt +0 -125
  88. data/test/data/cert/sub-ca/signing.csr +0 -28
  89. data/test/data/cert/sub-ca/signing.p12 +0 -0
  90. data/test/data/cert/sub-ca/sub-ca.conf +0 -65
  91. data/test/data/cert/sub-ca/sub-ca.crt +0 -125
  92. data/test/data/cert/sub-ca/sub-ca.csr +0 -28
@@ -597,6 +597,11 @@ end
597
597
  describe HexaPDF::Layout::Style do
598
598
  before do
599
599
  @style = HexaPDF::Layout::Style.new
600
+ @style.font = Object.new.tap do |obj|
601
+ obj.define_singleton_method(:pdf_object) do
602
+ Object.new.tap {|pdf| pdf.define_singleton_method(:glyph_scaling_factor) { 0.001 } }
603
+ end
604
+ end
600
605
  end
601
606
 
602
607
  it "can assign values on initialization" do
@@ -644,6 +649,7 @@ describe HexaPDF::Layout::Style do
644
649
  end
645
650
 
646
651
  it "has several simple and dynamically generated properties with default values" do
652
+ @style = HexaPDF::Layout::Style.new
647
653
  assert_raises(HexaPDF::Error) { @style.font }
648
654
  assert_equal(10, @style.font_size)
649
655
  assert_equal(0, @style.character_spacing)
@@ -725,6 +731,11 @@ describe HexaPDF::Layout::Style do
725
731
  font = Object.new
726
732
  font.define_singleton_method(:scaling_factor) { 1 }
727
733
  font.define_singleton_method(:wrapped_font) { wrapped_font }
734
+ font.define_singleton_method(:pdf_object) do
735
+ obj = Object.new
736
+ obj.define_singleton_method(:glyph_scaling_factor) { 0.001 }
737
+ obj
738
+ end
728
739
  @style.font = font
729
740
  end
730
741
 
@@ -596,6 +596,34 @@ describe HexaPDF::Document do
596
596
  end
597
597
  end
598
598
 
599
+ describe "signature interface" do
600
+ it "returns whether the document is signed or not" do
601
+ refute(@doc.signed?)
602
+
603
+ form = @doc.acro_form(create: true)
604
+ form.signature_flag(:signatures_exist)
605
+ assert(@doc.signed?)
606
+ end
607
+
608
+ it "returns all signature fields of the document" do
609
+ form = @doc.acro_form(create: true)
610
+ sig1 = @doc.add({FT: :Sig, T: 'sig1', V: :sig1})
611
+ sig2 = @doc.add({FT: :Sig, T: 'sig2', V: :sig2})
612
+ form.root_fields << sig1 << sig2
613
+ assert_equal([:sig1, :sig2], @doc.signatures.to_a)
614
+ end
615
+
616
+ it "allows to conveniently sign a document" do
617
+ mock = Minitest::Mock.new
618
+ mock.expect(:handler, :handler, [{name: :handler, opt: :key}])
619
+ mock.expect(:add, :added, [:io, :handler, {signature: :sig, write_options: :write_options}])
620
+ @doc.instance_variable_set(:@signatures, mock)
621
+ result = @doc.sign(:io, handler: :handler, write_options: :write_options, signature: :sig, opt: :key)
622
+ assert_equal(:added, result)
623
+ mock.verify
624
+ end
625
+ end
626
+
599
627
  describe "listener interface" do
600
628
  it "allows registering and dispatching messages" do
601
629
  args = []
@@ -182,7 +182,7 @@ describe HexaPDF::Object do
182
182
  assert_match(/\[5, 0\].*value=5/, obj.inspect)
183
183
  end
184
184
 
185
- it "can be compared to another object or reference" do
185
+ it "can be compared to another object, reference or, if not indirect, a simple value" do
186
186
  obj = HexaPDF::Object.new(5, oid: 5)
187
187
 
188
188
  assert_equal(obj, HexaPDF::Object.new(obj))
@@ -192,6 +192,11 @@ describe HexaPDF::Object do
192
192
  refute_equal(obj, HexaPDF::Object.new(5, oid: 5, gen: 1))
193
193
 
194
194
  assert_equal(obj, HexaPDF::Reference.new(5, 0))
195
+
196
+ refute_equal(obj, 5)
197
+ obj.data.oid = 0
198
+ assert_equal(obj, 5)
199
+ assert_equal(obj, HexaPDF::Object.new(5))
195
200
  end
196
201
 
197
202
  it "works correctly as hash key, is interchangable in this regard with Reference objects" do
@@ -216,7 +221,7 @@ describe HexaPDF::Object do
216
221
  it "creates an independent object" do
217
222
  obj = HexaPDF::Object.new({a: "mystring", b: HexaPDF::Reference.new(1, 0), c: 5})
218
223
  copy = obj.deep_copy
219
- refute_equal(copy, obj)
224
+ refute_same(copy, obj)
220
225
  assert_equal(copy.value, obj.value)
221
226
  refute_same(copy.value[:a], obj.value[:a])
222
227
  end
@@ -338,6 +338,11 @@ describe HexaPDF::Parser do
338
338
  assert_equal(5, @parser.startxref_offset)
339
339
  end
340
340
 
341
+ it "handles the case of startxref and its value being on the same line" do
342
+ create_parser("startxref 5\n%%EOF")
343
+ assert_equal(5, @parser.startxref_offset)
344
+ end
345
+
341
346
  it "fails even in big files when nothing is found" do
342
347
  create_parser("\nhallo" * 5000)
343
348
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
@@ -366,6 +371,13 @@ describe HexaPDF::Parser do
366
371
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
367
372
  assert_match(/end-of-file marker not found/, exp.message)
368
373
  end
374
+
375
+ it "fails on strict parsing if the startxref is on the same line as its value" do
376
+ @document.config['parser.on_correctable_error'] = proc { true }
377
+ create_parser("startxref 5\n%%EOF")
378
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
379
+ assert_match(/startxref on same line/, exp.message)
380
+ end
369
381
  end
370
382
 
371
383
  describe "file_header_version" do
@@ -531,12 +543,14 @@ describe HexaPDF::Parser do
531
543
  xref_section, trailer = @parser.load_revision(@parser.startxref_offset)
532
544
  assert_equal({Test: 'now'}, trailer)
533
545
  assert(xref_section[1].in_use?)
546
+ refute(@parser.contains_xref_streams?)
534
547
  end
535
548
 
536
549
  it "works for a cross-reference stream" do
537
550
  xref_section, trailer = @parser.load_revision(212)
538
551
  assert_equal({Size: 2}, trailer)
539
552
  assert(xref_section[1].in_use?)
553
+ assert(@parser.contains_xref_streams?)
540
554
  end
541
555
 
542
556
  it "fails if another object is found instead of a cross-reference stream" do
@@ -50,13 +50,6 @@ describe HexaPDF::Rectangle do
50
50
  assert_equal(12, rect.top)
51
51
  end
52
52
 
53
- it "allows comparison to arrays" do
54
- rect = HexaPDF::Rectangle.new([0, 1, 2, 5])
55
- assert(rect == [0, 1, 2, 5])
56
- rect.oid = 5
57
- refute(rect == [0, 1, 2, 5])
58
- end
59
-
60
53
  describe "validation" do
61
54
  it "ensures that it is a correct PDF rectangle" do
62
55
  doc = HexaPDF::Document.new
@@ -2,9 +2,10 @@
2
2
 
3
3
  require 'test_helper'
4
4
  require 'hexapdf/revision'
5
- require 'hexapdf/object'
5
+ require 'hexapdf/dictionary'
6
6
  require 'hexapdf/reference'
7
7
  require 'hexapdf/xref_section'
8
+ require 'hexapdf/type/catalog'
8
9
  require 'stringio'
9
10
 
10
11
  describe HexaPDF::Revision do
@@ -12,6 +13,10 @@ describe HexaPDF::Revision do
12
13
  @xref_section = HexaPDF::XRefSection.new
13
14
  @xref_section.add_in_use_entry(2, 0, 5000)
14
15
  @xref_section.add_free_entry(3, 0)
16
+ @xref_section.add_in_use_entry(4, 0, 1000)
17
+ @xref_section.add_in_use_entry(5, 0, 1000)
18
+ @xref_section.add_in_use_entry(6, 0, 5000)
19
+ @xref_section.add_in_use_entry(7, 0, 5000)
15
20
  @obj = HexaPDF::Object.new(:val, oid: 1, gen: 0)
16
21
  @ref = HexaPDF::Reference.new(1, 0)
17
22
 
@@ -19,7 +24,13 @@ describe HexaPDF::Revision do
19
24
  if entry.type == :free
20
25
  HexaPDF::Object.new(nil, oid: entry.oid, gen: entry.gen)
21
26
  else
22
- HexaPDF::Object.new(:Test, oid: entry.oid, gen: entry.gen)
27
+ case entry.oid
28
+ when 4 then HexaPDF::Dictionary.new({Type: :XRef}, oid: entry.oid, gen: entry.gen)
29
+ when 5 then HexaPDF::Dictionary.new({Type: :ObjStm}, oid: entry.oid, gen: entry.gen)
30
+ when 7 then HexaPDF::Type::Catalog.new({Type: :Catalog}, oid: entry.oid, gen: entry.gen,
31
+ document: self)
32
+ else HexaPDF::Object.new(:Test, oid: entry.oid, gen: entry.gen)
33
+ end
23
34
  end
24
35
  end
25
36
  @rev = HexaPDF::Revision.new({}, xref_section: @xref_section, loader: @loader)
@@ -36,10 +47,10 @@ describe HexaPDF::Revision do
36
47
  end
37
48
 
38
49
  it "returns the next free object number" do
39
- assert_equal(4, @rev.next_free_oid)
40
- @obj.oid = 4
50
+ assert_equal(8, @rev.next_free_oid)
51
+ @obj.oid = 8
41
52
  @rev.add(@obj)
42
- assert_equal(5, @rev.next_free_oid)
53
+ assert_equal(9, @rev.next_free_oid)
43
54
  end
44
55
 
45
56
  describe "add" do
@@ -151,15 +162,14 @@ describe HexaPDF::Revision do
151
162
  assert(@rev.object(@ref).null?)
152
163
  assert(@obj.null?)
153
164
  assert_raises(HexaPDF::Error) { @obj.document }
165
+ assert_same(@obj.data, @rev.object(@ref).data)
154
166
  end
155
167
  end
156
168
 
157
169
  describe "object iteration" do
158
170
  it "iterates over all objects via each" do
159
171
  @rev.add(@obj)
160
- obj2 = @rev.object(2)
161
- obj3 = @rev.object(3)
162
- assert_equal([@obj, obj2, obj3], @rev.each.to_a)
172
+ assert_equal([@obj, *(2..7).map {|i| @rev.object(i) }], @rev.each.to_a)
163
173
  end
164
174
 
165
175
  it "iterates only over loaded objects" do
@@ -178,11 +188,31 @@ describe HexaPDF::Revision do
178
188
  refute(rev.object?(@ref))
179
189
  end
180
190
 
181
- it "can iterate over all modified objects" do
182
- obj = @rev.object(2)
183
- assert_equal([], @rev.each_modified_object.to_a)
184
- obj.value = :Other
185
- @rev.add(@obj)
186
- assert_equal([obj, @obj], @rev.each_modified_object.to_a)
191
+ describe "each_modified_object" do
192
+ it "returns modified objects" do
193
+ obj = @rev.object(2)
194
+ obj.value = :Other
195
+ @rev.add(@obj)
196
+ deleted = @rev.object(6)
197
+ @rev.delete(6)
198
+ assert_equal([obj, @obj, deleted], @rev.each_modified_object.to_a)
199
+ end
200
+
201
+ it "ignores object and xref streams that were deleted" do
202
+ @rev.delete(4)
203
+ @rev.delete(5)
204
+ assert_equal([], @rev.each_modified_object.to_a)
205
+ end
206
+
207
+ it "doesn't return non-modified objects" do
208
+ @rev.object(2)
209
+ assert_equal([], @rev.each_modified_object.to_a)
210
+ end
211
+
212
+ it "doesn't return objects that have modified values just because of reading" do
213
+ obj = @rev.object(7)
214
+ obj.delete(:Type)
215
+ assert_equal([], @rev.each_modified_object.to_a)
216
+ end
187
217
  end
188
218
  end
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.19.0)>>
43
+ <</Producer(HexaPDF version 0.20.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.19.0)>>
75
+ <</Producer(HexaPDF version 0.20.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -94,7 +94,8 @@ describe HexaPDF::Writer do
94
94
  document = HexaPDF::Document.new(io: input_io)
95
95
  document.trailer.info[:Producer] = "unknown"
96
96
  output_io = StringIO.new(''.force_encoding(Encoding::BINARY))
97
- HexaPDF::Writer.write(document, output_io)
97
+ xref_section = HexaPDF::Writer.write(document, output_io)
98
+ assert_kind_of(HexaPDF::XRefSection, xref_section)
98
99
  assert_equal(input_io.string, output_io.string)
99
100
  end
100
101
 
@@ -103,21 +104,50 @@ describe HexaPDF::Writer do
103
104
  assert_document_conversion(@compressed_input_io)
104
105
  end
105
106
 
106
- it "writes a document in incremental mode" do
107
- doc = HexaPDF::Document.new(io: @std_input_io)
108
- doc.pages.add
109
- output_io = StringIO.new
110
- HexaPDF::Writer.write(doc, output_io, incremental: true)
111
- assert_equal(output_io.string[0, @std_input_io.string.length], @std_input_io.string)
112
- doc = HexaPDF::Document.new(io: output_io)
113
- assert_equal(4, doc.revisions.size)
114
- assert_equal(2, doc.revisions.current.each.to_a.size)
107
+ describe "write_incremental" do
108
+ it "writes a document in incremental mode" do
109
+ doc = HexaPDF::Document.new(io: @std_input_io)
110
+ doc.pages.add
111
+ output_io = StringIO.new
112
+ HexaPDF::Writer.write(doc, output_io, incremental: true)
113
+ assert_equal(output_io.string[0, @std_input_io.string.length], @std_input_io.string)
114
+ doc = HexaPDF::Document.new(io: output_io)
115
+ assert_equal(4, doc.revisions.size)
116
+ assert_equal(2, doc.revisions.current.each.to_a.size)
117
+ end
118
+
119
+ it "uses an xref stream if the document already contains at least one" do
120
+ doc = HexaPDF::Document.new(io: @compressed_input_io)
121
+ doc.pages.add
122
+ output_io = StringIO.new
123
+ HexaPDF::Writer.write(doc, output_io, incremental: true)
124
+ refute_match(/^trailer/, output_io.string)
125
+ end
115
126
  end
116
127
 
117
- it "raises an error if no xref stream is in a revision but object streams are" do
128
+ it "creates an xref stream if no xref stream is in a revision but object streams are" do
118
129
  document = HexaPDF::Document.new
119
130
  document.add({Type: :ObjStm})
120
- assert_raises(HexaPDF::Error) { HexaPDF::Writer.new(document, StringIO.new).write }
131
+ HexaPDF::Writer.new(document, StringIO.new).write
132
+ assert(:XRef, document.object(2).type)
133
+ end
134
+
135
+ it "creates an xref stream if a previous revision had one" do
136
+ document = HexaPDF::Document.new
137
+ document.pages.add
138
+ document.revisions.add
139
+ document.pages.add
140
+ document.add({Type: :ObjStm})
141
+ document.revisions.add
142
+ document.pages.add
143
+ io = StringIO.new
144
+ HexaPDF::Writer.new(document, io).write
145
+
146
+ document = HexaPDF::Document.new(io: io)
147
+ assert_equal(3, document.revisions.count)
148
+ assert(document.revisions[0].none? {|obj| obj.type == :XRef })
149
+ assert(document.revisions[1].one? {|obj| obj.type == :XRef })
150
+ assert(document.revisions[2].one? {|obj| obj.type == :XRef })
121
151
  end
122
152
 
123
153
  it "raises an error if the class is misused and an xref section contains invalid entries" do
@@ -155,6 +155,16 @@ describe HexaPDF::Type::AcroForm::Field do
155
155
  assert_equal(5, widget[:X])
156
156
  end
157
157
 
158
+ it "sets the print flag on the widget" do
159
+ widget = @field.create_widget(@page, X: 5)
160
+ assert_equal([:print], widget.flags)
161
+ end
162
+
163
+ it "associates the page with the widget" do
164
+ widget = @field.create_widget(@page, X: 5)
165
+ assert_same(@page, widget[:P])
166
+ end
167
+
158
168
  it "adds the new widget to the given page's annotations" do
159
169
  widget = @field.create_widget(@page)
160
170
  assert_equal([widget], @page[:Annots].value)
@@ -179,7 +189,7 @@ describe HexaPDF::Type::AcroForm::Field do
179
189
  refute_same(widget1, kids[0])
180
190
  assert_same(widget2, kids[1])
181
191
  assert_nil(@field[:Rect])
182
- assert_equal({Rect: [1, 2, 3, 4], Type: :Annot, Subtype: :Widget, Parent: @field},
192
+ assert_equal({Rect: [1, 2, 3, 4], Type: :Annot, Subtype: :Widget, Parent: @field, F: 4, P: @page},
183
193
  kids[0].value)
184
194
  assert_equal([2, 1, 4, 3], kids[1][:Rect].value)
185
195
 
@@ -226,6 +226,11 @@ describe HexaPDF::Type::AcroForm::Form do
226
226
  assert(field.flagged?(:multi_select))
227
227
  applies_variable_text_properties(:create_list_box)
228
228
  end
229
+
230
+ it "creates a signature field" do
231
+ field = @acro_form.create_signature_field("field")
232
+ assert_equal(:signature_field, field.concrete_field_type)
233
+ end
229
234
  end
230
235
 
231
236
  it "returns the default resources" do
@@ -0,0 +1,71 @@
1
+ require 'openssl'
2
+
3
+ module TestHelper
4
+
5
+ class Certificates
6
+
7
+ def ca_key
8
+ @ca_key ||= OpenSSL::PKey::RSA.new(512)
9
+ end
10
+
11
+ def ca_certificate
12
+ @ca_cert ||=
13
+ begin
14
+ ca_name = OpenSSL::X509::Name.parse('/C=AT/O=HexaPDF/CN=HexaPDF Test Root CA')
15
+
16
+ ca_cert = OpenSSL::X509::Certificate.new
17
+ ca_cert.serial = 0
18
+ ca_cert.version = 2
19
+ ca_cert.not_before = Time.now - 86400
20
+ ca_cert.not_after = Time.now + 86400
21
+ ca_cert.public_key = ca_key.public_key
22
+ ca_cert.subject = ca_name
23
+ ca_cert.issuer = ca_name
24
+
25
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
26
+ extension_factory.subject_certificate = ca_cert
27
+ extension_factory.issuer_certificate = ca_cert
28
+ ca_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
29
+ ca_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:TRUE', true))
30
+ ca_cert.add_extension(extension_factory.create_extension('keyUsage', 'cRLSign,keyCertSign', true))
31
+ ca_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
32
+
33
+ ca_cert
34
+ end
35
+ end
36
+
37
+ def signer_key
38
+ @signer_key ||= OpenSSL::PKey::RSA.new(512)
39
+ end
40
+
41
+ def signer_certificate
42
+ @signer_certificate ||=
43
+ begin
44
+ name = OpenSSL::X509::Name.parse('/CN=signer/DC=gettalong')
45
+
46
+ signer_cert = OpenSSL::X509::Certificate.new
47
+ signer_cert.serial = 2
48
+ signer_cert.version = 2
49
+ signer_cert.not_before = Time.now - 86400
50
+ signer_cert.not_after = Time.now + 86400
51
+ signer_cert.public_key = signer_key.public_key
52
+ signer_cert.subject = name
53
+ signer_cert.issuer = ca_certificate.subject
54
+
55
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
56
+ extension_factory.subject_certificate = signer_cert
57
+ extension_factory.issuer_certificate = ca_certificate
58
+ signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
59
+ signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
60
+ signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
61
+ signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
62
+
63
+ signer_cert
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ CERTIFICATES = TestHelper::Certificates.new
@@ -0,0 +1,99 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative 'common'
5
+ require 'hexapdf/type/signature'
6
+ require 'ostruct'
7
+
8
+ describe HexaPDF::Type::Signature::AdbePkcs7Detached do
9
+ before do
10
+ @data = 'Some data'
11
+ @dict = OpenStruct.new
12
+ @pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.signer_certificate, CERTIFICATES.signer_key,
13
+ @data, [CERTIFICATES.ca_certificate],
14
+ OpenSSL::PKCS7::DETACHED)
15
+ @dict.contents = @pkcs7.to_der
16
+ @dict.signed_data = @data
17
+ @handler = HexaPDF::Type::Signature::AdbePkcs7Detached.new(@dict)
18
+ end
19
+
20
+ it "returns the signer name" do
21
+ assert_equal("signer", @handler.signer_name)
22
+ end
23
+
24
+ it "returns the signing time" do
25
+ assert_equal(@pkcs7.signers.first.signed_time, @handler.signing_time)
26
+ end
27
+
28
+ it "returns the certificate chain" do
29
+ assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
30
+ @handler.certificate_chain)
31
+ end
32
+
33
+ it "returns the signer certificate" do
34
+ assert_equal(CERTIFICATES.signer_certificate, @handler.signer_certificate)
35
+ end
36
+
37
+ it "allows access to the signer information" do
38
+ info = @handler.signer_info
39
+ assert(info)
40
+ assert_equal(2, info.serial)
41
+ assert_equal(CERTIFICATES.signer_certificate.issuer, info.issuer)
42
+ end
43
+
44
+ describe "verify" do
45
+ before do
46
+ @store = OpenSSL::X509::Store.new
47
+ @store.add_cert(CERTIFICATES.ca_certificate)
48
+ end
49
+
50
+ it "logs an error if there are no certificates" do
51
+ def @handler.certificate_chain; []; end
52
+ result = @handler.verify(@store)
53
+ assert_equal(1, result.messages.size)
54
+ assert_equal(:error, result.messages.first.type)
55
+ assert_match(/No certificates/, result.messages.first.content)
56
+ end
57
+
58
+ it "logs an error if there is more than one signer" do
59
+ @pkcs7.add_signer(OpenSSL::PKCS7::SignerInfo.new(CERTIFICATES.signer_certificate,
60
+ CERTIFICATES.signer_key, 'SHA1'))
61
+ @dict.contents = @pkcs7.to_der
62
+ @handler = HexaPDF::Type::Signature::AdbePkcs7Detached.new(@dict)
63
+ result = @handler.verify(@store)
64
+ assert_equal(2, result.messages.size)
65
+ assert_equal(:error, result.messages.first.type)
66
+ assert_match(/Exactly one signer needed/, result.messages.first.content)
67
+ end
68
+
69
+ it "logs an error if the signer certificate is not found" do
70
+ def @handler.signer_certificate; nil end
71
+ result = @handler.verify(@store)
72
+ assert_equal(1, result.messages.size)
73
+ assert_equal(:error, result.messages.first.type)
74
+ assert_match(/Signer.*not found/, result.messages.first.content)
75
+ end
76
+
77
+ it "logs an error if the signer certificate is not usable for digital signatures" do
78
+ @pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.ca_certificate, CERTIFICATES.ca_key,
79
+ @data, [CERTIFICATES.ca_certificate],
80
+ OpenSSL::PKCS7::DETACHED)
81
+ @dict.contents = @pkcs7.to_der
82
+ @handler = HexaPDF::Type::Signature::AdbePkcs7Detached.new(@dict)
83
+ result = @handler.verify(@store)
84
+ assert_equal(:error, result.messages.first.type)
85
+ assert_match(/key usage is missing 'Digital Signature'/, result.messages.first.content)
86
+ end
87
+
88
+ it "verifies the signature itself" do
89
+ result = @handler.verify(@store)
90
+ assert_equal(:info, result.messages.last.type)
91
+ assert_match(/Signature valid/, result.messages.last.content)
92
+
93
+ @dict.signed_data = 'other data'
94
+ result = @handler.verify(@store)
95
+ assert_equal(:error, result.messages.last.type)
96
+ assert_match(/Signature verification failed/, result.messages.last.content)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,66 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative 'common'
5
+ require 'hexapdf/type/signature'
6
+ require 'ostruct'
7
+
8
+ describe HexaPDF::Type::Signature::AdbeX509RsaSha1 do
9
+ before do
10
+ @data = 'Some data'
11
+ @dict = OpenStruct.new
12
+ @dict.signed_data = @data
13
+ encoded_data = CERTIFICATES.signer_key.sign(OpenSSL::Digest.new('SHA1'), @data)
14
+ @dict.contents = OpenSSL::ASN1::OctetString.new(encoded_data).to_der
15
+ @dict.Cert = [CERTIFICATES.signer_certificate.to_der]
16
+ def @dict.key?(*); true; end
17
+ @handler = HexaPDF::Type::Signature::AdbeX509RsaSha1.new(@dict)
18
+ end
19
+
20
+ it "returns the certificate chain" do
21
+ assert_equal([CERTIFICATES.signer_certificate], @handler.certificate_chain)
22
+
23
+ @dict.singleton_class.undef_method(:key?)
24
+ def @dict.key?(*); false; end
25
+ assert_equal([], @handler.certificate_chain)
26
+ end
27
+
28
+ it "returns the signer certificate" do
29
+ assert_equal(CERTIFICATES.signer_certificate, @handler.signer_certificate)
30
+ end
31
+
32
+ describe "verify" do
33
+ before do
34
+ @store = OpenSSL::X509::Store.new
35
+ @store.set_default_paths
36
+ @store.purpose = OpenSSL::X509::PURPOSE_SMIME_SIGN
37
+ end
38
+
39
+ it "logs an error if there are no certificates" do
40
+ def @handler.certificate_chain; []; end
41
+ result = @handler.verify(@store)
42
+ assert_equal(1, result.messages.size)
43
+ assert_equal(:error, result.messages.first.type)
44
+ assert_match(/No certificates/, result.messages.first.content)
45
+ end
46
+
47
+ it "logs an error if signature contents is not of the expected type" do
48
+ @dict.contents = OpenSSL::ASN1::Boolean.new(true).to_der
49
+ result = @handler.verify(@store)
50
+ assert_equal(1, result.messages.size)
51
+ assert_equal(:error, result.messages.first.type)
52
+ assert_match(/signature object invalid/, result.messages.first.content)
53
+ end
54
+
55
+ it "verifies the signature itself" do
56
+ result = @handler.verify(@store)
57
+ assert_equal(:info, result.messages.last.type)
58
+ assert_match(/Signature valid/, result.messages.last.content)
59
+
60
+ @dict.signed_data = 'other data'
61
+ result = @handler.verify(@store)
62
+ assert_equal(:error, result.messages.last.type)
63
+ assert_match(/Signature verification failed/, result.messages.last.content)
64
+ end
65
+ end
66
+ end