hexapdf 0.19.2 → 0.20.2

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -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/info.rb +21 -1
  11. data/lib/hexapdf/configuration.rb +26 -0
  12. data/lib/hexapdf/content/processor.rb +1 -1
  13. data/lib/hexapdf/document/signatures.rb +327 -0
  14. data/lib/hexapdf/document.rb +26 -0
  15. data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
  16. data/lib/hexapdf/importer.rb +1 -1
  17. data/lib/hexapdf/object.rb +5 -3
  18. data/lib/hexapdf/parser.rb +14 -9
  19. data/lib/hexapdf/rectangle.rb +0 -6
  20. data/lib/hexapdf/revision.rb +13 -6
  21. data/lib/hexapdf/task/dereference.rb +12 -4
  22. data/lib/hexapdf/task/optimize.rb +3 -3
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
  24. data/lib/hexapdf/type/acro_form/field.rb +2 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +9 -1
  26. data/lib/hexapdf/type/annotation.rb +36 -3
  27. data/lib/hexapdf/type/font_simple.rb +1 -1
  28. data/lib/hexapdf/type/object_stream.rb +3 -1
  29. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
  30. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
  31. data/lib/hexapdf/type/signature/handler.rb +140 -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 +16 -8
  37. data/test/hexapdf/content/test_processor.rb +1 -1
  38. data/test/hexapdf/document/test_signatures.rb +225 -0
  39. data/test/hexapdf/task/test_optimize.rb +4 -1
  40. data/test/hexapdf/test_document.rb +28 -0
  41. data/test/hexapdf/test_object.rb +7 -2
  42. data/test/hexapdf/test_parser.rb +12 -0
  43. data/test/hexapdf/test_rectangle.rb +0 -7
  44. data/test/hexapdf/test_revision.rb +44 -14
  45. data/test/hexapdf/test_writer.rb +4 -3
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -1
  47. data/test/hexapdf/type/acro_form/test_form.rb +5 -0
  48. data/test/hexapdf/type/signature/common.rb +71 -0
  49. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
  50. data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
  51. data/test/hexapdf/type/signature/test_handler.rb +102 -0
  52. data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
  53. data/test/hexapdf/type/test_annotation.rb +40 -2
  54. data/test/hexapdf/type/test_font_simple.rb +5 -5
  55. data/test/hexapdf/type/test_object_stream.rb +9 -0
  56. data/test/hexapdf/type/test_signature.rb +131 -0
  57. metadata +21 -3
@@ -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.2)>>
43
+ <</Producer(HexaPDF version 0.20.2)>>
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.2)>>
75
+ <</Producer(HexaPDF version 0.20.2)>>
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
 
@@ -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
@@ -66,7 +87,24 @@ describe HexaPDF::Type::Annotation do
66
87
  assert_same(stream.data, @annot.appearance.data)
67
88
 
68
89
  @annot[:AP][:D] = {X: stream}
69
- assert_same(stream.data, @annot.appearance(:down).data)
90
+ assert_same(stream.data, @annot.appearance(type: :down).data)
91
+ assert_same(stream.data, @annot.appearance(type: :down, state_name: :X).data)
92
+ end
93
+
94
+ describe "create_appearance" do
95
+ it "creates the appearance stream directly underneath /AP" do
96
+ stream = @annot.create_appearance
97
+ assert_same(stream, @annot.appearance_dict.normal_appearance)
98
+ end
99
+
100
+ it "respects the state name when creating the appearance" do
101
+ stream = @annot.create_appearance(type: :down, state_name: :X)
102
+ assert_same(stream, @annot.appearance_dict.down_appearance[:X])
103
+
104
+ @annot[:AS] = :X
105
+ stream = @annot.create_appearance(type: :down)
106
+ assert_same(stream, @annot.appearance_dict.down_appearance[:X])
107
+ end
70
108
  end
71
109
 
72
110
  describe "flags" do
@@ -25,7 +25,7 @@ describe HexaPDF::Type::FontSimple do
25
25
  describe "encoding" do
26
26
  it "fails if /Encoding is absent because encoding_from_font is not implemented" do
27
27
  @font.delete(:Encoding)
28
- assert_raises(NotImplementedError) { @font.encoding }
28
+ assert_raises(RuntimeError) { @font.encoding }
29
29
  end
30
30
 
31
31
  describe "/Encoding is a name" do
@@ -35,7 +35,7 @@ describe HexaPDF::Type::FontSimple do
35
35
 
36
36
  it "fails if /Encoding is an invalid name because encoding_from_font is not implemented" do
37
37
  @font[:Encoding] = :SomethingUnknown
38
- assert_raises(NotImplementedError) { @font.encoding }
38
+ assert_raises(RuntimeError) { @font.encoding }
39
39
  end
40
40
  end
41
41
 
@@ -47,12 +47,12 @@ describe HexaPDF::Type::FontSimple do
47
47
  describe "no /BaseEncoding is specified" do
48
48
  it "fails if the font is embedded because encoding_from_font is not implemented" do
49
49
  @font[:FontDescriptor][:FontFile] = 5
50
- assert_raises(NotImplementedError) { @font.encoding }
50
+ assert_raises(RuntimeError) { @font.encoding }
51
51
  end
52
52
 
53
53
  it "fails for a symbolic non-embedded font because encoding_from_font is not implemented" do
54
54
  @font[:FontDescriptor].flag(:symbolic, clear_existing: true)
55
- assert_raises(NotImplementedError) { @font.encoding }
55
+ assert_raises(RuntimeError) { @font.encoding }
56
56
  end
57
57
 
58
58
  it "returns the StandardEncoding for a non-symbolic non-embedded font" do
@@ -68,7 +68,7 @@ describe HexaPDF::Type::FontSimple do
68
68
 
69
69
  it "fails if /BaseEncoding is invalid because encoding_from_font is not implemented" do
70
70
  @font[:Encoding] = {BaseEncoding: :SomethingUnknown}
71
- assert_raises(NotImplementedError) { @font.encoding }
71
+ assert_raises(RuntimeError) { @font.encoding }
72
72
  end
73
73
 
74
74
  it "returns a difference encoding if /Differences is specified" do
@@ -104,6 +104,15 @@ describe HexaPDF::Type::ObjectStream do
104
104
  assert_equal(0, @obj.value[:First])
105
105
  assert_equal("", @obj.stream)
106
106
  end
107
+
108
+ it "doesn't allow signature dictionaries to be compressed" do
109
+ @obj.add_object(HexaPDF::Dictionary.new({Type: :Sig}, oid: 1))
110
+ @obj.add_object(HexaPDF::Dictionary.new({Type: :DocTimeStamp}, oid: 2))
111
+ @obj.add_object(HexaPDF::Dictionary.new({ByteRange: [], Contents: ''}, oid: 3))
112
+ @obj.write_objects(@revision)
113
+ assert_equal(0, @obj.value[:N])
114
+ assert_equal("", @obj.stream)
115
+ end
107
116
  end
108
117
 
109
118
  it "fails validation if gen != 0" do