easy_code_sign 0.1.0 → 0.2.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 +18 -19
- data/lib/easy_code_sign/pdf/cms_builder.rb +142 -0
- data/lib/easy_code_sign/pdf/native_signer.rb +275 -0
- data/lib/easy_code_sign/signable/pdf_file.rb +54 -424
- data/lib/easy_code_sign/version.rb +1 -1
- data/lib/easy_code_sign.rb +0 -2
- data/test/pdf_signable_test.rb +87 -463
- metadata +6 -4
data/test/pdf_signable_test.rb
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "test_helper"
|
|
4
4
|
require "tempfile"
|
|
5
|
-
require "
|
|
5
|
+
require "openssl"
|
|
6
6
|
|
|
7
7
|
class PdfFileSignableTest < EasyCodeSignTest
|
|
8
8
|
def setup
|
|
9
9
|
@temp_pdf = Tempfile.new(["test", ".pdf"])
|
|
10
|
-
|
|
10
|
+
create_minimal_pdf(@temp_pdf.path)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def teardown
|
|
@@ -49,77 +49,97 @@ class PdfFileSignableTest < EasyCodeSignTest
|
|
|
49
49
|
refute signable.signed?
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_prepare_for_signing_succeeds
|
|
58
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
59
|
-
signable.prepare_for_signing
|
|
60
|
-
|
|
61
|
-
assert_equal "PDF_SIGNING_PLACEHOLDER", signable.content_to_sign
|
|
62
|
-
end
|
|
52
|
+
def test_apply_signature_produces_signed_pdf
|
|
53
|
+
key = generate_rsa_key
|
|
54
|
+
cert = build_self_signed_cert(key)
|
|
55
|
+
out = Tempfile.new(["signed", ".pdf"])
|
|
63
56
|
|
|
64
|
-
def test_default_signature_config
|
|
65
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
66
|
-
|
|
67
|
-
refute signable.signature_config[:visible]
|
|
68
|
-
assert_equal 1, signable.signature_config[:page]
|
|
69
|
-
assert_equal :bottom_right, signable.signature_config[:position]
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def test_visible_signature_option
|
|
73
57
|
signable = EasyCodeSign::Signable::PdfFile.new(
|
|
74
58
|
@temp_pdf.path,
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
output_path: out.path,
|
|
60
|
+
signature_reason: "Testing"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
signed_path = signable.apply_signature(
|
|
64
|
+
->(hash) { key.sign_raw("SHA256", hash) },
|
|
65
|
+
[cert]
|
|
77
66
|
)
|
|
78
67
|
|
|
79
|
-
assert
|
|
80
|
-
|
|
68
|
+
assert File.exist?(signed_path)
|
|
69
|
+
|
|
70
|
+
verifier = EasyCodeSign::Signable::PdfFile.new(signed_path)
|
|
71
|
+
sig = verifier.extract_signature
|
|
72
|
+
|
|
73
|
+
assert sig, "Expected a signature to be present"
|
|
74
|
+
assert sig[:contents], "Expected /Contents to be non-empty"
|
|
75
|
+
assert_equal 4, sig[:byte_range].size
|
|
76
|
+
ensure
|
|
77
|
+
out.close
|
|
78
|
+
out.unlink
|
|
81
79
|
end
|
|
82
80
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
81
|
+
def test_signed_returns_true_after_signing
|
|
82
|
+
key = generate_rsa_key
|
|
83
|
+
cert = build_self_signed_cert(key)
|
|
84
|
+
out = Tempfile.new(["signed2", ".pdf"])
|
|
85
|
+
|
|
86
|
+
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path, output_path: out.path)
|
|
87
|
+
signable.apply_signature(->(hash) { key.sign_raw("SHA256", hash) }, [cert])
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
verifier = EasyCodeSign::Signable::PdfFile.new(out.path)
|
|
90
|
+
assert verifier.signed?
|
|
91
|
+
ensure
|
|
92
|
+
out.close
|
|
93
|
+
out.unlink
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
signable = EasyCodeSign::Signable::PdfFile.new(
|
|
96
|
-
@temp_pdf.path,
|
|
97
|
-
signature_page: 2
|
|
98
|
-
)
|
|
96
|
+
private
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
def generate_rsa_key
|
|
99
|
+
OpenSSL::PKey::RSA.generate(2048)
|
|
101
100
|
end
|
|
102
101
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
def build_self_signed_cert(key)
|
|
103
|
+
cert = OpenSSL::X509::Certificate.new
|
|
104
|
+
cert.version = 2
|
|
105
|
+
cert.serial = 1
|
|
106
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=Test Signer")
|
|
107
|
+
cert.issuer = cert.subject
|
|
108
|
+
cert.public_key = key.public_key
|
|
109
|
+
cert.not_before = Time.now
|
|
110
|
+
cert.not_after = Time.now + 86_400
|
|
111
|
+
cert.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
112
|
+
cert
|
|
106
113
|
end
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
# Builds a minimal valid PDF using only stdlib — no hexapdf or prawn.
|
|
116
|
+
def create_minimal_pdf(path)
|
|
117
|
+
hdr = "%PDF-1.4\n"
|
|
118
|
+
obj1 = "1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n"
|
|
119
|
+
obj2 = "2 0 obj\n<</Type /Pages /Kids [3 0 R] /Count 1>>\nendobj\n"
|
|
120
|
+
obj3 = "3 0 obj\n<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]>>\nendobj\n"
|
|
121
|
+
|
|
122
|
+
o1 = hdr.bytesize
|
|
123
|
+
o2 = o1 + obj1.bytesize
|
|
124
|
+
o3 = o2 + obj2.bytesize
|
|
125
|
+
xref_off = o3 + obj3.bytesize
|
|
109
126
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
xref = "xref\n0 4\n" \
|
|
128
|
+
"0000000000 65535 f \n" \
|
|
129
|
+
"#{o1.to_s.rjust(10, "0")} 00000 n \n" \
|
|
130
|
+
"#{o2.to_s.rjust(10, "0")} 00000 n \n" \
|
|
131
|
+
"#{o3.to_s.rjust(10, "0")} 00000 n \n"
|
|
132
|
+
|
|
133
|
+
trailer = "trailer\n<</Size 4 /Root 1 0 R>>\nstartxref\n#{xref_off}\n%%EOF\n"
|
|
134
|
+
|
|
135
|
+
File.binwrite(path, hdr + obj1 + obj2 + obj3 + xref + trailer)
|
|
116
136
|
end
|
|
117
137
|
end
|
|
118
138
|
|
|
119
139
|
class PdfVerificationTest < EasyCodeSignTest
|
|
120
140
|
def setup
|
|
121
141
|
@temp_pdf = Tempfile.new(["test", ".pdf"])
|
|
122
|
-
|
|
142
|
+
create_minimal_pdf(@temp_pdf.path)
|
|
123
143
|
end
|
|
124
144
|
|
|
125
145
|
def teardown
|
|
@@ -144,426 +164,30 @@ class PdfVerificationTest < EasyCodeSignTest
|
|
|
144
164
|
|
|
145
165
|
private
|
|
146
166
|
|
|
147
|
-
def create_test_pdf(path)
|
|
148
|
-
doc = HexaPDF::Document.new
|
|
149
|
-
page = doc.pages.add
|
|
150
|
-
page.canvas.font("Helvetica", size: 12)
|
|
151
|
-
page.canvas.text("Test PDF for verification", at: [100, 700])
|
|
152
|
-
doc.write(path)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
167
|
def assert_any_match(array, pattern)
|
|
156
168
|
assert array.any? { |item| item.match?(pattern) },
|
|
157
169
|
"Expected at least one item in #{array.inspect} to match #{pattern.inspect}"
|
|
158
170
|
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
class PdfDeferredSigningTest < EasyCodeSignTest
|
|
162
|
-
def setup
|
|
163
|
-
super
|
|
164
|
-
@temp_pdf = Tempfile.new(["deferred_test", ".pdf"])
|
|
165
|
-
create_test_pdf(@temp_pdf.path)
|
|
166
|
-
@key = OpenSSL::PKey::RSA.new(2048)
|
|
167
|
-
@cert = create_test_cert(@key)
|
|
168
|
-
@chain = [@cert]
|
|
169
|
-
@cleanup_files = []
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def teardown
|
|
173
|
-
@temp_pdf.close
|
|
174
|
-
@temp_pdf.unlink
|
|
175
|
-
@cleanup_files.each { |f| File.delete(f) if File.exist?(f) }
|
|
176
|
-
super
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def test_prepare_deferred_returns_request
|
|
180
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
181
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
182
|
-
track_prepared(request)
|
|
183
|
-
|
|
184
|
-
assert_instance_of EasyCodeSign::DeferredSigningRequest, request
|
|
185
|
-
assert_instance_of String, request.digest
|
|
186
|
-
assert_equal :sha256, request.digest_algorithm
|
|
187
|
-
assert File.exist?(request.prepared_pdf_path)
|
|
188
|
-
assert_instance_of Array, request.byte_range
|
|
189
|
-
assert_equal 4, request.byte_range.size
|
|
190
|
-
assert_equal @cert.to_pem, request.certificate.to_pem
|
|
191
|
-
assert_instance_of Time, request.signing_time
|
|
192
|
-
assert_instance_of Time, request.created_at
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def test_prepare_deferred_creates_valid_pdf
|
|
196
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
197
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
198
|
-
track_prepared(request)
|
|
199
|
-
|
|
200
|
-
assert File.exist?(request.prepared_pdf_path)
|
|
201
|
-
header = File.open(request.prepared_pdf_path, "rb") { |f| f.read(5) }
|
|
202
|
-
assert_equal "%PDF-", header
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def test_prepare_deferred_digest_size
|
|
206
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
207
|
-
request = signable.prepare_deferred(@cert, @chain, digest_algorithm: "sha256")
|
|
208
|
-
track_prepared(request)
|
|
209
|
-
|
|
210
|
-
assert_equal 32, request.digest.bytesize
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def test_prepare_deferred_sha512_digest_size
|
|
214
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
215
|
-
request = signable.prepare_deferred(@cert, @chain, digest_algorithm: "sha512")
|
|
216
|
-
track_prepared(request)
|
|
217
|
-
|
|
218
|
-
assert_equal 64, request.digest.bytesize
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def test_deferred_request_serialization_roundtrip
|
|
222
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
223
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
224
|
-
track_prepared(request)
|
|
225
|
-
|
|
226
|
-
hash = request.to_h
|
|
227
|
-
restored = EasyCodeSign::DeferredSigningRequest.from_h(hash)
|
|
228
|
-
|
|
229
|
-
assert_equal request.digest, restored.digest
|
|
230
|
-
assert_equal request.digest_algorithm, restored.digest_algorithm
|
|
231
|
-
assert_equal request.prepared_pdf_path, restored.prepared_pdf_path
|
|
232
|
-
assert_equal request.byte_range, restored.byte_range
|
|
233
|
-
assert_equal request.certificate.to_pem, restored.certificate.to_pem
|
|
234
|
-
assert_equal request.certificate_chain.map(&:to_pem), restored.certificate_chain.map(&:to_pem)
|
|
235
|
-
assert_equal request.estimated_size, restored.estimated_size
|
|
236
|
-
assert_equal request.signing_time.to_i, restored.signing_time.to_i
|
|
237
|
-
assert_equal request.created_at.to_i, restored.created_at.to_i
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def test_deferred_request_digest_hex_and_base64
|
|
241
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
242
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
243
|
-
track_prepared(request)
|
|
244
|
-
|
|
245
|
-
assert_equal request.digest.unpack1("H*"), request.digest_hex
|
|
246
|
-
assert_equal Base64.strict_encode64(request.digest), request.digest_base64
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def test_finalize_deferred_produces_signed_pdf
|
|
250
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
251
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
252
|
-
track_prepared(request)
|
|
253
|
-
|
|
254
|
-
# Sign the digest with our test key (simulating external signer)
|
|
255
|
-
raw_signature = @key.sign_raw("sha256", request.digest)
|
|
256
|
-
|
|
257
|
-
finalizer = EasyCodeSign::Signable::PdfFile.new(request.prepared_pdf_path)
|
|
258
|
-
signed_path = finalizer.finalize_deferred(request, raw_signature)
|
|
259
|
-
|
|
260
|
-
assert File.exist?(signed_path)
|
|
261
|
-
|
|
262
|
-
# Verify the signed PDF has a signature
|
|
263
|
-
verifier_signable = EasyCodeSign::Signable::PdfFile.new(signed_path)
|
|
264
|
-
sig = verifier_signable.extract_signature
|
|
265
|
-
refute_nil sig, "Expected signed PDF to have an extractable signature"
|
|
266
|
-
refute_nil sig[:contents]
|
|
267
|
-
refute_nil sig[:byte_range]
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def test_signed_attributes_data_present
|
|
271
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
272
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
273
|
-
track_prepared(request)
|
|
274
|
-
|
|
275
|
-
refute_nil request.signed_attributes_data
|
|
276
|
-
refute_empty request.signed_attributes_data
|
|
277
|
-
refute_nil request.signed_attributes_base64
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def test_signed_attributes_data_hash_matches_digest
|
|
281
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
282
|
-
request = signable.prepare_deferred(@cert, @chain, digest_algorithm: "sha256")
|
|
283
|
-
track_prepared(request)
|
|
284
|
-
|
|
285
|
-
computed_hash = OpenSSL::Digest::SHA256.digest(request.signed_attributes_data)
|
|
286
|
-
assert_equal request.digest, computed_hash,
|
|
287
|
-
"SHA256(signed_attributes_data) must equal the captured digest"
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def test_webcrypto_style_signing_produces_valid_pdf
|
|
291
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
292
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
293
|
-
track_prepared(request)
|
|
294
|
-
|
|
295
|
-
# Simulate WebCrypto: hash-and-sign in one step (like crypto.subtle.sign)
|
|
296
|
-
raw_signature = @key.sign("SHA256", request.signed_attributes_data)
|
|
297
|
-
|
|
298
|
-
finalizer = EasyCodeSign::Signable::PdfFile.new(request.prepared_pdf_path)
|
|
299
|
-
signed_path = finalizer.finalize_deferred(request, raw_signature)
|
|
300
|
-
|
|
301
|
-
assert File.exist?(signed_path)
|
|
302
|
-
|
|
303
|
-
verifier_signable = EasyCodeSign::Signable::PdfFile.new(signed_path)
|
|
304
|
-
sig = verifier_signable.extract_signature
|
|
305
|
-
refute_nil sig, "Expected signed PDF to have an extractable signature"
|
|
306
|
-
refute_nil sig[:contents]
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def test_signed_attributes_data_serialization_roundtrip
|
|
310
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
311
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
312
|
-
track_prepared(request)
|
|
313
|
-
|
|
314
|
-
hash = request.to_h
|
|
315
|
-
assert hash.key?("signed_attributes_data"), "to_h should include signed_attributes_data"
|
|
316
|
-
|
|
317
|
-
restored = EasyCodeSign::DeferredSigningRequest.from_h(hash)
|
|
318
|
-
assert_equal request.signed_attributes_data, restored.signed_attributes_data
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def test_finalize_deferred_raises_on_missing_pdf
|
|
322
|
-
request = EasyCodeSign::DeferredSigningRequest.new(
|
|
323
|
-
digest: "\x00" * 32,
|
|
324
|
-
digest_algorithm: :sha256,
|
|
325
|
-
prepared_pdf_path: "/nonexistent/path/missing.pdf",
|
|
326
|
-
byte_range: [0, 100, 200, 100],
|
|
327
|
-
certificate: @cert,
|
|
328
|
-
certificate_chain: @chain,
|
|
329
|
-
estimated_size: 8192,
|
|
330
|
-
signing_time: Time.now
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
334
|
-
|
|
335
|
-
assert_raises(EasyCodeSign::DeferredSigningError) do
|
|
336
|
-
signable.finalize_deferred(request, "fake_sig")
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def test_finalize_deferred_raises_on_oversized_signature
|
|
341
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
342
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
343
|
-
track_prepared(request)
|
|
344
|
-
|
|
345
|
-
# Create a signature that's way too large for the reserved space
|
|
346
|
-
oversized_signature = "\xFF" * (request.estimated_size * 3)
|
|
347
|
-
|
|
348
|
-
finalizer = EasyCodeSign::Signable::PdfFile.new(request.prepared_pdf_path)
|
|
349
|
-
|
|
350
|
-
assert_raises(EasyCodeSign::DeferredSigningError) do
|
|
351
|
-
finalizer.finalize_deferred(request, oversized_signature)
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def test_finalize_deferred_with_timestamp_embeds_token
|
|
356
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
357
|
-
request = signable.prepare_deferred(@cert, @chain, timestamp_size: 4096)
|
|
358
|
-
track_prepared(request)
|
|
359
|
-
|
|
360
|
-
raw_signature = @key.sign_raw("sha256", request.digest)
|
|
361
|
-
mock_token = create_mock_timestamp_token
|
|
362
|
-
|
|
363
|
-
finalizer = EasyCodeSign::Signable::PdfFile.new(request.prepared_pdf_path)
|
|
364
|
-
signed_path = finalizer.finalize_deferred(request, raw_signature, timestamp_token: mock_token)
|
|
365
|
-
|
|
366
|
-
assert File.exist?(signed_path)
|
|
367
|
-
|
|
368
|
-
# Parse the CMS from the signed PDF and check for timestamp unsigned attribute
|
|
369
|
-
verifier_signable = EasyCodeSign::Signable::PdfFile.new(signed_path)
|
|
370
|
-
sig = verifier_signable.extract_signature
|
|
371
|
-
refute_nil sig, "Expected signed PDF to have a signature"
|
|
372
|
-
|
|
373
|
-
# Decode the CMS DER (trimming zero-padding) and look for id-aa-timeStampToken OID
|
|
374
|
-
cms_asn1 = decode_cms_from_contents(sig[:contents])
|
|
375
|
-
assert_timestamp_attribute_present(cms_asn1)
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def test_finalize_deferred_without_timestamp_unchanged
|
|
379
|
-
signable = EasyCodeSign::Signable::PdfFile.new(@temp_pdf.path)
|
|
380
|
-
request = signable.prepare_deferred(@cert, @chain)
|
|
381
|
-
track_prepared(request)
|
|
382
|
-
|
|
383
|
-
raw_signature = @key.sign_raw("sha256", request.digest)
|
|
384
|
-
|
|
385
|
-
finalizer = EasyCodeSign::Signable::PdfFile.new(request.prepared_pdf_path)
|
|
386
|
-
signed_path = finalizer.finalize_deferred(request, raw_signature)
|
|
387
|
-
|
|
388
|
-
assert File.exist?(signed_path)
|
|
389
|
-
|
|
390
|
-
verifier_signable = EasyCodeSign::Signable::PdfFile.new(signed_path)
|
|
391
|
-
sig = verifier_signable.extract_signature
|
|
392
|
-
refute_nil sig
|
|
393
|
-
|
|
394
|
-
# Verify no timestamp unsigned attribute is present
|
|
395
|
-
cms_asn1 = decode_cms_from_contents(sig[:contents])
|
|
396
|
-
refute_timestamp_attribute_present(cms_asn1)
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
private
|
|
400
|
-
|
|
401
|
-
def track_prepared(request)
|
|
402
|
-
@cleanup_files << request.prepared_pdf_path
|
|
403
|
-
end
|
|
404
171
|
|
|
405
|
-
def
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
doc.write(path)
|
|
411
|
-
end
|
|
172
|
+
def create_minimal_pdf(path)
|
|
173
|
+
hdr = "%PDF-1.4\n"
|
|
174
|
+
obj1 = "1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n"
|
|
175
|
+
obj2 = "2 0 obj\n<</Type /Pages /Kids [3 0 R] /Count 1>>\nendobj\n"
|
|
176
|
+
obj3 = "3 0 obj\n<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]>>\nendobj\n"
|
|
412
177
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
cert.subject = OpenSSL::X509::Name.parse("/CN=Test Deferred Signer")
|
|
418
|
-
cert.issuer = cert.subject
|
|
419
|
-
cert.public_key = key.public_key
|
|
420
|
-
cert.not_before = Time.now
|
|
421
|
-
cert.not_after = Time.now + 86_400
|
|
422
|
-
cert.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
423
|
-
cert
|
|
424
|
-
end
|
|
178
|
+
o1 = hdr.bytesize
|
|
179
|
+
o2 = o1 + obj1.bytesize
|
|
180
|
+
o3 = o2 + obj2.bytesize
|
|
181
|
+
xref_off = o3 + obj3.bytesize
|
|
425
182
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
tsa_cert.serial = 99
|
|
432
|
-
tsa_cert.subject = OpenSSL::X509::Name.parse("/CN=Mock TSA")
|
|
433
|
-
tsa_cert.issuer = tsa_cert.subject
|
|
434
|
-
tsa_cert.public_key = tsa_key.public_key
|
|
435
|
-
tsa_cert.not_before = Time.now
|
|
436
|
-
tsa_cert.not_after = Time.now + 86_400
|
|
437
|
-
tsa_cert.sign(tsa_key, OpenSSL::Digest.new("SHA256"))
|
|
438
|
-
|
|
439
|
-
pkcs7 = OpenSSL::PKCS7.sign(tsa_cert, tsa_key, "mock timestamp data",
|
|
440
|
-
[], OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
|
441
|
-
|
|
442
|
-
EasyCodeSign::Timestamp::TimestampToken.new(
|
|
443
|
-
token_der: pkcs7.to_der,
|
|
444
|
-
timestamp: Time.now,
|
|
445
|
-
serial_number: 99,
|
|
446
|
-
policy_oid: "1.2.3.4",
|
|
447
|
-
tsa_url: "http://test.tsa"
|
|
448
|
-
)
|
|
449
|
-
end
|
|
183
|
+
xref = "xref\n0 4\n" \
|
|
184
|
+
"0000000000 65535 f \n" \
|
|
185
|
+
"#{o1.to_s.rjust(10, "0")} 00000 n \n" \
|
|
186
|
+
"#{o2.to_s.rjust(10, "0")} 00000 n \n" \
|
|
187
|
+
"#{o3.to_s.rjust(10, "0")} 00000 n \n"
|
|
450
188
|
|
|
451
|
-
|
|
452
|
-
def decode_cms_from_contents(contents)
|
|
453
|
-
# The PDF reserves a fixed-size space; trailing zeros must be stripped.
|
|
454
|
-
# Parse just the first valid DER structure using decode_all which ignores trailing data.
|
|
455
|
-
OpenSSL::ASN1.decode(contents.sub(/\x00+\z/, ""))
|
|
456
|
-
end
|
|
189
|
+
trailer = "trailer\n<</Size 4 /Root 1 0 R>>\nstartxref\n#{xref_off}\n%%EOF\n"
|
|
457
190
|
|
|
458
|
-
|
|
459
|
-
TIMESTAMP_TOKEN_OID = "1.2.840.113549.1.9.16.2.14"
|
|
460
|
-
|
|
461
|
-
def find_oid_in_asn1(asn1, target_oid)
|
|
462
|
-
if asn1.is_a?(OpenSSL::ASN1::ObjectId) && asn1.oid == target_oid
|
|
463
|
-
return true
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
if asn1.respond_to?(:value) && asn1.value.is_a?(Array)
|
|
467
|
-
asn1.value.any? { |child| find_oid_in_asn1(child, target_oid) }
|
|
468
|
-
else
|
|
469
|
-
false
|
|
470
|
-
end
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
def assert_timestamp_attribute_present(cms_asn1)
|
|
474
|
-
assert find_oid_in_asn1(cms_asn1, TIMESTAMP_TOKEN_OID),
|
|
475
|
-
"Expected CMS to contain id-aa-timeStampToken unsigned attribute"
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
def refute_timestamp_attribute_present(cms_asn1)
|
|
479
|
-
refute find_oid_in_asn1(cms_asn1, TIMESTAMP_TOKEN_OID),
|
|
480
|
-
"Expected CMS NOT to contain id-aa-timeStampToken unsigned attribute"
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
class PdfTimestampHandlerTest < Minitest::Test
|
|
485
|
-
def test_sign_returns_token_der_from_eager_token
|
|
486
|
-
mock_token = Struct.new(:token_der).new("mock_der_bytes")
|
|
487
|
-
handler = EasyCodeSign::Pdf::TimestampHandler.new(mock_token)
|
|
488
|
-
|
|
489
|
-
result = handler.sign(StringIO.new, [0, 0, 0, 0])
|
|
490
|
-
assert_equal "mock_der_bytes", result
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
def test_sign_returns_token_der_from_lazy_proc
|
|
494
|
-
mock_token = Struct.new(:token_der).new("lazy_der_bytes")
|
|
495
|
-
handler = EasyCodeSign::Pdf::TimestampHandler.new(-> { mock_token })
|
|
496
|
-
|
|
497
|
-
result = handler.sign(StringIO.new, [0, 0, 0, 0])
|
|
498
|
-
assert_equal "lazy_der_bytes", result
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
def test_sign_returns_nil_when_token_is_nil
|
|
502
|
-
handler = EasyCodeSign::Pdf::TimestampHandler.new(nil)
|
|
503
|
-
|
|
504
|
-
result = handler.sign(StringIO.new, [0, 0, 0, 0])
|
|
505
|
-
assert_nil result
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
def test_sign_returns_nil_when_lazy_proc_returns_nil
|
|
509
|
-
handler = EasyCodeSign::Pdf::TimestampHandler.new(-> { nil })
|
|
510
|
-
|
|
511
|
-
result = handler.sign(StringIO.new, [0, 0, 0, 0])
|
|
512
|
-
assert_nil result
|
|
513
|
-
end
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
class ExternalSigningCallbackTest < Minitest::Test
|
|
517
|
-
def test_callback_receives_data_and_returns_signature
|
|
518
|
-
expected_signature = "mock_signature"
|
|
519
|
-
callback = EasyCodeSign::Signable::ExternalSigningCallback.new(->(data) { expected_signature })
|
|
520
|
-
|
|
521
|
-
result = callback.sign("test data", "SHA256")
|
|
522
|
-
assert_equal expected_signature, result
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
def test_callback_is_private
|
|
526
|
-
callback = EasyCodeSign::Signable::ExternalSigningCallback.new(->(_) { "sig" })
|
|
527
|
-
assert callback.private?
|
|
528
|
-
end
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
class ExternalSigningProxyTest < Minitest::Test
|
|
532
|
-
def setup
|
|
533
|
-
@key = OpenSSL::PKey::RSA.new(2048)
|
|
534
|
-
@cert = create_test_cert(@key)
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
def test_proxy_returns_precomputed_signature
|
|
538
|
-
signature = "precomputed_signature"
|
|
539
|
-
proxy = EasyCodeSign::Signable::ExternalSigningProxy.new(signature, @cert, [@cert])
|
|
540
|
-
|
|
541
|
-
result = proxy.sign("any data", "SHA256")
|
|
542
|
-
assert_equal signature, result
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
def test_proxy_has_certificate
|
|
546
|
-
proxy = EasyCodeSign::Signable::ExternalSigningProxy.new("sig", @cert, [@cert])
|
|
547
|
-
assert_equal @cert, proxy.certificate
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
def test_proxy_is_private
|
|
551
|
-
proxy = EasyCodeSign::Signable::ExternalSigningProxy.new("sig", @cert, [@cert])
|
|
552
|
-
assert proxy.private?
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
private
|
|
556
|
-
|
|
557
|
-
def create_test_cert(key)
|
|
558
|
-
cert = OpenSSL::X509::Certificate.new
|
|
559
|
-
cert.version = 2
|
|
560
|
-
cert.serial = 1
|
|
561
|
-
cert.subject = OpenSSL::X509::Name.parse("/CN=Test")
|
|
562
|
-
cert.issuer = cert.subject
|
|
563
|
-
cert.public_key = key.public_key
|
|
564
|
-
cert.not_before = Time.now
|
|
565
|
-
cert.not_after = Time.now + 86_400
|
|
566
|
-
cert.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
567
|
-
cert
|
|
191
|
+
File.binwrite(path, hdr + obj1 + obj2 + obj3 + xref + trailer)
|
|
568
192
|
end
|
|
569
193
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easy_code_sign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- michail
|
|
@@ -24,19 +24,19 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: pdf-reader
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '2.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '2.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: pkcs11
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -142,6 +142,8 @@ files:
|
|
|
142
142
|
- lib/easy_code_sign/deferred_signing_request.rb
|
|
143
143
|
- lib/easy_code_sign/errors.rb
|
|
144
144
|
- lib/easy_code_sign/pdf/appearance_builder.rb
|
|
145
|
+
- lib/easy_code_sign/pdf/cms_builder.rb
|
|
146
|
+
- lib/easy_code_sign/pdf/native_signer.rb
|
|
145
147
|
- lib/easy_code_sign/pdf/timestamp_handler.rb
|
|
146
148
|
- lib/easy_code_sign/providers/base.rb
|
|
147
149
|
- lib/easy_code_sign/providers/pkcs11_base.rb
|