hexapdf 1.10.0 → 1.11.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.
@@ -5,6 +5,9 @@ module HexaPDF
5
5
 
6
6
  class Certificates
7
7
 
8
+ # Port for the combined OCSP responder / CRL distribution point test server.
9
+ OCSP_CRL_PORT = 34568
10
+
8
11
  def ca_key
9
12
  @ca_key ||= OpenSSL::PKey::RSA.new(2048)
10
13
  end
@@ -43,6 +46,106 @@ module HexaPDF
43
46
  end
44
47
  end
45
48
 
49
+ def signer_certificate_with_ocsp
50
+ @signer_certificate_with_ocsp ||=
51
+ begin
52
+ cert = create_cert(name: '/CN=RSA signer with OCSP', serial: 6,
53
+ public_key: signer_key, issuer: ca_certificate)
54
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
55
+ ocsp_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/ocsp")
56
+ cert
57
+ end
58
+ end
59
+
60
+ def signer_certificate_with_crl
61
+ @signer_certificate_with_crl ||=
62
+ begin
63
+ cert = create_cert(name: '/CN=RSA signer with CRL', serial: 7,
64
+ public_key: signer_key, issuer: ca_certificate)
65
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
66
+ crl_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/crl")
67
+ cert
68
+ end
69
+ end
70
+
71
+ def signer_certificate_with_unsuccessful_ocsp
72
+ @signer_certificate_with_unsuccessful_ocsp ||=
73
+ begin
74
+ cert = create_cert(name: '/CN=RSA signer with unsucessful OCSP', serial: 11,
75
+ public_key: signer_key, issuer: ca_certificate)
76
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
77
+ ocsp_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/ocsp-unsuccessful")
78
+ cert
79
+ end
80
+ end
81
+
82
+ def signer_certificate_with_revoked_ocsp
83
+ @signer_certificate_with_revoked_ocsp ||=
84
+ begin
85
+ cert = create_cert(name: '/CN=RSA signer with revoked OCSP', serial: 12,
86
+ public_key: signer_key, issuer: ca_certificate)
87
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
88
+ ocsp_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/ocsp-revoked")
89
+ cert
90
+ end
91
+ end
92
+
93
+ def signer_certificate_with_http_error_ocsp
94
+ @signer_certificate_with_http_error_ocsp ||=
95
+ begin
96
+ cert = create_cert(name: '/CN=RSA signer with HTTP error OCSP', serial: 13,
97
+ public_key: signer_key, issuer: ca_certificate)
98
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
99
+ ocsp_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/ocsp-http-error")
100
+ cert
101
+ end
102
+ end
103
+
104
+ def signer_certificate_with_bad_ocsp
105
+ @signer_certificate_with_bad_ocsp ||=
106
+ begin
107
+ cert = create_cert(name: '/CN=RSA signer with bad OCSP', serial: 8,
108
+ public_key: signer_key, issuer: ca_certificate)
109
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
110
+ ocsp_url: "http://127.0.0.1:#{OCSP_CRL_PORT + 1}/ocsp")
111
+ cert
112
+ end
113
+ end
114
+
115
+ def signer_certificate_with_revoked_crl
116
+ @signer_certificate_with_revoked_crl ||=
117
+ begin
118
+ cert = create_cert(name: '/CN=RSA signer with revoked CRL', serial: 14,
119
+ public_key: signer_key, issuer: ca_certificate)
120
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
121
+ crl_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/crl-revoked")
122
+ cert
123
+ end
124
+ end
125
+
126
+ def signer_certificate_with_http_error_crl
127
+ @signer_certificate_with_http_error_crl ||=
128
+ begin
129
+ cert = create_cert(name: '/CN=RSA signer with HTTP error CRL', serial: 15,
130
+ public_key: signer_key, issuer: ca_certificate)
131
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
132
+ crl_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/crl-http-error")
133
+ cert
134
+ end
135
+ end
136
+
137
+ def signer_certificate_with_bad_crl
138
+ @signer_certificate_with_bad_crl ||=
139
+ begin
140
+ cert = create_cert(name: '/CN=RSA signer with bad CRL', serial: 9,
141
+ public_key: signer_key, issuer: ca_certificate)
142
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
143
+ crl_url: "http://127.0.0.1:#{OCSP_CRL_PORT + 1}/crl")
144
+ cert
145
+ end
146
+ end
147
+
148
+
46
149
  def dsa_signer_key
