hexapdf 0.28.0 → 0.30.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 +59 -10
- data/examples/024-digital-signatures.rb +23 -0
- data/lib/hexapdf/configuration.rb +12 -12
- data/lib/hexapdf/dictionary_fields.rb +13 -4
- data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
- data/lib/hexapdf/digital_signature/handler.rb +138 -0
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
- data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
- data/lib/hexapdf/digital_signature/signatures.rb +210 -0
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
- data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
- data/lib/hexapdf/digital_signature/signing.rb +101 -0
- data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
- data/lib/hexapdf/digital_signature.rb +56 -0
- data/lib/hexapdf/document/pages.rb +35 -18
- data/lib/hexapdf/document.rb +21 -14
- data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
- data/lib/hexapdf/type/font_simple.rb +14 -2
- data/lib/hexapdf/type.rb +0 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
- data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
- data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
- data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
- data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
- data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
- data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
- data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
- data/test/hexapdf/digital_signature/test_signing.rb +53 -0
- data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
- data/test/hexapdf/document/test_pages.rb +25 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
- data/test/hexapdf/test_dictionary_fields.rb +9 -3
- data/test/hexapdf/test_document.rb +1 -1
- data/test/hexapdf/test_writer.rb +6 -6
- data/test/hexapdf/type/test_font_simple.rb +18 -6
- metadata +25 -15
- data/lib/hexapdf/document/signatures.rb +0 -546
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
- data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
- data/lib/hexapdf/type/signature/handler.rb +0 -140
- data/test/hexapdf/document/test_signatures.rb +0 -352
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/document'
|
|
5
|
+
require_relative '../common'
|
|
6
|
+
|
|
7
|
+
describe HexaPDF::DigitalSignature::Signing::SignedDataCreator do
|
|
8
|
+
before do
|
|
9
|
+
@klass = HexaPDF::DigitalSignature::Signing::SignedDataCreator
|
|
10
|
+
@signed_data = @klass.new
|
|
11
|
+
@signed_data.certificate = CERTIFICATES.signer_certificate
|
|
12
|
+
@signed_data.key = CERTIFICATES.signer_key
|
|
13
|
+
@signed_data.certificates = [CERTIFICATES.ca_certificate]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "allows setting the attributes" do
|
|
17
|
+
obj = @klass.new
|
|
18
|
+
obj.certificate = :cert
|
|
19
|
+
obj.key = :key
|
|
20
|
+
obj.certificates = :certs
|
|
21
|
+
obj.digest_algorithm = 'sha512'
|
|
22
|
+
obj.timestamp_handler = :tsh
|
|
23
|
+
assert_equal(:cert, obj.certificate)
|
|
24
|
+
assert_equal(:key, obj.key)
|
|
25
|
+
assert_equal(:certs, obj.certificates)
|
|
26
|
+
assert_equal('sha512', obj.digest_algorithm)
|
|
27
|
+
assert_equal(:tsh, obj.timestamp_handler)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "doesn't allow setting attributes to nil using ::create" do
|
|
31
|
+
asn1 = @klass.create("data",
|
|
32
|
+
certificate: CERTIFICATES.signer_certificate,
|
|
33
|
+
key: CERTIFICATES.signer_key,
|
|
34
|
+
digest_algorithm: nil)
|
|
35
|
+
assert_equal('2.16.840.1.101.3.4.2.1', asn1.value[1].value[1].value[0].value[0].value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "content info structure" do
|
|
39
|
+
it "sets the correct content type value for the outer container" do
|
|
40
|
+
asn1 = @signed_data.create("data")
|
|
41
|
+
assert_equal('1.2.840.113549.1.7.2', asn1.value[0].value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "has the signed data structure marked as explicit" do
|
|
45
|
+
asn1 = @signed_data.create("data")
|
|
46
|
+
signed_data = asn1.value[1]
|
|
47
|
+
assert_equal(0, signed_data.tag)
|
|
48
|
+
assert_equal(:EXPLICIT, signed_data.tagging)
|
|
49
|
+
assert_equal(:CONTEXT_SPECIFIC, signed_data.tag_class)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "signed data structure" do
|
|
54
|
+
before do
|
|
55
|
+
@structure = @signed_data.create("data").value[1]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "sets the correct version" do
|
|
59
|
+
assert_equal(1, @structure.value[0].value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "contains a reference to the used digest algorithm" do
|
|
63
|
+
assert_equal('2.16.840.1.101.3.4.2.1', @structure.value[1].value[0].value[0].value)
|
|
64
|
+
assert_nil(@structure.value[1].value[0].value[1].value)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "contains an empty encapsulated content structure" do
|
|
68
|
+
assert_equal(1, @structure.value[2].value.size)
|
|
69
|
+
assert_equal('1.2.840.113549.1.7.1', @structure.value[2].value[0].value)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "contains the assigned certificates" do
|
|
73
|
+
assert_equal(2, @structure.value[3].value.size)
|
|
74
|
+
assert_equal(0, @structure.value[3].tag)
|
|
75
|
+
assert_equal(:IMPLICIT, @structure.value[3].tagging)
|
|
76
|
+
assert_equal(:CONTEXT_SPECIFIC, @structure.value[3].tag_class)
|
|
77
|
+
assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
|
|
78
|
+
@structure.value[3].value)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "contains a single signer info structure" do
|
|
82
|
+
assert_equal(1, @structure.value[4].value.size)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "signer info" do
|
|
87
|
+
before do
|
|
88
|
+
@structure = @signed_data.create("data").value[1].value[4].value[0]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "has the expected number of entries" do
|
|
92
|
+
assert_equal(6, @structure.value.size)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "sets the correct version" do
|
|
96
|
+
assert_equal(1, @structure.value[0].value)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "uses issuer and serial for the signer identifer" do
|
|
100
|
+
assert_equal(CERTIFICATES.signer_certificate.issuer, @structure.value[1].value[0])
|
|
101
|
+
assert_equal(CERTIFICATES.signer_certificate.serial, @structure.value[1].value[1].value)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "contains a reference to the used digest algorithm" do
|
|
105
|
+
assert_equal('2.16.840.1.101.3.4.2.1', @structure.value[2].value[0].value)
|
|
106
|
+
assert_nil(@structure.value[2].value[1].value)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "signed attributes" do
|
|
110
|
+
it "uses the correct tagging for the attributes" do
|
|
111
|
+
assert_equal(0, @structure.value[3].tag)
|
|
112
|
+
assert_equal(:IMPLICIT, @structure.value[3].tagging)
|
|
113
|
+
assert_equal(:CONTEXT_SPECIFIC, @structure.value[3].tag_class)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "contains the content type identifier" do
|
|
117
|
+
attr = @structure.value[3].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.3' }
|
|
118
|
+
assert_equal('1.2.840.113549.1.7.1', attr.value[1].value[0].value)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "contains the message digest attribute" do
|
|
122
|
+
attr = @structure.value[3].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.4' }
|
|
123
|
+
assert_equal(OpenSSL::Digest.digest('SHA256', 'data'), attr.value[1].value[0].value)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "contains the signing certificate attribute" do
|
|
127
|
+
attr = @structure.value[3].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.16.2.47' }
|
|
128
|
+
signing_cert = attr.value[1].value[0]
|
|
129
|
+
assert_equal(1, signing_cert.value.size)
|
|
130
|
+
assert_equal(1, signing_cert.value[0].value.size)
|
|
131
|
+
assert_equal(2, signing_cert.value[0].value[0].value.size)
|
|
132
|
+
assert_equal(OpenSSL::Digest.digest('sha256', CERTIFICATES.signer_certificate.to_der),
|
|
133
|
+
signing_cert.value[0].value[0].value[0].value)
|
|
134
|
+
assert_equal(2, signing_cert.value[0].value[0].value[1].value.size)
|
|
135
|
+
assert_equal(1, signing_cert.value[0].value[0].value[1].value[0].value.size)
|
|
136
|
+
assert_equal(1, signing_cert.value[0].value[0].value[1].value[0].value[0].value.size)
|
|
137
|
+
assert_equal(4, signing_cert.value[0].value[0].value[1].value[0].value[0].tag)
|
|
138
|
+
assert_equal(:IMPLICIT, signing_cert.value[0].value[0].value[1].value[0].value[0].tagging)
|
|
139
|
+
assert_equal(:CONTEXT_SPECIFIC, signing_cert.value[0].value[0].value[1].value[0].value[0].tag_class)
|
|
140
|
+
assert_equal(CERTIFICATES.signer_certificate.issuer,
|
|
141
|
+
signing_cert.value[0].value[0].value[1].value[0].value[0].value[0])
|
|
142
|
+
assert_equal(CERTIFICATES.signer_certificate.serial,
|
|
143
|
+
signing_cert.value[0].value[0].value[1].value[1].value)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "contains the signature algorithm reference" do
|
|
148
|
+
assert_equal('1.2.840.113549.1.1.1', @structure.value[4].value[0].value)
|
|
149
|
+
assert_nil(@structure.value[4].value[1].value)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "contains the signature itself" do
|
|
153
|
+
to_sign = OpenSSL::ASN1::Set.new(@structure.value[3].value).to_der
|
|
154
|
+
assert_equal(CERTIFICATES.signer_key.sign('SHA256', to_sign), @structure.value[5].value)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "fails if the signature algorithm is not supported" do
|
|
158
|
+
@signed_data.certificate = CERTIFICATES.dsa_signer_certificate
|
|
159
|
+
@signed_data.key = CERTIFICATES.dsa_signer_key
|
|
160
|
+
assert_raises(HexaPDF::Error) { @signed_data.create("data") }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "can use a different digest algorithm" do
|
|
164
|
+
@signed_data.digest_algorithm = 'sha384'
|
|
165
|
+
structure = @signed_data.create("data").value[1].value[4].value[0]
|
|
166
|
+
to_sign = OpenSSL::ASN1::Set.new(structure.value[3].value).to_der
|
|
167
|
+
assert_equal('2.16.840.1.101.3.4.2.2', structure.value[2].value[0].value)
|
|
168
|
+
assert_equal(CERTIFICATES.signer_key.sign('SHA384', to_sign), structure.value[5].value)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "allows delegating the signature to a provided signing block" do
|
|
172
|
+
@signed_data.key = nil
|
|
173
|
+
digest_algorithm = nil
|
|
174
|
+
calculated_hash = nil
|
|
175
|
+
structure = @signed_data.create("data") do |algorithm, hash|
|
|
176
|
+
digest_algorithm = algorithm
|
|
177
|
+
calculated_hash = hash
|
|
178
|
+
"signed"
|
|
179
|
+
end.value[1].value[4].value[0]
|
|
180
|
+
to_sign = OpenSSL::Digest.digest('SHA256', OpenSSL::ASN1::Set.new(structure.value[3].value).to_der)
|
|
181
|
+
assert_equal('sha256', digest_algorithm)
|
|
182
|
+
assert_equal(calculated_hash, to_sign)
|
|
183
|
+
assert_equal('signed', structure.value[5].value)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe "unsigned attributes" do
|
|
187
|
+
it "allows adding a timestamp token" do
|
|
188
|
+
tsh = Object.new
|
|
189
|
+
io = nil
|
|
190
|
+
byte_range = nil
|
|
191
|
+
tsh.define_singleton_method(:sign) do |i_io, i_byte_range|
|
|
192
|
+
io = i_io
|
|
193
|
+
byte_range = i_byte_range
|
|
194
|
+
"timestamp"
|
|
195
|
+
end
|
|
196
|
+
@signed_data.timestamp_handler = tsh
|
|
197
|
+
|
|
198
|
+
structure = @signed_data.create("data").value[1].value[4].value[0]
|
|
199
|
+
assert_equal(structure.value[5].value, io.string)
|
|
200
|
+
assert_equal([0, io.string.size, 0, 0], byte_range)
|
|
201
|
+
|
|
202
|
+
attr = structure.value[6].value.find {|obj| obj.value[0].value == '1.2.840.113549.1.9.16.2.14' }
|
|
203
|
+
assert_equal("timestamp", attr.value[1].value[0])
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe "cms signature" do
|
|
209
|
+
it "includes the current time as signing time" do
|
|
210
|
+
Time.stub(:now, Time.at(0)) do
|
|
211
|
+
asn1 = OpenSSL::ASN1.decode(@signed_data.create("data"))
|
|
212
|
+
attr = asn1.value[1].value[0].value[4].value[0].value[3].value.
|
|
213
|
+
find {|obj| obj.value[0].value == 'signingTime' }
|
|
214
|
+
assert_equal(Time.now.utc, attr.value[1].value[0].value)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
describe "pades signature" do
|
|
220
|
+
it "doesn't include the signing-time attribute" do
|
|
221
|
+
signer_info = @signed_data.create("data", type: :pades).value[1].value[4].value[0]
|
|
222
|
+
refute(signer_info.value[3].value.find {|obj| obj.value[0].value == 'signingTime' })
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/document'
|
|
5
|
+
require_relative '../common'
|
|
6
|
+
|
|
7
|
+
describe HexaPDF::DigitalSignature::Signing::TimestampHandler do
|
|
8
|
+
before do
|
|
9
|
+
@doc = HexaPDF::Document.new
|
|
10
|
+
@handler = HexaPDF::DigitalSignature::Signing::TimestampHandler.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "allows setting the attributes in the constructor" do
|
|
14
|
+
handler = @handler.class.new(
|
|
15
|
+
tsa_url: "url", tsa_hash_algorithm: "MD5", tsa_policy_id: "5",
|
|
16
|
+
reason: "Reason", location: "Location", contact_info: "Contact",
|
|
17
|
+
signature_size: 1_000
|
|
18
|
+
)
|
|
19
|
+
assert_equal("url", handler.tsa_url)
|
|
20
|
+
assert_equal("MD5", handler.tsa_hash_algorithm)
|
|
21
|
+
assert_equal("5", handler.tsa_policy_id)
|
|
22
|
+
assert_equal("Reason", handler.reason)
|
|
23
|
+
assert_equal("Location", handler.location)
|
|
24
|
+
assert_equal("Contact", handler.contact_info)
|
|
25
|
+
assert_equal(1_000, handler.signature_size)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "finalizes the signature field and signature objects" do
|
|
29
|
+
@field = @doc.wrap({})
|
|
30
|
+
@sig = @doc.wrap({})
|
|
31
|
+
@handler.reason = 'Reason'
|
|
32
|
+
@handler.location = 'Location'
|
|
33
|
+
@handler.contact_info = 'Contact'
|
|
34
|
+
|
|
35
|
+
@handler.finalize_objects(@field, @sig)
|
|
36
|
+
assert_equal('2.0', @doc.version)
|
|
37
|
+
assert_equal(:DocTimeStamp, @sig[:Type])
|
|
38
|
+
assert_equal(:'Adobe.PPKLite', @sig[:Filter])
|
|
39
|
+
assert_equal(:'ETSI.RFC3161', @sig[:SubFilter])
|
|
40
|
+
assert_equal('Reason', @sig[:Reason])
|
|
41
|
+
assert_equal('Location', @sig[:Location])
|
|
42
|
+
assert_equal('Contact', @sig[:ContactInfo])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "returns the size of serialized signature" do
|
|
46
|
+
@handler.tsa_url = "http://127.0.0.1:34567"
|
|
47
|
+
CERTIFICATES.start_tsa_server
|
|
48
|
+
assert(@handler.signature_size > 1000)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "sign" do
|
|
52
|
+
before do
|
|
53
|
+
@data = StringIO.new("data")
|
|
54
|
+
@range = [0, 4, 0, 0]
|
|
55
|
+
@handler.tsa_url = "http://127.0.0.1:34567"
|
|
56
|
+
CERTIFICATES.start_tsa_server
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "respects the set hash algorithm and policy id" do
|
|
60
|
+
@handler.tsa_hash_algorithm = 'SHA256'
|
|
61
|
+
@handler.tsa_policy_id = '1.2.3.4.2'
|
|
62
|
+
token = OpenSSL::ASN1.decode(@handler.sign(@data, @range))
|
|
63
|
+
content = OpenSSL::ASN1.decode(token.value[1].value[0].value[2].value[1].value[0].value)
|
|
64
|
+
policy_id = content.value[1].value
|
|
65
|
+
digest_algorithm = content.value[2].value[0].value[0].value
|
|
66
|
+
assert_equal('SHA256', digest_algorithm)
|
|
67
|
+
assert_equal("1.2.3.4.2", policy_id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "returns the serialized timestamp token" do
|
|
71
|
+
token = OpenSSL::PKCS7.new(@handler.sign(@data, @range))
|
|
72
|
+
assert_equal(CERTIFICATES.ca_certificate.subject, token.signers[0].issuer)
|
|
73
|
+
assert_equal(CERTIFICATES.timestamp_certificate.serial, token.signers[0].serial)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "fails if the timestamp token could not be created" do
|
|
77
|
+
@handler.tsa_hash_algorithm = 'SHA1'
|
|
78
|
+
msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
|
|
79
|
+
assert_match(/BAD_ALG/, msg.message)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "fails if the timestamp server couldn't process the request" do
|
|
83
|
+
@handler.tsa_policy_id = '1.2.3.4.1'
|
|
84
|
+
msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
|
|
85
|
+
assert_match(/Invalid TSA server response/, msg.message)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
require 'digest'
|
|
4
4
|
require 'test_helper'
|
|
5
5
|
require_relative 'common'
|
|
6
|
-
require 'hexapdf/
|
|
6
|
+
require 'hexapdf/digital_signature'
|
|
7
7
|
require 'ostruct'
|
|
8
8
|
|
|
9
|
-
describe HexaPDF::
|
|
9
|
+
describe HexaPDF::DigitalSignature::CMSHandler do
|
|
10
10
|
before do
|
|
11
11
|
@data = 'Some data'
|
|
12
12
|
@dict = OpenStruct.new
|
|
@@ -15,11 +15,11 @@ describe HexaPDF::Type::Signature::AdbePkcs7Detached do
|
|
|
15
15
|
OpenSSL::PKCS7::DETACHED)
|
|
16
16
|
@dict.contents = @pkcs7.to_der
|
|
17
17
|
@dict.signed_data = @data
|
|
18
|
-
@handler = HexaPDF::
|
|
18
|
+
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
it "returns the signer name" do
|
|
22
|
-
assert_equal("signer", @handler.signer_name)
|
|
22
|
+
assert_equal("RSA signer", @handler.signer_name)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
it "returns the signing time" do
|
|
@@ -60,7 +60,7 @@ describe HexaPDF::Type::Signature::AdbePkcs7Detached do
|
|
|
60
60
|
@pkcs7.add_signer(OpenSSL::PKCS7::SignerInfo.new(CERTIFICATES.signer_certificate,
|
|
61
61
|
CERTIFICATES.signer_key, 'SHA1'))
|
|
62
62
|
@dict.contents = @pkcs7.to_der
|
|
63
|
-
@handler = HexaPDF::
|
|
63
|
+
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
64
64
|
result = @handler.verify(@store)
|
|
65
65
|
assert_equal(2, result.messages.size)
|
|
66
66
|
assert_equal(:error, result.messages.first.type)
|
|
@@ -80,7 +80,7 @@ describe HexaPDF::Type::Signature::AdbePkcs7Detached do
|
|
|
80
80
|
@data, [CERTIFICATES.ca_certificate],
|
|
81
81
|
OpenSSL::PKCS7::DETACHED)
|
|
82
82
|
@dict.contents = @pkcs7.to_der
|
|
83
|
-
@handler = HexaPDF::
|
|
83
|
+
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
84
84
|
result = @handler.verify(@store)
|
|
85
85
|
assert_equal(:error, result.messages.first.type)
|
|
86
86
|
assert_match(/key usage is missing 'Digital Signature'/, result.messages.first.content)
|
|
@@ -110,7 +110,7 @@ describe HexaPDF::Type::Signature::AdbePkcs7Detached do
|
|
|
110
110
|
res = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate, req)
|
|
111
111
|
@dict.contents = res.token.to_der
|
|
112
112
|
@dict.signature_type = 'ETSI.RFC3161'
|
|
113
|
-
@handler = HexaPDF::
|
|
113
|
+
@handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
|
|
114
114
|
|
|
115
115
|
result = @handler.verify(@store)
|
|
116
116
|
assert_equal(:info, result.messages.last.type)
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
|
|
3
3
|
require 'test_helper'
|
|
4
|
-
require 'hexapdf/
|
|
4
|
+
require 'hexapdf/digital_signature'
|
|
5
5
|
require 'hexapdf/document'
|
|
6
6
|
require 'time'
|
|
7
7
|
require 'ostruct'
|
|
8
8
|
|
|
9
|
-
describe HexaPDF::
|
|
9
|
+
describe HexaPDF::DigitalSignature::Handler do
|
|
10
10
|
before do
|
|
11
11
|
@time = Time.parse("2021-11-14 7:00")
|
|
12
12
|
@dict = {Name: "handler", M: @time}
|
|
13
|
-
@handler = HexaPDF::
|
|
14
|
-
@result = HexaPDF::
|
|
13
|
+
@handler = HexaPDF::DigitalSignature::Handler.new(@dict)
|
|
14
|
+
@result = HexaPDF::DigitalSignature::VerificationResult.new
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
it "returns the signer name" do
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require 'test_helper'
|
|
4
4
|
require_relative 'common'
|
|
5
|
-
require 'hexapdf/
|
|
5
|
+
require 'hexapdf/digital_signature'
|
|
6
6
|
require 'ostruct'
|
|
7
7
|
|
|
8
|
-
describe HexaPDF::
|
|
8
|
+
describe HexaPDF::DigitalSignature::PKCS1Handler do
|
|
9
9
|
before do
|
|
10
10
|
@data = 'Some data'
|
|
11
11
|
@dict = OpenStruct.new
|
|
@@ -14,7 +14,7 @@ describe HexaPDF::Type::Signature::AdbeX509RsaSha1 do
|
|
|
14
14
|
@dict.contents = OpenSSL::ASN1::OctetString.new(encoded_data).to_der
|
|
15
15
|
@dict.Cert = [CERTIFICATES.signer_certificate.to_der]
|
|
16
16
|
def @dict.key?(*); true; end
|
|
17
|
-
@handler = HexaPDF::
|
|
17
|
+
@handler = HexaPDF::DigitalSignature::PKCS1Handler.new(@dict)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
it "returns the certificate chain" do
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require 'test_helper'
|
|
4
4
|
require 'hexapdf/document'
|
|
5
|
-
require 'hexapdf/
|
|
6
|
-
require_relative '
|
|
5
|
+
require 'hexapdf/digital_signature'
|
|
6
|
+
require_relative 'common'
|
|
7
7
|
require 'stringio'
|
|
8
8
|
|
|
9
|
-
describe HexaPDF::
|
|
9
|
+
describe HexaPDF::DigitalSignature::Signature::TransformParams do
|
|
10
10
|
before do
|
|
11
11
|
@doc = HexaPDF::Document.new
|
|
12
12
|
@params = @doc.add({Type: :TransformParams})
|
|
@@ -36,7 +36,7 @@ describe HexaPDF::Type::Signature::TransformParams do
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
describe HexaPDF::
|
|
39
|
+
describe HexaPDF::DigitalSignature::Signature::SignatureReference do
|
|
40
40
|
before do
|
|
41
41
|
@doc = HexaPDF::Document.new
|
|
42
42
|
@sigref = @doc.add({Type: :SigRef})
|
|
@@ -52,7 +52,7 @@ describe HexaPDF::Type::Signature::SignatureReference do
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
describe HexaPDF::
|
|
55
|
+
describe HexaPDF::DigitalSignature::Signature do
|
|
56
56
|
before do
|
|
57
57
|
@doc = HexaPDF::Document.new
|
|
58
58
|
@sig = @doc.add({Type: :Sig, Filter: :'Adobe.PPKLite', SubFilter: :'ETSI.CAdES.detached'})
|
|
@@ -65,7 +65,7 @@ describe HexaPDF::Type::Signature do
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
it "returns the signer name" do
|
|
68
|
-
assert_equal('signer', @sig.signer_name)
|
|
68
|
+
assert_equal('RSA signer', @sig.signer_name)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
it "returns the signing time" do
|
|
@@ -88,7 +88,7 @@ describe HexaPDF::Type::Signature do
|
|
|
88
88
|
|
|
89
89
|
describe "signature_handler" do
|
|
90
90
|
it "returns the signature handler" do
|
|
91
|
-
assert_kind_of(HexaPDF::
|
|
91
|
+
assert_kind_of(HexaPDF::DigitalSignature::Handler, @sig.signature_handler)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
it "fails if the required handler is not available" do
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
require 'hexapdf/document'
|
|
7
|
+
require_relative 'common'
|
|
8
|
+
|
|
9
|
+
describe HexaPDF::DigitalSignature::Signatures do
|
|
10
|
+
before do
|
|
11
|
+
@doc = HexaPDF::Document.new
|
|
12
|
+
@form = @doc.acro_form(create: true)
|
|
13
|
+
@sig1 = @form.create_signature_field("test1")
|
|
14
|
+
@sig2 = @form.create_signature_field("test2")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "iterates over all signature dictionaries" do
|
|
18
|
+
assert_equal([], @doc.signatures.to_a)
|
|
19
|
+
@sig1.field_value = :sig1
|
|
20
|
+
@sig2.field_value = :sig2
|
|
21
|
+
assert_equal([:sig1, :sig2], @doc.signatures.to_a)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "returns the number of signature dictionaries" do
|
|
25
|
+
@sig1.field_value = :sig1
|
|
26
|
+
assert_equal(1, @doc.signatures.count)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "signing_handler" do
|
|
30
|
+
it "return the initialized handler" do
|
|
31
|
+
handler = @doc.signatures.signing_handler(certificate: 'cert', reason: 'reason')
|
|
32
|
+
assert_equal('cert', handler.certificate)
|
|
33
|
+
assert_equal('reason', handler.reason)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "fails if the given task is not available" do
|
|
37
|
+
assert_raises(HexaPDF::Error) { @doc.signatures.signing_handler(name: :unknown) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "add" do
|
|
42
|
+
before do
|
|
43
|
+
@doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
|
|
44
|
+
@io = StringIO.new(''.b)
|
|
45
|
+
@handler = @doc.signatures.signing_handler(
|
|
46
|
+
certificate: CERTIFICATES.signer_certificate,
|
|
47
|
+
key: CERTIFICATES.signer_key,
|
|
48
|
+
certificate_chain: [CERTIFICATES.ca_certificate]
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "uses the provided signature dictionary" do
|
|
53
|
+
sig = @doc.add({Type: :Sig, Key: :value})
|
|
54
|
+
@doc.signatures.add(@io, @handler, signature: sig)
|
|
55
|
+
assert_equal(1, @doc.signatures.to_a.compact.size)
|
|
56
|
+
assert_equal(:value, @doc.signatures.to_a[0][:Key])
|
|
57
|
+
refute_equal(:value, @doc.acro_form.each_field.first[:Key])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "creates the signature dictionary if none is provided" do
|
|
61
|
+
@doc.signatures.add(@io, @handler)
|
|
62
|
+
assert_equal(1, @doc.signatures.to_a.compact.size)
|
|
63
|
+
refute(@doc.acro_form.each_field.first.key?(:Contents))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "sets the needed information on the signature dictionary" do
|
|
67
|
+
def @handler.finalize_objects(sigfield, sig)
|
|
68
|
+
sig[:key] = :sig
|
|
69
|
+
sigfield[:key] = :sig_field
|
|
70
|
+
end
|
|
71
|
+
@doc.signatures.add(@io, @handler, write_options: {update_fields: false})
|
|
72
|
+
sig = @doc.signatures.first
|
|
73
|
+
assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2501], sig[:ByteRange].value)
|
|
74
|
+
assert_equal(:sig, sig[:key])
|
|
75
|
+
assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
|
|
76
|
+
assert(sig.key?(:Contents))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "creates the main form dictionary if necessary" do
|
|
80
|
+
@doc.signatures.add(@io, @handler)
|
|
81
|
+
assert(@doc.acro_form)
|
|
82
|
+
assert_equal([:signatures_exist, :append_only], @doc.acro_form.signature_flags)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "uses the provided signature field" do
|
|
86
|
+
field = @doc.acro_form(create: true).create_signature_field('Signature2')
|
|
87
|
+
@doc.signatures.add(@io, @handler, signature: field)
|
|
88
|
+
assert_nil(@doc.acro_form.field_by_name("Signature3"))
|
|
89
|
+
refute_nil(field.field_value)
|
|
90
|
+
assert_nil(@doc.signatures.first[:T])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "uses an existing signature field if possible" do
|
|
94
|
+
field = @doc.acro_form(create: true).create_signature_field('Signature2')
|
|
95
|
+
field.field_value = sig = @doc.add({Type: :Sig, key: :value})
|
|
96
|
+
@doc.signatures.add(@io, @handler, signature: sig)
|
|
97
|
+
assert_nil(@doc.acro_form.field_by_name("Signature3"))
|
|
98
|
+
assert_same(sig, @doc.signatures.first)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "creates the signature field if necessary" do
|
|
102
|
+
@doc.acro_form(create: true).create_text_field('Signature2')
|
|
103
|
+
@doc.signatures.add(@io, @handler)
|
|
104
|
+
field = @doc.acro_form.field_by_name("Signature3")
|
|
105
|
+
assert_equal(:Sig, field.field_type)
|
|
106
|
+
refute_nil(field.field_value)
|
|
107
|
+
assert_equal(1, field.each_widget.count)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "handles different xref section types correctly when determing the offsets" do
|
|
111
|
+
@doc.delete(7)
|
|
112
|
+
sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
|
|
113
|
+
assert_equal([0, 1036, 1036 + sig[:Contents].size * 2 + 2, 2483], sig[:ByteRange].value)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "works if the signature object is the last object of the xref section" do
|
|
117
|
+
field = @doc.acro_form(create: true).create_signature_field('Signature2')
|
|
118
|
+
field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
|
|
119
|
+
sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
|
|
120
|
+
assert_equal([0, 3143, 3143 + sig[:Contents].size * 2 + 2, 380], sig[:ByteRange].value)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "allows writing to a file in addition to writing to an IO" do
|
|
124
|
+
tempfile = Tempfile.new('hexapdf-signature')
|
|
125
|
+
tempfile.close
|
|
126
|
+
@doc.signatures.add(tempfile.path, @handler)
|
|
127
|
+
doc = HexaPDF::Document.open(tempfile.path)
|
|
128
|
+
assert(doc.signatures.first.verify(allow_self_signed: true).success?)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "adds a new revision with the signature" do
|
|
132
|
+
@doc.signatures.add(@io, @handler)
|
|
133
|
+
signed_doc = HexaPDF::Document.new(io: @io)
|
|
134
|
+
assert(signed_doc.signatures.first.verify)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
require 'hexapdf/document'
|
|
5
|
+
require 'hexapdf/digital_signature'
|
|
6
|
+
require_relative 'common'
|
|
7
|
+
|
|
8
|
+
describe HexaPDF::DigitalSignature::Signing do
|
|
9
|
+
before do
|
|
10
|
+
@handler = HexaPDF::DigitalSignature::Signing::DefaultHandler.new(
|
|
11
|
+
certificate: CERTIFICATES.signer_certificate,
|
|
12
|
+
key: CERTIFICATES.signer_key,
|
|
13
|
+
certificate_chain: [CERTIFICATES.ca_certificate]
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "allows embedding an external signature value" do
|
|
18
|
+
# Create first signature normally for testing the signature-finding code later
|
|
19
|
+
doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
|
|
20
|
+
io = StringIO.new(''.b)
|
|
21
|
+
doc.signatures.add(io, @handler)
|
|
22
|
+
doc = HexaPDF::Document.new(io: io)
|
|
23
|
+
io = StringIO.new(''.b)
|
|
24
|
+
|
|
25
|
+
byte_range = nil
|
|
26
|
+
@handler.signature_size = 5000
|
|
27
|
+
@handler.certificate = nil
|
|
28
|
+
@handler.external_signing = proc {|_, br| byte_range = br; "" }
|
|
29
|
+
doc.signatures.add(io, @handler)
|
|
30
|
+
|
|
31
|
+
io.pos = byte_range[0]
|
|
32
|
+
data = io.read(byte_range[1])
|
|
33
|
+
io.pos = byte_range[2]
|
|
34
|
+
data << io.read(byte_range[3])
|
|
35
|
+
contents = OpenSSL::PKCS7.sign(CERTIFICATES.signer_certificate, @handler.key, data,
|
|
36
|
+
@handler.certificate_chain,
|
|
37
|
+
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
|
|
38
|
+
HexaPDF::DigitalSignature::Signing.embed_signature(io, contents)
|
|
39
|
+
doc = HexaPDF::Document.new(io: io)
|
|
40
|
+
assert_equal(2, doc.signatures.each.count)
|
|
41
|
+
doc.signatures.each do |signature|
|
|
42
|
+
assert(signature.verify(allow_self_signed: true).messages.find {|m| m.content == 'Signature valid' })
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "fails if the reserved signature space is too small" do
|
|
47
|
+
doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
|
|
48
|
+
io = StringIO.new(''.b)
|
|
49
|
+
def @handler.signature_size; 200; end
|
|
50
|
+
msg = assert_raises(HexaPDF::Error) { doc.signatures.add(io, @handler) }
|
|
51
|
+
assert_match(/space.*too small.*200 vs/, msg.message)
|
|
52
|
+
end
|
|
53
|
+
end
|