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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/hexapdf/cli/command.rb +1 -2
- data/lib/hexapdf/content/canvas.rb +2 -2
- data/lib/hexapdf/digital_signature/signatures.rb +105 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +0 -2
- data/lib/hexapdf/document.rb +2 -2
- data/lib/hexapdf/layout/style.rb +2 -2
- data/lib/hexapdf/layout/text_shaper.rb +1 -57
- data/lib/hexapdf/task/import_pages.rb +41 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
- data/lib/hexapdf/type/acro_form/choice_field.rb +3 -1
- data/lib/hexapdf/type/acro_form/field.rb +1 -1
- data/lib/hexapdf/type/document_security_store.rb +14 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/digital_signature/common.rb +209 -2
- data/test/hexapdf/digital_signature/test_signatures.rb +131 -0
- data/test/hexapdf/layout/test_style.rb +3 -1
- data/test/hexapdf/task/test_import_pages.rb +42 -1
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_field.rb +3 -1
- data/test/hexapdf/type/test_document_security_store.rb +30 -0
- data/test/test_helper.rb +7 -3
- metadata +939 -905
|
@@ -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,
|
|
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] = :
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|