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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyCodeSign
4
+ module Signable
5
+ # Abstract base class for signable file types
6
+ #
7
+ # Subclasses must implement:
8
+ # - #prepare_for_signing - Extract/prepare content for signing
9
+ # - #apply_signature(signature, certificate_chain) - Embed signature in file
10
+ # - #extract_signature - Extract existing signature for verification
11
+ # - #content_to_sign - Return the bytes to be signed
12
+ #
13
+ class Base
14
+ attr_reader :file_path, :options
15
+
16
+ def initialize(file_path, **options)
17
+ @file_path = File.expand_path(file_path)
18
+ @options = options
19
+ validate_file!
20
+ end
21
+
22
+ # Prepare content for signing
23
+ # @return [void]
24
+ def prepare_for_signing
25
+ raise NotImplementedError, "#{self.class}#prepare_for_signing must be implemented"
26
+ end
27
+
28
+ # Get the content that will be signed (typically a hash of the file)
29
+ # @return [String] bytes to be signed
30
+ def content_to_sign
31
+ raise NotImplementedError, "#{self.class}#content_to_sign must be implemented"
32
+ end
33
+
34
+ # Apply signature to the file
35
+ # @param signature [String] raw signature bytes
36
+ # @param certificate_chain [Array<OpenSSL::X509::Certificate>] signing certificate chain
37
+ # @param timestamp_token [String, nil] optional RFC 3161 timestamp token
38
+ # @return [String] path to the signed output file
39
+ def apply_signature(signature, certificate_chain, timestamp_token: nil)
40
+ raise NotImplementedError, "#{self.class}#apply_signature must be implemented"
41
+ end
42
+
43
+ # Extract existing signature from the file
44
+ # @return [Hash, nil] signature data or nil if unsigned
45
+ def extract_signature
46
+ raise NotImplementedError, "#{self.class}#extract_signature must be implemented"
47
+ end
48
+
49
+ # Check if file is already signed
50
+ # @return [Boolean]
51
+ def signed?
52
+ !extract_signature.nil?
53
+ end
54
+
55
+ # Get the hash algorithm to use
56
+ # @return [Symbol] :sha256, :sha384, or :sha512
57
+ def hash_algorithm
58
+ options.fetch(:hash_algorithm, :sha256)
59
+ end
60
+
61
+ # Compute hash of data using configured algorithm
62
+ # @param data [String] data to hash
63
+ # @return [String] hash bytes
64
+ def compute_hash(data)
65
+ digest_class.digest(data)
66
+ end
67
+
68
+ # Get the OpenSSL digest class for the hash algorithm
69
+ # @return [Class]
70
+ def digest_class
71
+ case hash_algorithm
72
+ when :sha256 then OpenSSL::Digest::SHA256
73
+ when :sha384 then OpenSSL::Digest::SHA384
74
+ when :sha512 then OpenSSL::Digest::SHA512
75
+ else raise ArgumentError, "Unsupported hash algorithm: #{hash_algorithm}"
76
+ end
77
+ end
78
+
79
+ # Get signature algorithm symbol for the provider
80
+ # @param key_type [Symbol] :rsa or :ecdsa
81
+ # @return [Symbol]
82
+ def signature_algorithm(key_type = :rsa)
83
+ :"#{hash_algorithm}_#{key_type}"
84
+ end
85
+
86
+ protected
87
+
88
+ def validate_file!
89
+ raise InvalidFileError, "File not found: #{file_path}" unless File.exist?(file_path)
90
+ raise InvalidFileError, "Cannot read file: #{file_path}" unless File.readable?(file_path)
91
+ end
92
+
93
+ def output_path
94
+ options[:output_path] || file_path
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/package"
4
+ require "openssl"
5
+ require "tempfile"
6
+ require "fileutils"
7
+
8
+ module EasyCodeSign
9
+ module Signable
10
+ # Handler for signing Ruby gem files (.gem)
11
+ #
12
+ # Gem signing follows the RubyGems signing format:
13
+ # - Creates PKCS#7 detached signatures for data.tar.gz, metadata.gz, checksums.yaml.gz
14
+ # - Signature files are stored as .sig files in the gem archive
15
+ #
16
+ # @example
17
+ # gem_file = EasyCodeSign::Signable::GemFile.new("my_gem-1.0.0.gem")
18
+ # gem_file.prepare_for_signing
19
+ # signature = provider.sign(gem_file.content_to_sign, algorithm: :sha256_rsa)
20
+ # gem_file.apply_signature(signature, [cert])
21
+ #
22
+ class GemFile < Base
23
+ # Files within the gem that get signed
24
+ SIGNABLE_FILES = %w[data.tar.gz metadata.gz checksums.yaml.gz].freeze
25
+
26
+ def initialize(file_path, **options)
27
+ super
28
+ validate_gem_format!
29
+ @contents = {}
30
+ @signatures = {}
31
+ end
32
+
33
+ def prepare_for_signing
34
+ extract_gem_contents
35
+ end
36
+
37
+ # Returns concatenated hashes of all signable files
38
+ # This is what gets signed by the hardware token
39
+ def content_to_sign
40
+ prepare_for_signing if @contents.empty?
41
+
42
+ # Create a digest of all signable content
43
+ combined = SIGNABLE_FILES.filter_map do |name|
44
+ content = @contents[name]
45
+ next unless content
46
+
47
+ "#{name}:#{compute_hash(content).unpack1('H*')}"
48
+ end.join("\n")
49
+
50
+ compute_hash(combined)
51
+ end
52
+
53
+ def apply_signature(signature, certificate_chain, timestamp_token: nil)
54
+ prepare_for_signing if @contents.empty?
55
+
56
+ # Build PKCS#7 signature structure for each file
57
+ SIGNABLE_FILES.each do |name|
58
+ content = @contents[name]
59
+ next unless content
60
+
61
+ pkcs7_sig = build_pkcs7_signature(content, signature, certificate_chain, timestamp_token)
62
+ @signatures["#{name}.sig"] = pkcs7_sig.to_der
63
+ end
64
+
65
+ write_signed_gem
66
+ end
67
+
68
+ def extract_signature
69
+ sigs = {}
70
+
71
+ File.open(file_path, "rb") do |io|
72
+ Gem::Package::TarReader.new(io) do |tar|
73
+ tar.each do |entry|
74
+ next unless entry.full_name.end_with?(".sig")
75
+
76
+ sigs[entry.full_name] = entry.read
77
+ end
78
+ end
79
+ end
80
+
81
+ sigs.empty? ? nil : sigs
82
+ rescue StandardError
83
+ nil
84
+ end
85
+
86
+ # Get the gem specification
87
+ # @return [Gem::Specification, nil]
88
+ def spec
89
+ @spec ||= extract_gemspec
90
+ end
91
+
92
+ private
93
+
94
+ def validate_gem_format!
95
+ unless file_path.end_with?(".gem")
96
+ raise InvalidFileError, "File must have .gem extension: #{file_path}"
97
+ end
98
+
99
+ # Verify it's a valid tar archive
100
+ File.open(file_path, "rb") do |io|
101
+ Gem::Package::TarReader.new(io) do |tar|
102
+ tar.first # Just check we can read it
103
+ end
104
+ end
105
+ rescue Gem::Package::TarInvalidError => e
106
+ raise InvalidFileError, "Invalid gem file: #{e.message}"
107
+ end
108
+
109
+ def extract_gem_contents
110
+ @contents = {}
111
+
112
+ File.open(file_path, "rb") do |io|
113
+ Gem::Package::TarReader.new(io) do |tar|
114
+ tar.each do |entry|
115
+ if SIGNABLE_FILES.include?(entry.full_name)
116
+ @contents[entry.full_name] = entry.read
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ if @contents.empty?
123
+ raise InvalidFileError, "Gem file contains no signable content"
124
+ end
125
+
126
+ @contents
127
+ end
128
+
129
+ def extract_gemspec
130
+ File.open(file_path, "rb") do |io|
131
+ Gem::Package::TarReader.new(io) do |tar|
132
+ tar.each do |entry|
133
+ if entry.full_name == "metadata.gz"
134
+ require "zlib"
135
+ yaml = Zlib::GzipReader.new(StringIO.new(entry.read)).read
136
+ return Gem::Specification.from_yaml(yaml)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ nil
142
+ rescue StandardError
143
+ nil
144
+ end
145
+
146
+ def build_pkcs7_signature(content, raw_signature, certificate_chain, timestamp_token)
147
+ # Create PKCS#7 signed data structure
148
+ signing_cert = certificate_chain.first
149
+
150
+ pkcs7 = OpenSSL::PKCS7.new
151
+ pkcs7.type = "signed"
152
+
153
+ # Add certificates to the signature
154
+ certificate_chain.each do |cert|
155
+ pkcs7.add_certificate(cert)
156
+ end
157
+
158
+ # Create signer info
159
+ # Note: We're using a pre-computed signature from the hardware token
160
+ # This creates a compatible PKCS#7 structure
161
+ signer_info = OpenSSL::PKCS7::SignerInfo.new(
162
+ signing_cert,
163
+ nil, # We don't have the private key - signature was made by HSM
164
+ digest_class.name.split("::").last
165
+ )
166
+
167
+ # The actual signature from the HSM needs to be embedded
168
+ # This is a simplified version - full implementation would need
169
+ # to properly construct the SignerInfo with the external signature
170
+
171
+ pkcs7.add_signer(signer_info)
172
+ pkcs7.add_data(content)
173
+
174
+ # Add timestamp if provided
175
+ if timestamp_token
176
+ # Timestamp would be added as an unsigned attribute
177
+ # Implementation depends on how we receive the timestamp
178
+ end
179
+
180
+ pkcs7
181
+ end
182
+
183
+ def write_signed_gem
184
+ output = output_path
185
+ temp_file = Tempfile.new(["signed_gem", ".gem"])
186
+
187
+ begin
188
+ # Write new gem with signatures
189
+ File.open(temp_file.path, "wb") do |out_io|
190
+ Gem::Package::TarWriter.new(out_io) do |tar|
191
+ # First, copy all original entries
192
+ File.open(file_path, "rb") do |in_io|
193
+ Gem::Package::TarReader.new(in_io) do |reader|
194
+ reader.each do |entry|
195
+ # Skip existing signatures if re-signing
196
+ next if entry.full_name.end_with?(".sig")
197
+
198
+ tar.add_file_simple(entry.full_name, entry.header.mode, entry.size) do |io|
199
+ io.write(entry.read)
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ # Add signature files
206
+ @signatures.each do |name, sig_data|
207
+ tar.add_file_simple(name, 0o444, sig_data.bytesize) do |io|
208
+ io.write(sig_data)
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ # Move temp file to output location
215
+ FileUtils.mv(temp_file.path, output)
216
+ output
217
+ ensure
218
+ temp_file.close
219
+ temp_file.unlink if File.exist?(temp_file.path)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end