hexapdf 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/data/hexapdf/cert/demo_cert.rb +22 -0
- data/data/hexapdf/cert/root-ca.crt +119 -0
- data/data/hexapdf/cert/signing.crt +125 -0
- data/data/hexapdf/cert/signing.key +52 -0
- data/data/hexapdf/cert/sub-ca.crt +125 -0
- data/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/graphics_state.rb +24 -5
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/document/signatures.rb +327 -0
- data/lib/hexapdf/document.rb +26 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +1 -2
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/layout/style.rb +2 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +21 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
- data/lib/hexapdf/type/acro_form/field.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +9 -1
- data/lib/hexapdf/type/annotation.rb +36 -3
- data/lib/hexapdf/type/font.rb +5 -0
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/font_type3.rb +20 -0
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +99 -0
- data/lib/hexapdf/type/signature/handler.rb +112 -0
- data/lib/hexapdf/type/signature/verification_result.rb +92 -0
- data/lib/hexapdf/type/signature.rb +236 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +24 -10
- data/test/hexapdf/content/test_graphics_state.rb +9 -1
- data/test/hexapdf/content/test_operator.rb +8 -3
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +8 -6
- data/test/hexapdf/layout/test_style.rb +11 -0
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +14 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +44 -14
- data/test/hexapdf/type/acro_form/test_field.rb +11 -1
- data/test/hexapdf/type/acro_form/test_form.rb +5 -0
- data/test/hexapdf/type/signature/common.rb +71 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
- data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
- data/test/hexapdf/type/signature/test_handler.rb +76 -0
- data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
- data/test/hexapdf/type/test_annotation.rb +40 -2
- data/test/hexapdf/type/test_font.rb +4 -0
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_font_type3.rb +16 -1
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- metadata +21 -33
- data/test/data/cert/create.sh +0 -171
- data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
- data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
- data/test/data/cert/root-ca/db/crlnumber +0 -1
- data/test/data/cert/root-ca/db/index +0 -2
- data/test/data/cert/root-ca/db/index.attr +0 -1
- data/test/data/cert/root-ca/db/index.attr.old +0 -1
- data/test/data/cert/root-ca/db/index.old +0 -1
- data/test/data/cert/root-ca/db/serial +0 -1
- data/test/data/cert/root-ca/db/serial.old +0 -1
- data/test/data/cert/root-ca/private/root-ca.key +0 -52
- data/test/data/cert/root-ca/root-ca.conf +0 -65
- data/test/data/cert/root-ca/root-ca.crt +0 -119
- data/test/data/cert/root-ca/root-ca.csr +0 -28
- data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
- data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
- data/test/data/cert/sub-ca/db/crlnumber +0 -1
- data/test/data/cert/sub-ca/db/index +0 -1
- data/test/data/cert/sub-ca/db/index.attr +0 -1
- data/test/data/cert/sub-ca/db/index.old +0 -0
- data/test/data/cert/sub-ca/db/serial +0 -1
- data/test/data/cert/sub-ca/db/serial.old +0 -1
- data/test/data/cert/sub-ca/private/signing.key +0 -52
- data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
- data/test/data/cert/sub-ca/signing.crt +0 -125
- data/test/data/cert/sub-ca/signing.csr +0 -28
- data/test/data/cert/sub-ca/signing.p12 +0 -0
- data/test/data/cert/sub-ca/sub-ca.conf +0 -65
- data/test/data/cert/sub-ca/sub-ca.crt +0 -125
- 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 = []
|
data/test/hexapdf/test_object.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
data/test/hexapdf/test_parser.rb
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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(
|
|
40
|
-
@obj.oid =
|
|
50
|
+
assert_equal(8, @rev.next_free_oid)
|
|
51
|
+
@obj.oid = 8
|
|
41
52
|
@rev.add(@obj)
|
|
42
|
-
assert_equal(
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
data/test/hexapdf/test_writer.rb
CHANGED
|
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
|
40
40
|
219
|
|
41
41
|
%%EOF
|
|
42
42
|
3 0 obj
|
|
43
|
-
<</Producer(HexaPDF version 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.
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|