47
150
  @dsa_signer_key ||= OpenSSL::PKey::DSA.new(2048)
48
151
  end
@@ -77,11 +180,36 @@ module HexaPDF
77
180
  cert = create_cert(name: '/CN=timestamp', serial: 5,
78
181
  public_key: signer_key, issuer: ca_certificate)
79
182
  add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
80
- extended_key_usage: 'timeStamping')
183
+ extended_key_usage: 'timeStamping',
184
+ crl_url: "http://127.0.0.1:#{OCSP_CRL_PORT}/crl")
185
+ cert
186
+ end
187
+ end
188
+
189
+ def ocsp_signing_certificate
190
+ @ocsp_signing_certificate ||=
191
+ begin
192
+ cert = create_cert(name: '/CN=OCSP', serial: 10,
193
+ public_key: signer_key, issuer: ca_certificate)
194
+ add_extensions(cert, ca_certificate, ca_key, key_usage: 'digitalSignature',
195
+ extended_key_usage: 'OCSPSigning')
81
196
  cert
82
197
  end
83
198
  end
84
199
 
200
+ def crl
201
+ @crl ||=
202
+ begin
203
+ crl = OpenSSL::X509::CRL.new
204
+ crl.version = 1
205
+ crl.issuer = ca_certificate.subject
206
+ crl.last_update = Time.now - 60
207
+ crl.next_update = Time.now + 86400
208
+ crl.sign(ca_key, OpenSSL::Digest.new('SHA256'))
209
+ crl
210
+ end
211
+ end
212
+
85
213
  def create_cert(name:, serial:, public_key:, issuer: nil)
86
214
  name = OpenSSL::X509::Name.parse(name)
87
215
  cert = OpenSSL::X509::Certificate.new
@@ -96,7 +224,7 @@ module HexaPDF
96
224
  end
97
225
 
98
226
  def add_extensions(subject_cert, issuer_cert, signing_key, is_ca: false, key_usage: nil,
99
- extended_key_usage: nil)
227
+ extended_key_usage: nil, ocsp_url: nil, crl_url: nil)
100
228
  extension_factory = OpenSSL::X509::ExtensionFactory.new
101
229
  extension_factory.subject_certificate = subject_cert
102
230
  extension_factory.issuer_certificate = issuer_cert
@@ -113,6 +241,16 @@ module HexaPDF
113
241
  subject_cert.add_extension(extension_factory.create_extension('extendedKeyUsage',
114
242
  extended_key_usage, true))
115
243
  end
244
+ if ocsp_url
245
+ subject_cert.add_extension(
246
+ extension_factory.create_extension('authorityInfoAccess', "OCSP;URI:#{ocsp_url}")
247
+ )
248
+ end
249
+ if crl_url
250
+ subject_cert.add_extension(
251
+ extension_factory.create_extension('crlDistributionPoints', "URI:#{crl_url}")
252
+ )
253
+ end
116
254
  subject_cert.sign(signing_key, OpenSSL::Digest.new('SHA1'))
117
255
  end
118
256
  private :add_extensions
@@ -148,6 +286,75 @@ module HexaPDF
148
286
  Thread.new { @tsa_server.start }
149
287
  end
150
288
 
