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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +95 -0
- data/LICENSE +21 -0
- data/README.md +331 -0
- data/Rakefile +16 -0
- data/exe/easysign +7 -0
- data/lib/easy_code_sign/cli.rb +428 -0
- data/lib/easy_code_sign/configuration.rb +102 -0
- data/lib/easy_code_sign/deferred_signing_request.rb +104 -0
- data/lib/easy_code_sign/errors.rb +113 -0
- data/lib/easy_code_sign/pdf/appearance_builder.rb +104 -0
- data/lib/easy_code_sign/pdf/timestamp_handler.rb +31 -0
- data/lib/easy_code_sign/providers/base.rb +126 -0
- data/lib/easy_code_sign/providers/pkcs11_base.rb +197 -0
- data/lib/easy_code_sign/providers/safenet.rb +109 -0
- data/lib/easy_code_sign/signable/base.rb +98 -0
- data/lib/easy_code_sign/signable/gem_file.rb +224 -0
- data/lib/easy_code_sign/signable/pdf_file.rb +486 -0
- data/lib/easy_code_sign/signable/zip_file.rb +226 -0
- data/lib/easy_code_sign/signer.rb +254 -0
- data/lib/easy_code_sign/timestamp/client.rb +184 -0
- data/lib/easy_code_sign/timestamp/request.rb +114 -0
- data/lib/easy_code_sign/timestamp/response.rb +246 -0
- data/lib/easy_code_sign/timestamp/verifier.rb +227 -0
- data/lib/easy_code_sign/verification/certificate_chain.rb +298 -0
- data/lib/easy_code_sign/verification/result.rb +222 -0
- data/lib/easy_code_sign/verification/signature_checker.rb +196 -0
- data/lib/easy_code_sign/verification/trust_store.rb +140 -0
- data/lib/easy_code_sign/verifier.rb +426 -0
- data/lib/easy_code_sign/version.rb +5 -0
- data/lib/easy_code_sign.rb +183 -0
- data/plugin/.gitignore +21 -0
- data/plugin/Gemfile +24 -0
- data/plugin/Gemfile.lock +134 -0
- data/plugin/README.md +248 -0
- data/plugin/Rakefile +121 -0
- data/plugin/docs/API_REFERENCE.md +366 -0
- data/plugin/docs/DEVELOPMENT.md +522 -0
- data/plugin/docs/INSTALLATION.md +204 -0
- data/plugin/native_host/build/Rakefile +90 -0
- data/plugin/native_host/install/com.easysign.host.json +9 -0
- data/plugin/native_host/install/install_chrome.sh +81 -0
- data/plugin/native_host/install/install_firefox.sh +81 -0
- data/plugin/native_host/src/easy_sign_host.rb +158 -0
- data/plugin/native_host/src/protocol.rb +101 -0
- data/plugin/native_host/src/signing_service.rb +167 -0
- data/plugin/native_host/test/native_host_test.rb +113 -0
- data/plugin/src/easy_sign/background.rb +323 -0
- data/plugin/src/easy_sign/content.rb +74 -0
- data/plugin/src/easy_sign/inject.rb +239 -0
- data/plugin/src/easy_sign/messaging.rb +109 -0
- data/plugin/src/easy_sign/popup.rb +200 -0
- data/plugin/templates/manifest.json +58 -0
- data/plugin/templates/popup.css +223 -0
- data/plugin/templates/popup.html +59 -0
- data/sig/easy_code_sign.rbs +4 -0
- data/test/easy_code_sign_test.rb +122 -0
- data/test/pdf_signable_test.rb +569 -0
- data/test/signable_test.rb +334 -0
- data/test/test_helper.rb +18 -0
- data/test/timestamp_test.rb +163 -0
- data/test/verification_test.rb +350 -0
- 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
|