hexapdf 0.19.3 → 0.20.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -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/data/hexapdf/encoding/glyphlist.txt +4283 -4282
  9. data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
  10. data/lib/hexapdf/cli/form.rb +9 -1
  11. data/lib/hexapdf/cli/info.rb +21 -1
  12. data/lib/hexapdf/configuration.rb +26 -0
  13. data/lib/hexapdf/content/processor.rb +1 -1
  14. data/lib/hexapdf/document/signatures.rb +327 -0
  15. data/lib/hexapdf/document.rb +26 -0
  16. data/lib/hexapdf/encryption/security_handler.rb +5 -1
  17. data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
  18. data/lib/hexapdf/importer.rb +1 -1
  19. data/lib/hexapdf/object.rb +5 -3
  20. data/lib/hexapdf/rectangle.rb +0 -6
  21. data/lib/hexapdf/revision.rb +13 -6
  22. data/lib/hexapdf/task/dereference.rb +12 -4
  23. data/lib/hexapdf/task/optimize.rb +3 -3
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  25. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  26. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  27. data/lib/hexapdf/type/annotation.rb +37 -4
  28. data/lib/hexapdf/type/font_simple.rb +1 -1
  29. data/lib/hexapdf/type/object_stream.rb +3 -1
  30. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
  31. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
  32. data/lib/hexapdf/type/signature/handler.rb +140 -0
  33. data/lib/hexapdf/type/signature/verification_result.rb +92 -0
  34. data/lib/hexapdf/type/signature.rb +236 -0
  35. data/lib/hexapdf/type.rb +1 -0
  36. data/lib/hexapdf/version.rb +1 -1
  37. data/lib/hexapdf/writer.rb +27 -11
  38. data/test/hexapdf/content/test_processor.rb +1 -1
  39. data/test/hexapdf/document/test_signatures.rb +225 -0
  40. data/test/hexapdf/encryption/test_security_handler.rb +11 -3
  41. data/test/hexapdf/task/test_optimize.rb +4 -1
  42. data/test/hexapdf/test_document.rb +28 -0
  43. data/test/hexapdf/test_object.rb +7 -2
  44. data/test/hexapdf/test_rectangle.rb +0 -7
  45. data/test/hexapdf/test_revision.rb +44 -14
  46. data/test/hexapdf/test_writer.rb +15 -3
  47. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  48. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  49. data/test/hexapdf/type/signature/common.rb +71 -0
  50. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  51. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  52. data/test/hexapdf/type/signature/test_handler.rb +102 -0
  53. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  54. data/test/hexapdf/type/test_annotation.rb +47 -3
  55. data/test/hexapdf/type/test_font_simple.rb +5 -5
  56. data/test/hexapdf/type/test_object_stream.rb +9 -0
  57. data/test/hexapdf/type/test_signature.rb +131 -0
  58. metadata +21 -3
@@ -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.3)>>
43
+ <</Producer(HexaPDF version 0.20.3)>>
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.3)>>
75
+ <</Producer(HexaPDF version 0.20.3)>>
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
 
@@ -122,6 +123,17 @@ describe HexaPDF::Writer do
122
123
  HexaPDF::Writer.write(doc, output_io, incremental: true)
123
124
  refute_match(/^trailer/, output_io.string)
124
125
  end
126
+
127
+ it "raises an error if the used encryption was changed" do
128
+ io = StringIO.new
129
+ doc = HexaPDF::Document.new
130
+ doc.encrypt
131
+ doc.write(io)
132
+
133
+ doc = HexaPDF::Document.new(io: io)
134
+ doc.encrypt(owner_password: 'test')
135
+ assert_raises(HexaPDF::Error) { doc.write('notused', incremental: true) }
136
+ end
125
137
  end
126
138
 
127
139
  it "creates an xref stream if no xref stream is in a revision but object streams are" 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