289
+ def start_ocsp_crl_server
290
+ return if defined?(@ocsp_crl_server)
291
+ require 'webrick'
292
+ @ocsp_crl_server = WEBrick::HTTPServer.new(Port: OCSP_CRL_PORT, BindAddress: '127.0.0.1',
293
+ Logger: WEBrick::Log.new(StringIO.new), AccessLog: [])
294
+ @ocsp_crl_server.mount_proc('/ocsp') do |request, response|
295
+ ocsp_req = OpenSSL::OCSP::Request.new(request.body)
296
+ basic_resp = OpenSSL::OCSP::BasicResponse.new
297
+ ocsp_req.certid.each do |certid|
298
+ basic_resp.add_status(certid, OpenSSL::OCSP::V_CERTSTATUS_GOOD,
299
+ OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED,
300
+ nil, Time.now - 60, Time.now + 86400, nil)
301
+ end
302
+ basic_resp.sign(CERTIFICATES.ocsp_signing_certificate, CERTIFICATES.signer_key,
303
+ [CERTIFICATES.ca_certificate])
304
+ response.body = OpenSSL::OCSP::Response.create(
305
+ OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, basic_resp
306
+ ).to_der
307
+ response['Content-Type'] = 'application/ocsp-response'
308
+ end
309
+
310
+ @ocsp_crl_server.mount_proc('/ocsp-unsuccessful') do |request, response|
311
+ basic_resp = OpenSSL::OCSP::BasicResponse.new
312
+ basic_resp.sign(CERTIFICATES.ocsp_signing_certificate, CERTIFICATES.signer_key,
313
+ [CERTIFICATES.ca_certificate])
314
+ response.body = OpenSSL::OCSP::Response.create(
315
+ OpenSSL::OCSP::RESPONSE_STATUS_INTERNALERROR, basic_resp
316
+ ).to_der
317
+ response['Content-Type'] = 'application/ocsp-response'
318
+ end
319
+
320
+ @ocsp_crl_server.mount_proc('/ocsp-revoked') do |request, response|
321
+ ocsp_req = OpenSSL::OCSP::Request.new(request.body)
322
+ basic_resp = OpenSSL::OCSP::BasicResponse.new
323
+ ocsp_req.certid.each do |certid|
324
+ basic_resp.add_status(certid, OpenSSL::OCSP::V_CERTSTATUS_REVOKED,
325
+ OpenSSL::OCSP::REVOKED_STATUS_UNSPECIFIED,
326
+ Time.now - 100, Time.now - 60, Time.now + 86400, nil)
327
+ end
328
+ basic_resp.sign(CERTIFICATES.ocsp_signing_certificate, CERTIFICATES.signer_key,
329
+ [CERTIFICATES.ca_certificate])
330
+ response.body = OpenSSL::OCSP::Response.create(
331
+ OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, basic_resp
332
+ ).to_der
333
+ response['Content-Type'] = 'application/ocsp-response'
334
+ end
335
+
336
+ @ocsp_crl_server.mount_proc('/crl') do |_request, response|
337
+ response.body = CERTIFICATES.crl.to_der
338
+ response['Content-Type'] = 'application/pkix-crl'
339
+ end
340
+
341
+ @ocsp_crl_server.mount_proc('/crl-revoked') do |_request, response|
342
+ crl = OpenSSL::X509::CRL.new
343
+ crl.version = 1
344
+ crl.issuer = ca_certificate.subject
345
+ crl.last_update = Time.now - 60
346
+ crl.next_update = Time.now + 86400
347
+ revoked = OpenSSL::X509::Revoked.new
348
+ revoked.serial = signer_certificate_with_revoked_crl.serial
349
+ revoked.time = Time.now - 100
350
+ crl.add_revoked(revoked)
351
+ crl.sign(ca_key, OpenSSL::Digest.new('SHA256'))
352
+ response.body = crl.to_der
353
+ response['Content-Type'] = 'application/pkix-crl'
354
+ end
355
+ Thread.new { @ocsp_crl_server.start }
356
+ end
357
+
151
358
  end
152
359
 
153
360
  end
@@ -161,4 +161,135 @@ describe HexaPDF::DigitalSignature::Signatures do
161
161
  assert(signed_doc.signatures.first.verify)
162
162
  end
163
163
  end
