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.
- 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
|