@@ -0,0 +1,102 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/type/signature'
5
+ require 'hexapdf/document'
6
+ require 'time'
7
+ require 'ostruct'
8
+
9
+ describe HexaPDF::Type::Signature::Handler do
10
+ before do
11
+ @time = Time.parse("2021-11-14 7:00")
12
+ @dict = {Name: "handler", M: @time}
13
+ @handler = HexaPDF::Type::Signature::Handler.new(@dict)
14
+ @result = HexaPDF::Type::Signature::VerificationResult.new
15
+ end
16
+
17
+ it "returns the signer name" do
18
+ assert_equal("handler", @handler.signer_name)
19
+ end
20
+
21
+ it "returns the signing time" do
22
+ assert_equal(@time, @handler.signing_time)
23
+ end
24
+
25
+ it "needs an implementation of certificate_chain" do
26
+ assert_raises(RuntimeError) { @handler.certificate_chain }
27
+ end
28
+
29
+ it "needs an implementation of signer_certificate" do
30
+ assert_raises(RuntimeError) { @handler.signer_certificate }
31
+ end
32
+
33
+ describe "store_verification_callback" do
34
+ before do
35
+ @context = OpenStruct.new
36
+ end
37
+
38
+ it "can allow self-signed certificates" do
39
+ [OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN,
40
+ OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN].each do |error|
41
+ [true, false].each do |allow_self_signed|
42
+ @result.messages.clear
43
+ @context.error = error
44
+ @handler.send(:store_verification_callback, @result, allow_self_signed: allow_self_signed).
45
+ call(false, @context)
46
+ assert_equal(1, @result.messages.size)
47
+ assert_match(/self-signed certificate/i, @result.messages[0].content)
48
+ assert_equal(allow_self_signed ? :info : :error, @result.messages[0].type)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ it "verifies the signing time" do
55
+ [
56
+ [true, '6:00', '8:00'],
57
+ [false, '7:30', '8:00'],
58
+ [false, '5:00', '6:00'],
59
+ ].each do |success, not_before, not_after|
60
+ @result.messages.clear
61
+ @handler.define_singleton_method(:signer_certificate) do
62
+ OpenStruct.new.tap do |struct|
63
+ struct.not_before = Time.parse("2021-11-14 #{not_before}")
64
+ struct.not_after = Time.parse("2021-11-14 #{not_after}")
65
+ end
66
+ end
67
+ @handler.send(:verify_signing_time, @result)
68
+ if success
69
+ assert(@result.messages.empty?)
70
+ else
71
+ assert_equal(1, @result.messages.size)
72
+ end
73
+ @handler.singleton_class.remove_method(:signer_certificate)
74
+ end
75
+ end
76
+
77
+ describe "check_certified_signature" do
78
+ before do
79
+ @dict = HexaPDF::Document.new.wrap({Type: :Sig})
80
+ @handler.instance_variable_set(:@signature_dict, @dict)
81
+ end
82
+
83
+ it "logs nothing if there is no signature reference dictionary" do
84
+ @handler.send(:check_certified_signature, @result)
85
+ assert(@result.messages.empty?)
86
+ end
87
+
88
+ it "logs nothing if the global DocMDP permissions entry doesn't point to the signature" do
89
+ @dict[:Reference] = [{TransformMethod: :DocMDP}]
90
+ @handler.send(:check_certified_signature, @result)
91
+ assert(@result.messages.empty?)
92
+ end
93
+
94
+ it "logs a message if the signature is a certified one" do
95
+ @dict[:Reference] = [{TransformMethod: :DocMDP}]
96
+ @dict.document.catalog[:Perms] = {DocMDP: @dict}
97
+ @handler.send(:check_certified_signature, @result)
98
+ assert_equal(1, @result.messages.size)
99
+ assert_match(/certified signature/i, @result.messages[0].content)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,47 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/type/signature'
5
+
6
+ describe HexaPDF::Type::Signature::VerificationResult do
7
+ describe "Message" do
8
+ it "accepts a type and a content argument on creation" do
9
+ m = HexaPDF::Type::Signature::VerificationResult::Message.new(:type, 'content')
10
+ assert_equal(:type, m.type)
11
+ assert_equal('content', m.content)
12
+ end
13
+
14
+ it "allows sorting by type" do
15
+ info = HexaPDF::Type::Signature::VerificationResult::Message.new(:info, 'c')
16
+ warning = HexaPDF::Type::Signature::VerificationResult::Message.new(:warning, 'c')
17
+ error = HexaPDF::Type::Signature::VerificationResult::Message.new(:error, 'c')
18
+ assert_equal([error, warning, info], [info, error, warning].sort)
19
+ end
20
+ end
21
+
22
+ before do
23
+ @result = HexaPDF::Type::Signature::VerificationResult.new
24
+ end
25
+
26
+ it "can add new messages" do
27
+ @result.log(:error, "content")
28
+ assert_equal(1, @result.messages.size)
29
+ assert_equal(:error, @result.messages[0].type)
30
+ assert_equal('content', @result.messages[0].content)
31
+ end
32
+
33
+ it "reports success if no error messages have been logged" do
34
+ assert(@result.success?)
35
+ @result.log(:info, 'content')
36
+ assert(@result.success?)
37
+ @result.log(:error, 'failure')
38
+ refute(@result.success?)
39
+ end
40
+
41
+ it "reports failure if there is at least one error message" do
42
+ @result.log(:info, 'content')
43
+ refute(@result.failure?)
44
+ @result.log(:error, 'failure')
45
+ assert(@result.failure?)
46
+ end
47
+ end
@@ -25,12 +25,33 @@ describe HexaPDF::Type::Annotation::AppearanceDictionary do
25
25
  @ap.delete(:D)
