hexapdf 0.19.2 → 0.20.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/data/hexapdf/cert/demo_cert.rb +22 -0
- data/data/hexapdf/cert/root-ca.crt +119 -0
- data/data/hexapdf/cert/signing.crt +125 -0
- data/data/hexapdf/cert/signing.key +52 -0
- data/data/hexapdf/cert/sub-ca.crt +125 -0
- data/data/hexapdf/encoding/glyphlist.txt +4283 -4282
- data/data/hexapdf/encoding/zapfdingbats.txt +203 -202
- data/lib/hexapdf/cli/info.rb +21 -1
- data/lib/hexapdf/configuration.rb +26 -0
- data/lib/hexapdf/content/processor.rb +1 -1
- data/lib/hexapdf/document/signatures.rb +327 -0
- data/lib/hexapdf/document.rb +26 -0
- data/lib/hexapdf/font/encoding/glyph_list.rb +5 -6
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/object.rb +5 -3
- data/lib/hexapdf/parser.rb +14 -9
- data/lib/hexapdf/rectangle.rb +0 -6
- data/lib/hexapdf/revision.rb +13 -6
- data/lib/hexapdf/task/dereference.rb +12 -4
- data/lib/hexapdf/task/optimize.rb +3 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +2 -4
- data/lib/hexapdf/type/acro_form/field.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +9 -1
- data/lib/hexapdf/type/annotation.rb +36 -3
- data/lib/hexapdf/type/font_simple.rb +1 -1
- data/lib/hexapdf/type/object_stream.rb +3 -1
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +121 -0
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +95 -0
- data/lib/hexapdf/type/signature/handler.rb +140 -0
- data/lib/hexapdf/type/signature/verification_result.rb +92 -0
- data/lib/hexapdf/type/signature.rb +236 -0
- data/lib/hexapdf/type.rb +1 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +16 -8
- data/test/hexapdf/content/test_processor.rb +1 -1
- data/test/hexapdf/document/test_signatures.rb +225 -0
- data/test/hexapdf/task/test_optimize.rb +4 -1
- data/test/hexapdf/test_document.rb +28 -0
- data/test/hexapdf/test_object.rb +7 -2
- data/test/hexapdf/test_parser.rb +12 -0
- data/test/hexapdf/test_rectangle.rb +0 -7
- data/test/hexapdf/test_revision.rb +44 -14
- data/test/hexapdf/test_writer.rb +4 -3
- data/test/hexapdf/type/acro_form/test_field.rb +11 -1
- data/test/hexapdf/type/acro_form/test_form.rb +5 -0
- data/test/hexapdf/type/signature/common.rb +71 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +99 -0
- data/test/hexapdf/type/signature/test_adbe_x509_rsa_sha1.rb +66 -0
- data/test/hexapdf/type/signature/test_handler.rb +102 -0
- data/test/hexapdf/type/signature/test_verification_result.rb +47 -0
- data/test/hexapdf/type/test_annotation.rb +40 -2
- data/test/hexapdf/type/test_font_simple.rb +5 -5
- data/test/hexapdf/type/test_object_stream.rb +9 -0
- data/test/hexapdf/type/test_signature.rb +131 -0
- metadata +21 -3
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
4
|
require 'hexapdf/revision'
|
5
|
-
require 'hexapdf/
|
5
|
+
require 'hexapdf/dictionary'
|
6
6
|
require 'hexapdf/reference'
|
7
7
|
require 'hexapdf/xref_section'
|
8
|
+
require 'hexapdf/type/catalog'
|
8
9
|
require 'stringio'
|
9
10
|
|
10
11
|
describe HexaPDF::Revision do
|
@@ -12,6 +13,10 @@ describe HexaPDF::Revision do
|
|
12
13
|
@xref_section = HexaPDF::XRefSection.new
|
13
14
|
@xref_section.add_in_use_entry(2, 0, 5000)
|
14
15
|
@xref_section.add_free_entry(3, 0)
|
16
|
+
@xref_section.add_in_use_entry(4, 0, 1000)
|
17
|
+
@xref_section.add_in_use_entry(5, 0, 1000)
|
18
|
+
@xref_section.add_in_use_entry(6, 0, 5000)
|
19
|
+
@xref_section.add_in_use_entry(7, 0, 5000)
|
15
20
|
@obj = HexaPDF::Object.new(:val, oid: 1, gen: 0)
|
16
21
|
@ref = HexaPDF::Reference.new(1, 0)
|
17
22
|
|
@@ -19,7 +24,13 @@ describe HexaPDF::Revision do
|
|
19
24
|
if entry.type == :free
|
20
25
|
HexaPDF::Object.new(nil, oid: entry.oid, gen: entry.gen)
|
21
26
|
else
|
22
|
-
|
27
|
+
case entry.oid
|
28
|
+
when 4 then HexaPDF::Dictionary.new({Type: :XRef}, oid: entry.oid, gen: entry.gen)
|
29
|
+
when 5 then HexaPDF::Dictionary.new({Type: :ObjStm}, oid: entry.oid, gen: entry.gen)
|
30
|
+
when 7 then HexaPDF::Type::Catalog.new({Type: :Catalog}, oid: entry.oid, gen: entry.gen,
|
31
|
+
document: self)
|
32
|
+
else HexaPDF::Object.new(:Test, oid: entry.oid, gen: entry.gen)
|
33
|
+
end
|
23
34
|
end
|
24
35
|
end
|
25
36
|
@rev = HexaPDF::Revision.new({}, xref_section: @xref_section, loader: @loader)
|
@@ -36,10 +47,10 @@ describe HexaPDF::Revision do
|
|
36
47
|
end
|
37
48
|
|
38
49
|
it "returns the next free object number" do
|
39
|
-
assert_equal(
|
40
|
-
@obj.oid =
|
50
|
+
assert_equal(8, @rev.next_free_oid)
|
51
|
+
@obj.oid = 8
|
41
52
|
@rev.add(@obj)
|
42
|
-
assert_equal(
|
53
|
+
assert_equal(9, @rev.next_free_oid)
|
43
54
|
end
|
44
55
|
|
45
56
|
describe "add" do
|
@@ -151,15 +162,14 @@ describe HexaPDF::Revision do
|
|
151
162
|
assert(@rev.object(@ref).null?)
|
152
163
|
assert(@obj.null?)
|
153
164
|
assert_raises(HexaPDF::Error) { @obj.document }
|
165
|
+
assert_same(@obj.data, @rev.object(@ref).data)
|
154
166
|
end
|
155
167
|
end
|
156
168
|
|
157
169
|
describe "object iteration" do
|
158
170
|
it "iterates over all objects via each" do
|
159
171
|
@rev.add(@obj)
|
160
|
-
|
161
|
-
obj3 = @rev.object(3)
|
162
|
-
assert_equal([@obj, obj2, obj3], @rev.each.to_a)
|
172
|
+
assert_equal([@obj, *(2..7).map {|i| @rev.object(i) }], @rev.each.to_a)
|
163
173
|
end
|
164
174
|
|
165
175
|
it "iterates only over loaded objects" do
|
@@ -178,11 +188,31 @@ describe HexaPDF::Revision do
|
|
178
188
|
refute(rev.object?(@ref))
|
179
189
|
end
|
180
190
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
191
|
+
describe "each_modified_object" do
|
192
|
+
it "returns modified objects" do
|
193
|
+
obj = @rev.object(2)
|
194
|
+
obj.value = :Other
|
195
|
+
@rev.add(@obj)
|
196
|
+
deleted = @rev.object(6)
|
197
|
+
@rev.delete(6)
|
198
|
+
assert_equal([obj, @obj, deleted], @rev.each_modified_object.to_a)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "ignores object and xref streams that were deleted" do
|
202
|
+
@rev.delete(4)
|
203
|
+
@rev.delete(5)
|
204
|
+
assert_equal([], @rev.each_modified_object.to_a)
|
205
|
+
end
|
206
|
+
|
207
|
+
it "doesn't return non-modified objects" do
|
208
|
+
@rev.object(2)
|
209
|
+
assert_equal([], @rev.each_modified_object.to_a)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "doesn't return objects that have modified values just because of reading" do
|
213
|
+
obj = @rev.object(7)
|
214
|
+
obj.delete(:Type)
|
215
|
+
assert_equal([], @rev.each_modified_object.to_a)
|
216
|
+
end
|
187
217
|
end
|
188
218
|
end
|
data/test/hexapdf/test_writer.rb
CHANGED
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
40
40
|
219
|
41
41
|
%%EOF
|
42
42
|
3 0 obj
|
43
|
-
<</Producer(HexaPDF version 0.
|
43
|
+
<</Producer(HexaPDF version 0.20.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.
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|