164
+
165
+ describe "add_ltv_information" do
166
+ before do
167
+ CERTIFICATES.start_ocsp_crl_server
168
+ @doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
169
+ @io = StringIO.new(''.b)
170
+ end
171
+
172
+ # Signs @doc and returns a new HexaPDF::Document instance for the signed document.
173
+ def sign_with(certificate, key: CERTIFICATES.signer_key, certificate_chain: [CERTIFICATES.ca_certificate])
174
+ io = StringIO.new(''.b)
175
+ @doc.sign(io, certificate: certificate, key: key, certificate_chain: certificate_chain)
176
+ HexaPDF::Document.new(io: io)
177
+ end
178
+
179
+ it "adds all certificates to the DSS" do
180
+ doc = sign_with(CERTIFICATES.signer_certificate_with_ocsp)
181
+ doc.signatures.add_ltv_information
182
+ dss = doc.catalog.dss
183
+ assert_equal([CERTIFICATES.signer_certificate_with_ocsp, CERTIFICATES.ca_certificate],
184
+ dss.certificates)
185
+ end
186
+
187
+ it "fails if one of the certificates has no OCSP or CRL URL" do
188
+ doc = sign_with(CERTIFICATES.signer_certificate)
189
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
190
+ assert_match(/No OCSP and CRL/, e.message)
191
+ end
192
+
193
+ it "adds a VRI entry for all non-root certificates" do
194
+ doc = sign_with(CERTIFICATES.signer_certificate_with_ocsp)
195
+ doc.signatures.add_ltv_information
196
+ dss = doc.catalog.dss
197
+ assert_equal(1, dss[:VRI].value.length)
198
+ assert(dss.vri_for(doc.signatures.first))
199
+ end
200
+
201
+ it "embeds OCSP validation data for a certificate that has an AIA OCSP URL" do
202
+ doc = sign_with(CERTIFICATES.signer_certificate_with_ocsp)
203
+ doc.signatures.add_ltv_information
204
+ dss = doc.catalog.dss
205
+ vri = dss.vri_for(doc.signatures.first)
206
+ assert_equal(dss[:OCSPs].value, vri[:OCSP].value)
207
+ assert_equal(dss[:Certs].value, vri[:Cert].value)
208
+ assert_nil(dss[:CRLs])
209
+ assert_nil(vri[:CRL])
210
+ end
211
+
212
+ it "handles a connection failure to the OCSP server" do
213
+ doc = sign_with(CERTIFICATES.signer_certificate_with_bad_ocsp)
214
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
215
+ assert_match(/No OCSP and CRL/, e.message)
216
+ end
217
+
218
+ it "handles unsuccessful OCSP validation" do
219
+ doc = sign_with(CERTIFICATES.signer_certificate_with_unsuccessful_ocsp)
220
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
221
+ assert_match(/No OCSP and CRL/, e.message)
222
+ end
223
+
224
+ it "handles successful OCSP validation but with a non-good certificate status" do
225
+ doc = sign_with(CERTIFICATES.signer_certificate_with_revoked_ocsp)
226
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
227
+ assert_match(/OCSP response.*not valid/, e.message)
228
+ end
229
+
230
+ it "handles non-OK HTTP response codes when fetching the OCSP response" do
231
+ doc = sign_with(CERTIFICATES.signer_certificate_with_http_error_ocsp)
232
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
233
+ assert_match(/No OCSP and CRL/, e.message)
234
+ end
235
+
236
+ it "doesn't use the OCSP information if the issuer certificate is not embedded" do
237
+ doc = sign_with(CERTIFICATES.signer_certificate_with_ocsp, certificate_chain: [])
238
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
239
+ assert_match(/No OCSP and CRL/, e.message)
240
+ end
241
+
242
+ it "embeds CRL validation data when no OCSP URL is present in the certificate" do
243
+ doc = sign_with(CERTIFICATES.signer_certificate_with_crl)
244
+ doc.signatures.add_ltv_information
245
+ dss = doc.catalog.dss
246
+ vri = dss.vri_for(doc.signatures.first)
247
+ assert_equal(dss[:CRLs].value, vri[:CRL].value)
248
+ assert_equal(dss[:Certs].value, vri[:Cert].value)
249
+ assert_nil(dss[:OCSPs])
250
+ assert_nil(vri[:OCSP])
251
+ end
252
+
253
+ it "handles a connection failure to the CRL server" do
254
+ doc = sign_with(CERTIFICATES.signer_certificate_with_bad_crl)
255
+ # CRL URL available but bad endpoint
256
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
257
+ assert_match(/No OCSP and CRL/, e.message)
258
+ end
259
+
260
+ it "handles a certificate that is revoked via CRL" do
261
+ doc = sign_with(CERTIFICATES.signer_certificate_with_revoked_crl)
262
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
263
+ assert_match(/CRL.*certificate is revoked/, e.message)
264
+ end
265
+
266
+ it "handles non-OK HTTP response codes when fetching the CRL response" do
267
+ doc = sign_with(CERTIFICATES.signer_certificate_with_http_error_crl)
268
+ e = assert_raises(HexaPDF::Error) { doc.signatures.add_ltv_information }
269
+ assert_match(/No OCSP and CRL/, e.message)
270
+ end
271
+
272
+ it "embeds validation data for an embedded timestamp signature" do
273
+ CERTIFICATES.start_tsa_server
274
+ ts_handler = @doc.signatures.signing_handler(name: :timestamp, signature_size: 20_000,
275
+ tsa_url: "http://127.0.0.1:34567")
276
+ io = StringIO.new(''.b)
277
+ @doc.sign(io, certificate: CERTIFICATES.signer_certificate_with_ocsp,
278
+ key: CERTIFICATES.signer_key, certificate_chain: [CERTIFICATES.ca_certificate],
279
+ timestamp_handler: ts_handler)
280
+ doc = HexaPDF::Document.new(io: io)
281
+ doc.signatures.add_ltv_information
282
+ dss = doc.catalog.dss
283
+ vri = dss.vri_for(doc.signatures.first)
284
+ assert_equal(3, vri[:Cert].value.size)
285
+ end
286
+
287
+ it "creates VRI entries for all signatures" do
288
+ @doc = sign_with(CERTIFICATES.signer_certificate_with_ocsp)
289
+ doc = sign_with(CERTIFICATES.signer_certificate_with_crl)
290
+ doc.signatures.add_ltv_information
291
+ dss = doc.catalog.dss
292
+ assert_equal(2, dss[:VRI].value.size)
293
+ end
294
+ end
164
295
  end