26
26
  assert_equal(:n, @ap.down_appearance)
27
27
  end
28
+
29
+ describe "set_appearance" do
30
+ it "sets the appearance for the given type" do
31
+ @ap.set_appearance(1, type: :normal)
32
+ @ap.set_appearance(2, type: :rollover)
33
+ @ap.set_appearance(3, type: :down)
34
+
35
+ assert_equal(1, @ap.normal_appearance)
36
+ assert_equal(2, @ap.rollover_appearance)
37
+ assert_equal(3, @ap.down_appearance)
38
+ end
39
+
40
+ it "respects the provided state name" do
41
+ @ap.set_appearance(1, state_name: :X)
42
+ assert_equal(1, @ap.normal_appearance[:X])
43
+ end
44
+
45
+ it "fails if an invalid appearance type is specified" do
46
+ assert_raises(ArgumentError) { @ap.set_appearance(5, type: :other) }
47
+ end
48
+ end
28
49
  end
29
50
 
30
51
  describe HexaPDF::Type::Annotation do
31
52
  before do
32
53
  @doc = HexaPDF::Document.new
33
- @annot = @doc.add({Type: :Annot, F: 0b100011})
54
+ @annot = @doc.add({Type: :Annot, F: 0b100011, Rect: [10, 10, 110, 60]})
34
55
  end
35
56
 
36
57
  it "must always be indirect" do
@@ -56,7 +77,13 @@ describe HexaPDF::Type::Annotation do
56
77
  stream[:BBox] = [1, 2, 3, 4]
57
78
  appearance = @annot.appearance
58
79
  assert_same(stream.data, appearance.data)
59
- assert_equal(:Form, appearance[:Subtype])
80
+ assert_kind_of(HexaPDF::Type::Form, appearance)
81
+
82
+ stream[:Type] = :XObject
83
+ stream[:Subtype] = :Form
84
+ appearance = @annot.appearance
85
+ assert_same(stream.data, appearance.data)
86
+ assert_kind_of(HexaPDF::Type::Form, appearance)
60
87
 
61
88
  @annot[:AP][:N] = {X: {}}
62
89
  assert_nil(@annot.appearance)
@@ -66,7 +93,24 @@ describe HexaPDF::Type::Annotation do
66
93
  assert_same(stream.data, @annot.appearance.data)
67
94
 
68
95
  @annot[:AP][:D] = {X: stream}
69
- assert_same(stream.data, @annot.appearance(:down).data)
96
+ assert_same(stream.data, @annot.appearance(type: :down).data)
97
+ assert_same(stream.data, @annot.appearance(type: :down, state_name: :X).data)
98
+ end
99
+
100
+ describe "create_appearance" do
101
+ it "creates the appearance stream directly underneath /AP" do
102
+ stream = @annot.create_appearance
103
+ assert_same(stream, @annot.appearance_dict.normal_appearance)
104
+ end
105
+
106
+ it "respects the state name when creating the appearance" do
107
+ stream = @annot.create_appearance(type: :down, state_name: :X)
108
+ assert_same(stream, @annot.appearance_dict.down_appearance[:X])
109
+
110
+ @annot[:AS] = :X
111
+ stream = @annot.create_appearance(type: :down)
112
+ assert_same(stream, @annot.appearance_dict.down_appearance[:X])
113
+ end
70
114
  end
71
115
 
72
116
  describe "flags" do