easy_code_sign 0.1.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/LICENSE +21 -0
  4. data/README.md +331 -0
  5. data/Rakefile +16 -0
  6. data/exe/easysign +7 -0
  7. data/lib/easy_code_sign/cli.rb +428 -0
  8. data/lib/easy_code_sign/configuration.rb +102 -0
  9. data/lib/easy_code_sign/deferred_signing_request.rb +104 -0
  10. data/lib/easy_code_sign/errors.rb +113 -0
  11. data/lib/easy_code_sign/pdf/appearance_builder.rb +104 -0
  12. data/lib/easy_code_sign/pdf/timestamp_handler.rb +31 -0
  13. data/lib/easy_code_sign/providers/base.rb +126 -0
  14. data/lib/easy_code_sign/providers/pkcs11_base.rb +197 -0
  15. data/lib/easy_code_sign/providers/safenet.rb +109 -0
  16. data/lib/easy_code_sign/signable/base.rb +98 -0
  17. data/lib/easy_code_sign/signable/gem_file.rb +224 -0
  18. data/lib/easy_code_sign/signable/pdf_file.rb +486 -0
  19. data/lib/easy_code_sign/signable/zip_file.rb +226 -0
  20. data/lib/easy_code_sign/signer.rb +254 -0
  21. data/lib/easy_code_sign/timestamp/client.rb +184 -0
  22. data/lib/easy_code_sign/timestamp/request.rb +114 -0
  23. data/lib/easy_code_sign/timestamp/response.rb +246 -0
  24. data/lib/easy_code_sign/timestamp/verifier.rb +227 -0
  25. data/lib/easy_code_sign/verification/certificate_chain.rb +298 -0
  26. data/lib/easy_code_sign/verification/result.rb +222 -0
  27. data/lib/easy_code_sign/verification/signature_checker.rb +196 -0
  28. data/lib/easy_code_sign/verification/trust_store.rb +140 -0
  29. data/lib/easy_code_sign/verifier.rb +426 -0
  30. data/lib/easy_code_sign/version.rb +5 -0
  31. data/lib/easy_code_sign.rb +183 -0
  32. data/plugin/.gitignore +21 -0
  33. data/plugin/Gemfile +24 -0
  34. data/plugin/Gemfile.lock +134 -0
  35. data/plugin/README.md +248 -0
  36. data/plugin/Rakefile +121 -0
  37. data/plugin/docs/API_REFERENCE.md +366 -0
  38. data/plugin/docs/DEVELOPMENT.md +522 -0
  39. data/plugin/docs/INSTALLATION.md +204 -0
  40. data/plugin/native_host/build/Rakefile +90 -0
  41. data/plugin/native_host/install/com.easysign.host.json +9 -0
  42. data/plugin/native_host/install/install_chrome.sh +81 -0
  43. data/plugin/native_host/install/install_firefox.sh +81 -0
  44. data/plugin/native_host/src/easy_sign_host.rb +158 -0
  45. data/plugin/native_host/src/protocol.rb +101 -0
  46. data/plugin/native_host/src/signing_service.rb +167 -0
  47. data/plugin/native_host/test/native_host_test.rb +113 -0
  48. data/plugin/src/easy_sign/background.rb +323 -0
  49. data/plugin/src/easy_sign/content.rb +74 -0
  50. data/plugin/src/easy_sign/inject.rb +239 -0
  51. data/plugin/src/easy_sign/messaging.rb +109 -0
  52. data/plugin/src/easy_sign/popup.rb +200 -0
  53. data/plugin/templates/manifest.json +58 -0
  54. data/plugin/templates/popup.css +223 -0
  55. data/plugin/templates/popup.html +59 -0
  56. data/sig/easy_code_sign.rbs +4 -0
  57. data/test/easy_code_sign_test.rb +122 -0
  58. data/test/pdf_signable_test.rb +569 -0
  59. data/test/signable_test.rb +334 -0
  60. data/test/test_helper.rb +18 -0
  61. data/test/timestamp_test.rb +163 -0
  62. data/test/verification_test.rb +350 -0
  63. metadata +219 -0
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "tempfile"
5
+ require "zip"
6
+
7
+ class SignableBaseTest < Minitest::Test
8
+ def test_raises_for_nonexistent_file
9
+ assert_raises(EasyCodeSign::InvalidFileError) do
10
+ EasyCodeSign::Signable::Base.new("/nonexistent/file.txt")
11
+ end
12
+ end
13
+
14
+ def test_default_hash_algorithm_is_sha256
15
+ file = Tempfile.new(["test", ".txt"])
16
+ file.write("test content")
17
+ file.close
18
+
19
+ signable = TestSignable.new(file.path)
20
+ assert_equal :sha256, signable.hash_algorithm
21
+ ensure
22
+ file.unlink
23
+ end
24
+
25
+ def test_compute_hash_returns_correct_digest
26
+ file = Tempfile.new(["test", ".txt"])
27
+ file.write("test content")
28
+ file.close
29
+
30
+ signable = TestSignable.new(file.path)
31
+ expected = OpenSSL::Digest::SHA256.digest("hello")
32
+ assert_equal expected, signable.compute_hash("hello")
33
+ ensure
34
+ file.unlink
35
+ end
36
+
37
+ def test_signature_algorithm_for_rsa
38
+ file = Tempfile.new(["test", ".txt"])
39
+ file.close
40
+
41
+ signable = TestSignable.new(file.path)
42
+ assert_equal :sha256_rsa, signable.signature_algorithm(:rsa)
43
+ ensure
44
+ file.unlink
45
+ end
46
+
47
+ # Concrete implementation for testing abstract base
48
+ class TestSignable < EasyCodeSign::Signable::Base
49
+ def prepare_for_signing; end
50
+ def content_to_sign; "test"; end
51
+ def apply_signature(sig, chain, timestamp_token: nil); file_path; end
52
+ def extract_signature; nil; end
53
+ end
54
+ end
55
+
56
+ class ZipFileSignableTest < Minitest::Test
57
+ def setup
58
+ @temp_zip = Tempfile.new(["test", ".zip"])
59
+ create_test_zip(@temp_zip.path)
60
+ end
61
+
62
+ def teardown
63
+ @temp_zip.close
64
+ @temp_zip.unlink
65
+ end
66
+
67
+ def test_validates_zip_extension
68
+ txt_file = Tempfile.new(["test", ".txt"])
69
+ txt_file.write("content")
70
+ txt_file.close
71
+
72
+ assert_raises(EasyCodeSign::InvalidFileError) do
73
+ EasyCodeSign::Signable::ZipFile.new(txt_file.path)
74
+ end
75
+ ensure
76
+ txt_file.unlink
77
+ end
78
+
79
+ def test_accepts_valid_zip_file
80
+ signable = EasyCodeSign::Signable::ZipFile.new(@temp_zip.path)
81
+ assert_instance_of EasyCodeSign::Signable::ZipFile, signable
82
+ end
83
+
84
+ def test_file_list_returns_zip_contents
85
+ signable = EasyCodeSign::Signable::ZipFile.new(@temp_zip.path)
86
+ files = signable.file_list
87
+
88
+ assert_includes files, "hello.txt"
89
+ assert_includes files, "subdir/world.txt"
90
+ end
91
+
92
+ def test_prepare_for_signing_builds_manifest
93
+ signable = EasyCodeSign::Signable::ZipFile.new(@temp_zip.path)
94
+ signable.prepare_for_signing
95
+
96
+ # Should not raise and content_to_sign should return data
97
+ refute_nil signable.content_to_sign
98
+ end
99
+
100
+ def test_signed_returns_false_for_unsigned_zip
101
+ signable = EasyCodeSign::Signable::ZipFile.new(@temp_zip.path)
102
+ refute signable.signed?
103
+ end
104
+
105
+ def test_extract_signature_returns_nil_for_unsigned
106
+ signable = EasyCodeSign::Signable::ZipFile.new(@temp_zip.path)
107
+ assert_nil signable.extract_signature
108
+ end
109
+
110
+ private
111
+
112
+ def create_test_zip(path)
113
+ Zip::File.open(path, Zip::File::CREATE) do |zip|
114
+ zip.get_output_stream("hello.txt") { |f| f.write("Hello, World!") }
115
+ zip.get_output_stream("subdir/world.txt") { |f| f.write("Nested file") }
116
+ end
117
+ end
118
+ end
119
+
120
+ class GemFileSignableTest < Minitest::Test
121
+ def setup
122
+ @temp_gem = Tempfile.new(["test", ".gem"])
123
+ create_test_gem(@temp_gem.path)
124
+ end
125
+
126
+ def teardown
127
+ @temp_gem.close
128
+ @temp_gem.unlink
129
+ end
130
+
131
+ def test_validates_gem_extension
132
+ txt_file = Tempfile.new(["test", ".txt"])
133
+ txt_file.write("content")
134
+ txt_file.close
135
+
136
+ assert_raises(EasyCodeSign::InvalidFileError) do
137
+ EasyCodeSign::Signable::GemFile.new(txt_file.path)
138
+ end
139
+ ensure
140
+ txt_file.unlink
141
+ end
142
+
143
+ def test_accepts_valid_gem_file
144
+ signable = EasyCodeSign::Signable::GemFile.new(@temp_gem.path)
145
+ assert_instance_of EasyCodeSign::Signable::GemFile, signable
146
+ end
147
+
148
+ def test_signed_returns_false_for_unsigned_gem
149
+ signable = EasyCodeSign::Signable::GemFile.new(@temp_gem.path)
150
+ refute signable.signed?
151
+ end
152
+
153
+ def test_prepare_for_signing_extracts_content
154
+ signable = EasyCodeSign::Signable::GemFile.new(@temp_gem.path)
155
+ signable.prepare_for_signing
156
+
157
+ refute_nil signable.content_to_sign
158
+ end
159
+
160
+ private
161
+
162
+ def create_test_gem(path)
163
+ # Create a minimal gem-like tar archive
164
+ File.open(path, "wb") do |io|
165
+ Gem::Package::TarWriter.new(io) do |tar|
166
+ # Add a fake data.tar.gz
167
+ data = "fake gem data content"
168
+ tar.add_file_simple("data.tar.gz", 0o644, data.bytesize) { |f| f.write(data) }
169
+
170
+ # Add a fake metadata.gz
171
+ metadata = "fake metadata content"
172
+ tar.add_file_simple("metadata.gz", 0o644, metadata.bytesize) { |f| f.write(metadata) }
173
+
174
+ # Add checksums
175
+ checksums = "checksums content"
176
+ tar.add_file_simple("checksums.yaml.gz", 0o644, checksums.bytesize) { |f| f.write(checksums) }
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ class SignerTest < Minitest::Test
183
+ def test_creates_signable_for_gem_extension
184
+ signer = EasyCodeSign::Signer.new
185
+
186
+ temp_gem = Tempfile.new(["test", ".gem"])
187
+ create_minimal_gem(temp_gem.path)
188
+
189
+ signable = signer.send(:create_signable, temp_gem.path)
190
+ assert_instance_of EasyCodeSign::Signable::GemFile, signable
191
+ ensure
192
+ temp_gem&.close
193
+ temp_gem&.unlink
194
+ end
195
+
196
+ def test_creates_signable_for_zip_extension
197
+ signer = EasyCodeSign::Signer.new
198
+
199
+ temp_zip = Tempfile.new(["test", ".zip"])
200
+ Zip::File.open(temp_zip.path, Zip::File::CREATE) do |zip|
201
+ zip.get_output_stream("test.txt") { |f| f.write("test") }
202
+ end
203
+
204
+ signable = signer.send(:create_signable, temp_zip.path)
205
+ assert_instance_of EasyCodeSign::Signable::ZipFile, signable
206
+ ensure
207
+ temp_zip&.close
208
+ temp_zip&.unlink
209
+ end
210
+
211
+ def test_raises_for_unsupported_extension
212
+ signer = EasyCodeSign::Signer.new
213
+
214
+ temp_file = Tempfile.new(["test", ".exe"])
215
+ temp_file.close
216
+
217
+ assert_raises(EasyCodeSign::InvalidFileError) do
218
+ signer.send(:create_signable, temp_file.path)
219
+ end
220
+ ensure
221
+ temp_file&.unlink
222
+ end
223
+
224
+ private
225
+
226
+ def create_minimal_gem(path)
227
+ File.open(path, "wb") do |io|
228
+ Gem::Package::TarWriter.new(io) do |tar|
229
+ tar.add_file_simple("data.tar.gz", 0o644, 4) { |f| f.write("data") }
230
+ tar.add_file_simple("metadata.gz", 0o644, 4) { |f| f.write("meta") }
231
+ tar.add_file_simple("checksums.yaml.gz", 0o644, 4) { |f| f.write("sums") }
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ class SigningResultTest < Minitest::Test
238
+ def test_timestamped_returns_true_when_timestamp_present
239
+ cert = OpenSSL::X509::Certificate.new
240
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Test Signer")
241
+
242
+ token = EasyCodeSign::Timestamp::TimestampToken.new(
243
+ token_der: "fake",
244
+ timestamp: Time.now,
245
+ serial_number: 1,
246
+ policy_oid: "1.2.3",
247
+ tsa_url: "http://tsa.example.com"
248
+ )
249
+
250
+ result = EasyCodeSign::SigningResult.new(
251
+ file_path: "/path/to/file",
252
+ certificate: cert,
253
+ algorithm: :sha256_rsa,
254
+ timestamp_token: token,
255
+ signed_at: Time.now
256
+ )
257
+
258
+ assert result.timestamped?
259
+ end
260
+
261
+ def test_timestamped_returns_false_when_no_timestamp
262
+ cert = OpenSSL::X509::Certificate.new
263
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Test Signer")
264
+
265
+ result = EasyCodeSign::SigningResult.new(
266
+ file_path: "/path/to/file",
267
+ certificate: cert,
268
+ algorithm: :sha256_rsa,
269
+ timestamp_token: nil,
270
+ signed_at: Time.now
271
+ )
272
+
273
+ refute result.timestamped?
274
+ end
275
+
276
+ def test_signer_name_returns_certificate_subject
277
+ cert = OpenSSL::X509::Certificate.new
278
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Test Signer/O=Test Org")
279
+
280
+ result = EasyCodeSign::SigningResult.new(
281
+ file_path: "/path/to/file",
282
+ certificate: cert,
283
+ algorithm: :sha256_rsa,
284
+ timestamp_token: nil,
285
+ signed_at: Time.now
286
+ )
287
+
288
+ assert_includes result.signer_name, "Test Signer"
289
+ end
290
+
291
+ def test_to_h_returns_hash_representation
292
+ cert = OpenSSL::X509::Certificate.new
293
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Test")
294
+ signed_at = Time.now
295
+
296
+ result = EasyCodeSign::SigningResult.new(
297
+ file_path: "/path/to/file.gem",
298
+ certificate: cert,
299
+ algorithm: :sha256_rsa,
300
+ timestamp_token: nil,
301
+ signed_at: signed_at
302
+ )
303
+
304
+ hash = result.to_h
305
+ assert_equal "/path/to/file.gem", hash[:file_path]
306
+ assert_equal :sha256_rsa, hash[:algorithm]
307
+ assert_equal false, hash[:timestamped]
308
+ assert_equal signed_at, hash[:signed_at]
309
+ end
310
+
311
+ def test_timestamp_returns_time_from_token
312
+ cert = OpenSSL::X509::Certificate.new
313
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Test")
314
+ ts_time = Time.utc(2024, 1, 15, 12, 0, 0)
315
+
316
+ token = EasyCodeSign::Timestamp::TimestampToken.new(
317
+ token_der: "fake",
318
+ timestamp: ts_time,
319
+ serial_number: 1,
320
+ policy_oid: "1.2.3",
321
+ tsa_url: "http://tsa.example.com"
322
+ )
323
+
324
+ result = EasyCodeSign::SigningResult.new(
325
+ file_path: "/path/to/file",
326
+ certificate: cert,
327
+ algorithm: :sha256_rsa,
328
+ timestamp_token: token,
329
+ signed_at: Time.now
330
+ )
331
+
332
+ assert_equal ts_time, result.timestamp
333
+ end
334
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "easy_code_sign"
5
+ require "minitest/autorun"
6
+
7
+ # Base test class with common helpers
8
+ class EasyCodeSignTest < Minitest::Test
9
+ def setup
10
+ EasyCodeSign.reset_configuration!
11
+ EasyCodeSign.reset_provider!
12
+ end
13
+
14
+ def teardown
15
+ EasyCodeSign.reset_configuration!
16
+ EasyCodeSign.reset_provider!
17
+ end
18
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TimestampRequestTest < Minitest::Test
6
+ def test_creates_request_with_defaults
7
+ request = EasyCodeSign::Timestamp::Request.new("test data")
8
+
9
+ assert_equal :sha256, request.algorithm
10
+ assert request.cert_req
11
+ assert_nil request.policy_oid
12
+ refute_nil request.nonce
13
+ end
14
+
15
+ def test_computes_message_imprint_hash
16
+ data = "test signature data"
17
+ request = EasyCodeSign::Timestamp::Request.new(data, algorithm: :sha256)
18
+
19
+ expected = OpenSSL::Digest::SHA256.digest(data)
20
+ assert_equal expected, request.message_imprint_hash
21
+ end
22
+
23
+ def test_supports_different_algorithms
24
+ data = "test data"
25
+
26
+ sha256_request = EasyCodeSign::Timestamp::Request.new(data, algorithm: :sha256)
27
+ sha384_request = EasyCodeSign::Timestamp::Request.new(data, algorithm: :sha384)
28
+ sha512_request = EasyCodeSign::Timestamp::Request.new(data, algorithm: :sha512)
29
+
30
+ assert_equal "2.16.840.1.101.3.4.2.1", sha256_request.algorithm_oid
31
+ assert_equal "2.16.840.1.101.3.4.2.2", sha384_request.algorithm_oid
32
+ assert_equal "2.16.840.1.101.3.4.2.3", sha512_request.algorithm_oid
33
+ end
34
+
35
+ def test_to_der_produces_valid_asn1
36
+ request = EasyCodeSign::Timestamp::Request.new("test data")
37
+ der = request.to_der
38
+
39
+ # Should be parseable ASN.1
40
+ asn1 = OpenSSL::ASN1.decode(der)
41
+ assert_instance_of OpenSSL::ASN1::Sequence, asn1
42
+
43
+ # First element should be version (INTEGER 1)
44
+ assert_equal 1, asn1.value[0].value
45
+ end
46
+
47
+ def test_generates_unique_nonces
48
+ request1 = EasyCodeSign::Timestamp::Request.new("data")
49
+ request2 = EasyCodeSign::Timestamp::Request.new("data")
50
+
51
+ refute_equal request1.nonce, request2.nonce
52
+ end
53
+
54
+ def test_raises_for_unsupported_algorithm
55
+ assert_raises(ArgumentError) do
56
+ EasyCodeSign::Timestamp::Request.new("data", algorithm: :md5).algorithm_oid
57
+ end
58
+ end
59
+ end
60
+
61
+ class TimestampClientTest < Minitest::Test
62
+ def test_initializes_with_url
63
+ client = EasyCodeSign::Timestamp::Client.new("http://timestamp.example.com")
64
+
65
+ assert_equal "http://timestamp.example.com", client.url
66
+ assert_equal 30, client.timeout
67
+ assert_nil client.username
68
+ assert_nil client.password
69
+ end
70
+
71
+ def test_initializes_with_custom_options
72
+ client = EasyCodeSign::Timestamp::Client.new(
73
+ "http://timestamp.example.com",
74
+ timeout: 60,
75
+ username: "user",
76
+ password: "pass"
77
+ )
78
+
79
+ assert_equal 60, client.timeout
80
+ assert_equal "user", client.username
81
+ assert_equal "pass", client.password
82
+ end
83
+
84
+ def test_known_tsas_are_defined
85
+ assert_includes EasyCodeSign::Timestamp::Client::KNOWN_TSAS, :digicert
86
+ assert_includes EasyCodeSign::Timestamp::Client::KNOWN_TSAS, :globalsign
87
+ assert_includes EasyCodeSign::Timestamp::Client::KNOWN_TSAS, :sectigo
88
+ end
89
+ end
90
+
91
+ class TimestampTokenTest < Minitest::Test
92
+ def setup
93
+ @token = EasyCodeSign::Timestamp::TimestampToken.new(
94
+ token_der: "fake_der_data",
95
+ timestamp: Time.utc(2024, 1, 15, 12, 0, 0),
96
+ serial_number: 12345,
97
+ policy_oid: "1.2.3.4",
98
+ tsa_url: "http://timestamp.example.com"
99
+ )
100
+ end
101
+
102
+ def test_stores_all_attributes
103
+ assert_equal "fake_der_data", @token.token_der
104
+ assert_equal 12345, @token.serial_number
105
+ assert_equal "1.2.3.4", @token.policy_oid
106
+ assert_equal "http://timestamp.example.com", @token.tsa_url
107
+ end
108
+
109
+ def test_timestamp_iso8601
110
+ assert_equal "2024-01-15T12:00:00Z", @token.timestamp_iso8601
111
+ end
112
+
113
+ def test_to_h_returns_hash
114
+ hash = @token.to_h
115
+
116
+ assert_equal "2024-01-15T12:00:00Z", hash[:timestamp]
117
+ assert_equal 12345, hash[:serial_number]
118
+ assert_equal "1.2.3.4", hash[:policy_oid]
119
+ assert_equal "http://timestamp.example.com", hash[:tsa_url]
120
+ end
121
+ end
122
+
123
+ class TimestampVerifierTest < Minitest::Test
124
+ def test_initializes_with_default_trust_store
125
+ verifier = EasyCodeSign::Timestamp::Verifier.new
126
+
127
+ assert_instance_of OpenSSL::X509::Store, verifier.trust_store
128
+ end
129
+
130
+ def test_initializes_with_custom_trust_store
131
+ custom_store = OpenSSL::X509::Store.new
132
+ verifier = EasyCodeSign::Timestamp::Verifier.new(trust_store: custom_store)
133
+
134
+ assert_same custom_store, verifier.trust_store
135
+ end
136
+ end
137
+
138
+ class TimestampVerificationResultTest < Minitest::Test
139
+ def test_initializes_with_default_values
140
+ result = EasyCodeSign::Timestamp::VerificationResult.new
141
+
142
+ refute result.valid?
143
+ refute result.token_parsed
144
+ refute result.signature_valid
145
+ refute result.imprint_valid
146
+ refute result.chain_valid
147
+ assert_empty result.errors
148
+ assert_empty result.warnings
149
+ end
150
+
151
+ def test_to_h_returns_complete_hash
152
+ result = EasyCodeSign::Timestamp::VerificationResult.new
153
+ result.valid = true
154
+ result.timestamp = Time.utc(2024, 1, 15)
155
+ result.serial_number = 123
156
+
157
+ hash = result.to_h
158
+
159
+ assert hash[:valid]
160
+ assert_equal Time.utc(2024, 1, 15), hash[:timestamp]
161
+ assert_equal 123, hash[:serial_number]
162
+ end
163
+ end