@@ -616,7 +616,7 @@ describe HexaPDF::Layout::Style::LinkLayer do
616
616
  annot = call_link(dest: true)
617
617
  assert_equal(:Link, annot[:Subtype])
618
618
  assert_equal([10, 10, 25, 20], annot[:Rect].value)
619
- assert_equal([10, 10, 25, 10, 25, 20, 10, 20], annot[:QuadPoints].value)
619
+ assert_equal([10, 20, 25, 20, 10, 10, 25, 10], annot[:QuadPoints].value)
620
620
  end
621
621
 
622
622
  it "removes the border by default" do
@@ -852,8 +852,10 @@ describe HexaPDF::Layout::Style do
852
852
  end
853
853
 
854
854
  it "has several dynamically generated properties with default values that take blocks" do
855
+ refute(@style.text_segmentation_algorithm?)
855
856
  assert_equal(HexaPDF::Layout::TextLayouter::SimpleTextSegmentation,
856
857
  @style.text_segmentation_algorithm)
858
+ refute(@style.text_line_wrapping_algorithm?)
857
859
  assert_equal(HexaPDF::Layout::TextLayouter::SimpleLineWrapping,
858
860
  @style.text_line_wrapping_algorithm)
859
861
 
@@ -99,9 +99,10 @@ describe HexaPDF::Task::ImportPages do
99
99
  canvas = @doc.pages[0].canvas
100
100
  form = canvas.form
101
101
  form[:OC] = @ocg1
102
+ form.canvas.optional_content(@ocmd)
102
103
  canvas.xobject(form, at: [0, 0])
103
104
  @target.task(:import_pages, source: @doc)
104
- assert_equal(['OCG'], @target.optional_content.ocgs.map(&:name))
105
+ assert_equal(['OCG', 'OCMD'], @target.optional_content.ocgs.map(&:name))
105
106
  end
106
107
 
107
108
  it "preserves OCGs/OCMDs associated with annotations" do
@@ -123,4 +124,44 @@ describe HexaPDF::Task::ImportPages do
123
124
  assert_equal(['OCG'], rb_groups[0].map(&:name))
124
125
  end
125
126
  end
127
+
128
+ describe "resize_to argument" do
129
+ before do
130
+ @doc.annotations.create_line(@pages[0], start_point: [10, 10], end_point: [100, 100]).
131
+ regenerate_appearance
132
+ end
133
+
134
+ it "works when given a symbolic page size" do
135
+ @target.task(:import_pages, source: @doc, resize_to: :A4)
136
+ assert_equal(2, @doc.each.select {|obj| obj.type == :Page }.size)
137
+ assert_operators(@target.pages[0].contents, [[:paint_xobject, [:XO1]]])
138
+ assert_operators(@target.pages[1].contents, [[:paint_xobject, [:XO1]]])
139
+ end
140
+
141
+ it "works when given a media box directly" do
142
+ @target.task(:import_pages, source: @doc, pages: 0, resize_to: [5, 5, 100, 100])
143
+ assert_equal(1, @target.each.select {|obj| obj.type == :Page }.size)
144
+ assert_operators(@target.pages[0].contents,
145
+ [[:concatenate_matrix, [1, 0, 0, 1, 5, 5]],
146
+ [:save_graphics_state],
147
+ [:concatenate_matrix, [0.15959, 0, 0, 0.112841, 0.0, 0.0]],
148
+ [:paint_xobject, [:XO1]],
149
+ [:restore_graphics_state]])
150
+ end
151
+
152
+ it "flattens the annotations" do
153
+ @target.task(:import_pages, source: @doc, pages: 0, resize_to: [5, 5, 100, 100])
154
+ assert_operators(@target.pages[0].resources.xobject(:XO1).contents,
155
+ [[:save_graphics_state],
156
+ [:restore_graphics_state],
157
+ [:save_graphics_state],
158
+ [:restore_graphics_state],
159
+ [:save_graphics_state],
160
+ [:save_graphics_state],
161
+ [:concatenate_matrix, [1.0, 0, 0, 1.0, 0.0, 0.0]],
162
+ [:paint_xobject, [:XO1]],
163
+ [:restore_graphics_state],
164
+ [:restore_graphics_state]])
165
+ end
166
+ end
126
167
  end
