hexapdf 1.9.1 → 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 +55 -0
- data/lib/hexapdf/cli/command.rb +1 -2
- data/lib/hexapdf/configuration.rb +2 -2
- data/lib/hexapdf/content/canvas.rb +2 -2
- data/lib/hexapdf/content/smart_text_extractor.rb +3 -2
- data/lib/hexapdf/dictionary_fields.rb +7 -0
- data/lib/hexapdf/digital_signature/cms_handler.rb +27 -2
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +5 -0
- 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/encryption/security_handler.rb +3 -1
- data/lib/hexapdf/layout/style.rb +2 -2
- data/lib/hexapdf/layout/text_fragment.rb +5 -1
- data/lib/hexapdf/layout/text_shaper.rb +1 -57
- data/lib/hexapdf/parser.rb +1 -2
- data/lib/hexapdf/serializer.rb +1 -1
- 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/catalog.rb +8 -1
- data/lib/hexapdf/type/document_security_store.rb +81 -13
- data/lib/hexapdf/type/font_type0.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/content/test_smart_text_extractor.rb +7 -0
- data/test/hexapdf/digital_signature/common.rb +209 -2
- data/test/hexapdf/digital_signature/test_cms_handler.rb +4 -2
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +4 -0
- data/test/hexapdf/digital_signature/test_signatures.rb +131 -0
- data/test/hexapdf/encryption/test_security_handler.rb +12 -0
- data/test/hexapdf/layout/test_style.rb +3 -1
- data/test/hexapdf/layout/test_text_fragment.rb +11 -4
- data/test/hexapdf/task/test_import_pages.rb +42 -1
- data/test/hexapdf/test_dictionary_fields.rb +8 -0
- data/test/hexapdf/test_parser.rb +25 -9
- data/test/hexapdf/test_serializer.rb +7 -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_catalog.rb +10 -0
- data/test/hexapdf/type/test_document_security_store.rb +120 -0
- data/test/test_helper.rb +7 -3
- metadata +939 -904
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
35
|
#++
|
|
36
36
|
|
|
37
|
+
require 'openssl'
|
|
37
38
|
require 'hexapdf/dictionary'
|
|
38
39
|
|
|
39
40
|
module HexaPDF
|
|
@@ -54,25 +55,92 @@ module HexaPDF
|
|
|
54
55
|
define_type :VRI
|
|
55
56
|
|
|
56
57
|
define_field :Type, type: Symbol, default: type
|
|
57
|
-
define_field :Cert, type: PDFArray
|
|
58
|
-
define_field :CRL, type: PDFArray
|
|
59
|
-
define_field :OCSP, type: PDFArray
|
|
60
|
-
define_field :TU, type: PDFDate
|
|
61
|
-
define_field :TS, type: Stream
|
|
58
|
+
define_field :Cert, type: PDFArray, version: '2.0'
|
|
59
|
+
define_field :CRL, type: PDFArray, version: '2.0'
|
|
60
|
+
define_field :OCSP, type: PDFArray, version: '2.0'
|
|
61
|
+
define_field :TU, type: PDFDate, version: '2.0'
|
|
62
|
+
define_field :TS, type: Stream, version: '2.0'
|
|
62
63
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
define_type :DSS
|
|
66
67
|
|
|
67
68
|
define_field :Type, type: Symbol, default: type
|
|
68
|
-
define_field :VRI, type: Dictionary
|
|
69
|
-
define_field :Certs, type: PDFArray
|
|
70
|
-
define_field :OCSPs, type: PDFArray
|
|
71
|
-
define_field :CRLs, type: PDFArray
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
define_field :VRI, type: Dictionary, version: '2.0'
|
|
70
|
+
define_field :Certs, type: PDFArray, version: '2.0'
|
|
71
|
+
define_field :OCSPs, type: PDFArray, version: '2.0'
|
|
72
|
+
define_field :CRLs, type: PDFArray, version: '2.0'
|
|
73
|
+
|
|
74
|
+
# Adds validation data for a single signature to the /VRI dictionary and to the various
|
|
75
|
+
# arrays, and returns the new VRI entry.
|
|
76
|
+
#
|
|
77
|
+
# +signature+::
|
|
78
|
+
# The signature for which the validation data should be added.
|
|
79
|
+
#
|
|
80
|
+
# +certs+::
|
|
81
|
+
# Array of DER-encoded certificates.
|
|
82
|
+
#
|
|
83
|
+
# +ocsps+::
|
|
84
|
+
# Array of DER-encoded OCSP responses.
|
|
85
|
+
#
|
|
86
|
+
# +crls+::
|
|
87
|
+
# Array of DER-encoded CRLs.
|
|
88
|
+
def add_vri(signature, certs: [], ocsps: [], crls: [])
|
|
89
|
+
key = OpenSSL::Digest::SHA1.hexdigest(signature.contents).upcase.to_sym
|
|
90
|
+
vri = {Type: :VRI}
|
|
91
|
+
vri[:Cert] = certs.map {|der| add_cert(der) } unless certs.empty?
|
|
92
|
+
vri[:OCSP] = ocsps.map {|der| add_ocsp(der) } unless ocsps.empty?
|
|
93
|
+
vri[:CRL] = crls.map {|der| add_crl(der) } unless crls.empty?
|
|
94
|
+
(self[:VRI] ||= {})[key] = document.add(vri)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns the VRI dictionary for the given signature if it exists or else +nil+.
|
|
98
|
+
def vri_for(signature)
|
|
99
|
+
self[:VRI] && self[:VRI][OpenSSL::Digest::SHA1.hexdigest(signature.contents).upcase.to_sym]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Adds the DER-encoded certificate to the /Certs array if not already present and returns
|
|
103
|
+
# the stream object containing it.
|
|
104
|
+
def add_cert(cert_der)
|
|
105
|
+
add_data_as_stream_to_field(cert_der, :Certs)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Adds the DER-encoded OCSP response to the /OCSPs array if not already present and returns
|
|
109
|
+
# the stream object containing it.
|
|
110
|
+
def add_ocsp(ocsp_der)
|
|
111
|
+
add_data_as_stream_to_field(ocsp_der, :OCSPs)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Adds the DER-encoded CRL to the /CRLs array if not already present and returns the stream
|
|
115
|
+
# object containing it.
|
|
116
|
+
def add_crl(crl_der)
|
|
117
|
+
add_data_as_stream_to_field(crl_der, :CRLs)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns all certificates in the /Certs array as OpenSSL::X509::Certificate objects.
|
|
121
|
+
def certificates
|
|
122
|
+
if key?(:Certs)
|
|
123
|
+
self[:Certs].length.times.map {|i| OpenSSL::X509::Certificate.new(self[:Certs][i].stream) }
|
|
124
|
+
else
|
|
125
|
+
[]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Adds the given string +data+ to the +field+ array as stream object and returns the resulting
|
|
132
|
+
# stream.
|
|
133
|
+
#
|
|
134
|
+
# If the field already contains a stream with the same data, this existing stream is returned.
|
|
135
|
+
def add_data_as_stream_to_field(data, field)
|
|
136
|
+
self[field] ||= []
|
|
137
|
+
existing_stream = self[field].find {|stream| stream.stream == data }
|
|
138
|
+
return existing_stream if existing_stream
|
|
139
|
+
|
|
140
|
+
stream = document.add({Filter: :FlateDecode}, stream: data)
|
|
141
|
+
self[field] << stream
|
|
142
|
+
stream
|
|
143
|
+
end
|
|
76
144
|
|
|
77
145
|
end
|
|
78
146
|
|
|
@@ -146,7 +146,7 @@ module HexaPDF
|
|
|
146
146
|
registry = system_info[:Registry]
|
|
147
147
|
ordering = system_info[:Ordering]
|
|
148
148
|
if (encoding.kind_of?(Symbol) && HexaPDF::Font::CMap.predefined?(encoding.to_s) &&
|
|
149
|
-
|
|
149
|
+
encoding != :'Identity-H' && encoding != :'Identity-V') ||
|
|
150
150
|
(registry == "Adobe" && ['GB1', 'CNS1', 'Japan1', 'Korea1'].include?(ordering))
|
|
151
151
|
HexaPDF::Font::CMap.for_name("#{registry}-#{ordering}-UCS2")
|
|
152
152
|
end
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -942,7 +942,7 @@ describe HexaPDF::Content::Canvas do
|
|
|
942
942
|
it "correctly serializes the form with just the width given" do
|
|
943
943
|
@canvas.image(@form, at: [1, 2], width: 50)
|
|
944
944
|
assert_operators(@page.contents, [[:save_graphics_state],
|
|
945
|
-
[:concatenate_matrix, [0.5, 0, 0, 0.5, -
|
|
945
|
+
[:concatenate_matrix, [0.5, 0, 0, 0.5, -49, -23]],
|
|
946
946
|
[:paint_xobject, [:XO1]],
|
|
947
947
|
[:restore_graphics_state]])
|
|
948
948
|
end
|
|
@@ -950,7 +950,7 @@ describe HexaPDF::Content::Canvas do
|
|
|
950
950
|
it "correctly serializes the form with just the height given" do
|
|
951
951
|
@canvas.image(@form, at: [1, 2], height: 10)
|
|
952
952
|
assert_operators(@page.contents, [[:save_graphics_state],
|
|
953
|
-
[:concatenate_matrix, [0.2, 0, 0, 0.2, -
|
|
953
|
+
[:concatenate_matrix, [0.2, 0, 0, 0.2, -19, -8]],
|
|
954
954
|
[:paint_xobject, [:XO1]],
|
|
955
955
|
[:restore_graphics_state]])
|
|
956
956
|
end
|
|
@@ -958,7 +958,7 @@ describe HexaPDF::Content::Canvas do
|
|
|
958
958
|
it "correctly serializes the form with both width and height given" do
|
|
959
959
|
@canvas.image(@form, at: [1, 2], width: 50, height: 10)
|
|
960
960
|
assert_operators(@page.contents, [[:save_graphics_state],
|
|
961
|
-
[:concatenate_matrix, [0.5, 0, 0, 0.2, -
|
|
961
|
+
[:concatenate_matrix, [0.5, 0, 0, 0.2, -49, -8]],
|
|
962
962
|
[:paint_xobject, [:XO1]],
|
|
963
963
|
[:restore_graphics_state]])
|
|
964
964
|
end
|
|
@@ -101,6 +101,13 @@ describe HexaPDF::Content::SmartTextExtractor do
|
|
|
101
101
|
['Foot', 50, 10, 66, 20]]))
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
|
|
105
|
+
it "works in case the majority of text runs have zero height" do
|
|
106
|
+
assert_equal(["Hello", "World News"].join("\n"*17), layout_runs([['World', 50, 80, 70, 80],
|
|
107
|
+
['News', 75, 80, 100, 80],
|
|
108
|
+
['Hello', 50, 100, 70, 100]]))
|
|
109
|
+
end
|
|
110
|
+
|
|
104
111
|
it "ignores outliers when calculating the normal line spacing" do
|
|
105
112
|
assert_equal("Hello\nWorld\n\n\n\nHere",
|
|
106
113
|
layout_runs([['Hello', 50, 100, 70, 110],
|
|
@@ -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
|
|
@@ -125,7 +125,9 @@ describe HexaPDF::DigitalSignature::CMSHandler do
|
|
|
125
125
|
fac.serial_number = 1
|
|
126
126
|
fac.allowed_digests = ["sha256", "sha512"]
|
|
127
127
|
res = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate, req)
|
|
128
|
-
|
|
128
|
+
der_form = res.token.to_der
|
|
129
|
+
# Convert DER to BER with indefinite length encoding as variation
|
|
130
|
+
@dict.contents = der_form[0] << "\x80".b << der_form[4..-1] << "\x00\x00".b << "\x00\x00\x00\x00".b
|
|
129
131
|
@dict.signature_type = 'ETSI.RFC3161'
|
|
130
132
|
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
131
133
|
|
|
@@ -151,7 +153,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
|
|
|
151
153
|
key: CERTIFICATES.signer_key, timestamp_handler: tsh,
|
|
152
154
|
certificates: [CERTIFICATES.ca_certificate]
|
|
153
155
|
)
|
|
154
|
-
@dict.contents = cms.to_der
|
|
156
|
+
@dict.contents = cms.to_der << "\x00\x00\x00\x00"
|
|
155
157
|
@dict.signed_data = @data
|
|
156
158
|
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
157
159
|
end
|
|
@@ -28,6 +28,10 @@ describe HexaPDF::DigitalSignature::PKCS1Handler do
|
|
|
28
28
|
assert_equal(CERTIFICATES.signer_certificate, @handler.signer_certificate)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
it "returns nil for the embedded timestamp signature" do
|
|
32
|
+
assert_nil(@handler.embedded_tsa_signature)
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
describe "verify" do
|
|
32
36
|
before do
|
|
33
37
|
@store = OpenSSL::X509::Store.new
|
|
@@ -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
|
|
@@ -302,6 +302,18 @@ describe HexaPDF::Encryption::SecurityHandler do
|
|
|
302
302
|
assert_equal('string', obj.stream)
|
|
303
303
|
end
|
|
304
304
|
|
|
305
|
+
it "handles decryption of should-be stream objects without actual streams" do
|
|
306
|
+
doc = HexaPDF::Document.new
|
|
307
|
+
obj = doc.add({}) # Create XObject as Dictionary and not as Stream
|
|
308
|
+
obj[:Type] = :XObject
|
|
309
|
+
obj[:Subtype] = :Form
|
|
310
|
+
doc.catalog[:Test] = obj
|
|
311
|
+
doc.encrypt
|
|
312
|
+
doc = HexaPDF::Document.new(io: StringIO.new(doc.write_to_string))
|
|
313
|
+
assert_kind_of(HexaPDF::Stream, doc.catalog[:Test])
|
|
314
|
+
assert_equal('', doc.catalog[:Test].raw_stream)
|
|
315
|
+
end
|
|
316
|
+
|
|
305
317
|
it "doesn't decrypt a document's Encrypt dictionaries" do
|
|
306
318
|
@document = HexaPDF::Document.new
|
|
307
319
|
@document.trailer[:Encrypt] = @document.add({Key: "Something"})
|
|
@@ -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
|
|