@@ -928,7 +928,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
928
928
 
929
929
  it "creates the /N appearance stream" do
930
930
  @field[:I] = [1, 2]
931
- @field[:V] = ['b', 'c']
931
+ @field[:V] = ['b', 'c', 'd']
932
932
  @field.set_default_appearance_string(font_size: 12, font_color: "red")
933
933
  @generator.create_appearances
934
934
  assert_operators(@widget[:AP][:N].stream,
@@ -103,6 +103,11 @@ describe HexaPDF::Type::AcroForm::ChoiceField do
103
103
  @field.option_items = [1, 2, 3, 4]
104
104
  @field.list_box_top_index = 2
105
105
  assert_equal(2, @field.list_box_top_index)
106
+
107
+ @field[:TI] = 100
108
+ assert_equal(0, @field.list_box_top_index)
109
+ @field[:TI] = "other"
110
+ assert_equal(0, @field.list_box_top_index)
106
111
  end
107
112
 
108
113
  it "fails if mulitple values are provided but the list box is not a multi-select" do
@@ -113,7 +113,9 @@ describe HexaPDF::Type::AcroForm::Field do
113
113
 
114
114
  it "can check whether a widget is embedded in the field" do
115
115
  refute(@field.embedded_widget?)
116
- @field[:Subtype] = :Wdiget
116
+ @field[:Subtype] = :Link
117
+ refute(@field.embedded_widget?)
118
+ @field[:Subtype] = :Widget
117
119
  assert(@field.embedded_widget?)
118
120
  end
119
121
 
@@ -1,6 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  require 'test_helper'
4
+ require_relative '../digital_signature/common'
4
5
  require 'hexapdf/document'
5
6
  require 'hexapdf/type/document_security_store'
6
7
 
@@ -87,4 +88,33 @@ describe HexaPDF::Type::DocumentSecurityStore do
87
88
  refute(vri.key?(:CRL))
88
89
  end
89
90
  end
91
+
92
+ describe "vri_for" do
93
+ before do
94
+ @signature = @doc.add({Type: :Sig, Contents: 'signature bytes'})
95
+ end
96
+
97
+ it "returns nil if no /VRI entry exists" do
98
+ assert_nil(@dss.vri_for(@signature))
99
+ end
100
+
101
+ it "returns the VRI entry for the signature if it exists" do
102
+ @dss[:VRI] = {}
103
+ assert_nil(@dss.vri_for(@signature))
104
+ @dss.add_vri(@signature)
105
+ assert_equal(@dss[:VRI].value.first[1], @dss.vri_for(@signature))
106
+ end
107
+ end
108
+
109
+ describe "certificates" do
110
+ it "returns an empty array if no /Certs entry exists" do
111
+ assert_equal([], @dss.certificates)
112
+ end
113
+
114
+ it "returns an array with certificate instances" do
115
+ cert = CERTIFICATES.signer_certificate
116
+ @dss[:Certs] = [@doc.add({}, stream: cert.to_der)]
117
+ assert_equal([cert], @dss.certificates)
118
+ end
119
+ end
90
120
  end
data/test/test_helper.rb CHANGED
@@ -3,9 +3,13 @@
3
3
  begin
4
4
  require 'simplecov'
5
5
  SimpleCov.start do
6
- minimum_coverage line: 100 unless ENV['NO_SIMPLECOV']
7
- add_filter '/test/'
8
- add_filter '/fast_arc4.rb'
6
+ enable_coverage(:method)
7
+ unless ENV['NO_SIMPLECOV']
8
+ coverage(:line) { minimum 100 }
9
+ coverage(:method) { minimum 100 }
10
+ end
11
+ skip '/test/'
12
+ skip '/fast_arc4.rb'
9
13
  end
10
14
  rescue LoadError
11
15
  end