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,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module EasyCodeSign
|
|
6
|
+
module Verification
|
|
7
|
+
# Manages trusted root certificates for verification
|
|
8
|
+
#
|
|
9
|
+
# By default, uses the system's trusted CA certificates.
|
|
10
|
+
# Additional certificates can be added for custom PKI.
|
|
11
|
+
#
|
|
12
|
+
# @example Using system trust store
|
|
13
|
+
# store = TrustStore.new
|
|
14
|
+
# store.trusted?(certificate, chain)
|
|
15
|
+
#
|
|
16
|
+
# @example Adding custom trusted certificate
|
|
17
|
+
# store = TrustStore.new
|
|
18
|
+
# store.add_certificate(my_root_ca)
|
|
19
|
+
# store.add_file("/path/to/custom_ca.pem")
|
|
20
|
+
#
|
|
21
|
+
class TrustStore
|
|
22
|
+
attr_reader :store
|
|
23
|
+
|
|
24
|
+
def initialize(use_system_certs: true)
|
|
25
|
+
@store = OpenSSL::X509::Store.new
|
|
26
|
+
@store.set_default_paths if use_system_certs
|
|
27
|
+
@custom_certs = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Add a trusted certificate
|
|
31
|
+
# @param cert [OpenSSL::X509::Certificate]
|
|
32
|
+
# @return [self]
|
|
33
|
+
def add_certificate(cert)
|
|
34
|
+
@store.add_cert(cert)
|
|
35
|
+
@custom_certs << cert
|
|
36
|
+
self
|
|
37
|
+
rescue OpenSSL::X509::StoreError => e
|
|
38
|
+
# Certificate might already be in store
|
|
39
|
+
raise unless e.message.include?("cert already in hash table")
|
|
40
|
+
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Add certificates from a PEM file
|
|
45
|
+
# @param path [String] path to PEM file
|
|
46
|
+
# @return [self]
|
|
47
|
+
def add_file(path)
|
|
48
|
+
content = File.read(path)
|
|
49
|
+
certs = extract_certificates(content)
|
|
50
|
+
certs.each { |cert| add_certificate(cert) }
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Add certificates from a directory
|
|
55
|
+
# @param path [String] path to directory containing PEM files
|
|
56
|
+
# @return [self]
|
|
57
|
+
def add_directory(path)
|
|
58
|
+
Dir.glob(File.join(path, "*.pem")).each do |file|
|
|
59
|
+
add_file(file)
|
|
60
|
+
end
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if a certificate is trusted
|
|
65
|
+
# @param cert [OpenSSL::X509::Certificate] certificate to verify
|
|
66
|
+
# @param chain [Array<OpenSSL::X509::Certificate>] intermediate certificates
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def trusted?(cert, chain = [])
|
|
69
|
+
@store.verify(cert, chain)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Verify and return detailed error if not trusted
|
|
73
|
+
# @param cert [OpenSSL::X509::Certificate]
|
|
74
|
+
# @param chain [Array<OpenSSL::X509::Certificate>]
|
|
75
|
+
# @return [Hash] { trusted: Boolean, error: String|nil }
|
|
76
|
+
def verify(cert, chain = [])
|
|
77
|
+
if @store.verify(cert, chain)
|
|
78
|
+
{ trusted: true, error: nil }
|
|
79
|
+
else
|
|
80
|
+
{ trusted: false, error: @store.error_string }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get the verification error string from last verify call
|
|
85
|
+
# @return [String, nil]
|
|
86
|
+
def error_string
|
|
87
|
+
@store.error_string
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Set verification time (for testing signatures against past time)
|
|
91
|
+
# @param time [Time]
|
|
92
|
+
# @return [self]
|
|
93
|
+
def at_time(time)
|
|
94
|
+
@store.time = time
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Enable/disable CRL checking
|
|
99
|
+
# @param enabled [Boolean]
|
|
100
|
+
# @return [self]
|
|
101
|
+
def check_crl(enabled = true)
|
|
102
|
+
if enabled
|
|
103
|
+
@store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK |
|
|
104
|
+
OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
|
|
105
|
+
else
|
|
106
|
+
@store.flags = 0
|
|
107
|
+
end
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Add a CRL for revocation checking
|
|
112
|
+
# @param crl [OpenSSL::X509::CRL]
|
|
113
|
+
# @return [self]
|
|
114
|
+
def add_crl(crl)
|
|
115
|
+
@store.add_crl(crl)
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Load CRL from file
|
|
120
|
+
# @param path [String] path to CRL file (PEM or DER)
|
|
121
|
+
# @return [self]
|
|
122
|
+
def add_crl_file(path)
|
|
123
|
+
content = File.read(path)
|
|
124
|
+
crl = OpenSSL::X509::CRL.new(content)
|
|
125
|
+
add_crl(crl)
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def extract_certificates(pem_content)
|
|
132
|
+
certs = []
|
|
133
|
+
pem_content.scan(/-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m).each do |pem|
|
|
134
|
+
certs << OpenSSL::X509::Certificate.new(pem)
|
|
135
|
+
end
|
|
136
|
+
certs
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCodeSign
|
|
4
|
+
# Main verifier for signed files
|
|
5
|
+
#
|
|
6
|
+
# Orchestrates signature verification including:
|
|
7
|
+
# - Extracting signatures from files
|
|
8
|
+
# - Verifying cryptographic signatures
|
|
9
|
+
# - Validating certificate chains
|
|
10
|
+
# - Checking timestamps
|
|
11
|
+
#
|
|
12
|
+
# @example Verify a signed gem
|
|
13
|
+
# verifier = EasyCodeSign::Verifier.new
|
|
14
|
+
# result = verifier.verify("signed.gem")
|
|
15
|
+
# puts result.valid? ? "Valid!" : result.errors
|
|
16
|
+
#
|
|
17
|
+
# @example Verify with custom trust store
|
|
18
|
+
# trust_store = EasyCodeSign::Verification::TrustStore.new
|
|
19
|
+
# trust_store.add_file("/path/to/custom_ca.pem")
|
|
20
|
+
# verifier = EasyCodeSign::Verifier.new(trust_store: trust_store)
|
|
21
|
+
# result = verifier.verify("signed.gem")
|
|
22
|
+
#
|
|
23
|
+
class Verifier
|
|
24
|
+
attr_reader :trust_store, :configuration
|
|
25
|
+
|
|
26
|
+
def initialize(trust_store: nil, configuration: nil, check_revocation: nil)
|
|
27
|
+
@configuration = configuration || EasyCodeSign.configuration
|
|
28
|
+
@trust_store = trust_store || build_trust_store
|
|
29
|
+
@check_revocation = check_revocation.nil? ? @configuration.check_revocation : check_revocation
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Verify a signed file
|
|
33
|
+
#
|
|
34
|
+
# @param file_path [String] path to the signed file
|
|
35
|
+
# @param check_timestamp [Boolean] whether to verify timestamp
|
|
36
|
+
# @return [Verification::Result]
|
|
37
|
+
#
|
|
38
|
+
def verify(file_path, check_timestamp: true)
|
|
39
|
+
result = Verification::Result.new
|
|
40
|
+
result.file_path = file_path
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
# Determine file type and create appropriate handler
|
|
44
|
+
signable = create_signable(file_path)
|
|
45
|
+
result.file_type = signable.class.name.split("::").last.downcase.to_sym
|
|
46
|
+
|
|
47
|
+
# Extract signature from file
|
|
48
|
+
signature_data = signable.extract_signature
|
|
49
|
+
unless signature_data
|
|
50
|
+
result.add_error("File is not signed")
|
|
51
|
+
return result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Verify based on file type
|
|
55
|
+
case result.file_type
|
|
56
|
+
when :gemfile
|
|
57
|
+
verify_gem(signable, signature_data, result)
|
|
58
|
+
when :zipfile
|
|
59
|
+
verify_zip(signable, signature_data, result)
|
|
60
|
+
when :pdffile
|
|
61
|
+
verify_pdf(signable, signature_data, result)
|
|
62
|
+
else
|
|
63
|
+
result.add_error("Unsupported file type for verification")
|
|
64
|
+
return result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Verify timestamp if present and requested
|
|
68
|
+
if check_timestamp && result.timestamped
|
|
69
|
+
verify_timestamp_token(signature_data, result)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Determine overall validity
|
|
73
|
+
result.valid = result.signature_valid &&
|
|
74
|
+
result.integrity_valid &&
|
|
75
|
+
result.certificate_valid &&
|
|
76
|
+
result.chain_valid &&
|
|
77
|
+
result.trusted
|
|
78
|
+
|
|
79
|
+
rescue InvalidFileError => e
|
|
80
|
+
result.add_error("Invalid file: #{e.message}")
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
result.add_error("Verification error: #{e.message}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Verify multiple files
|
|
89
|
+
#
|
|
90
|
+
# @param file_paths [Array<String>]
|
|
91
|
+
# @return [Hash<String, Verification::Result>]
|
|
92
|
+
#
|
|
93
|
+
def verify_batch(file_paths)
|
|
94
|
+
file_paths.to_h { |path| [path, verify(path)] }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def build_trust_store
|
|
100
|
+
store = Verification::TrustStore.new(use_system_certs: true)
|
|
101
|
+
|
|
102
|
+
# Add custom trust store if configured
|
|
103
|
+
if @configuration.trust_store_path
|
|
104
|
+
if File.directory?(@configuration.trust_store_path)
|
|
105
|
+
store.add_directory(@configuration.trust_store_path)
|
|
106
|
+
elsif File.file?(@configuration.trust_store_path)
|
|
107
|
+
store.add_file(@configuration.trust_store_path)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
store
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create_signable(file_path)
|
|
115
|
+
extension = File.extname(file_path).downcase
|
|
116
|
+
|
|
117
|
+
case extension
|
|
118
|
+
when ".gem"
|
|
119
|
+
Signable::GemFile.new(file_path)
|
|
120
|
+
when ".zip", ".jar", ".apk", ".war", ".ear"
|
|
121
|
+
Signable::ZipFile.new(file_path)
|
|
122
|
+
when ".pdf"
|
|
123
|
+
Signable::PdfFile.new(file_path)
|
|
124
|
+
else
|
|
125
|
+
raise InvalidFileError, "Unsupported file type: #{extension}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def verify_gem(signable, signature_data, result)
|
|
130
|
+
# Gem signatures are in .sig files
|
|
131
|
+
signature_checker = Verification::SignatureChecker.new
|
|
132
|
+
|
|
133
|
+
# Get the original content and signature
|
|
134
|
+
signable.prepare_for_signing
|
|
135
|
+
content = signable.content_to_sign
|
|
136
|
+
|
|
137
|
+
# Find the primary signature file (checksums.yaml.gz.sig)
|
|
138
|
+
sig_file = signature_data["checksums.yaml.gz.sig"] || signature_data.values.first
|
|
139
|
+
|
|
140
|
+
unless sig_file
|
|
141
|
+
result.add_error("No signature found in gem")
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Verify PKCS#7 signature
|
|
146
|
+
sig_result = signature_checker.verify_detached_pkcs7(sig_file, content, trust_store)
|
|
147
|
+
|
|
148
|
+
result.signature_valid = sig_result.signature_valid
|
|
149
|
+
result.signer_certificate = sig_result.signer_certificate
|
|
150
|
+
result.certificate_chain = sig_result.certificates
|
|
151
|
+
result.signature_algorithm = sig_result.signature_algorithm
|
|
152
|
+
|
|
153
|
+
sig_result.errors.each { |e| result.add_error(e) }
|
|
154
|
+
|
|
155
|
+
if result.signer_certificate
|
|
156
|
+
extract_signer_info(result)
|
|
157
|
+
verify_certificate_chain(result)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Verify file integrity
|
|
161
|
+
verify_gem_integrity(signable, signature_data, result)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def verify_zip(signable, signature_data, result)
|
|
165
|
+
signature_checker = Verification::SignatureChecker.new
|
|
166
|
+
|
|
167
|
+
# ZIP uses JAR-style signatures in META-INF/
|
|
168
|
+
manifest = signature_data[:manifest]
|
|
169
|
+
signature_file = signature_data[:signature_file]
|
|
170
|
+
signature_block = signature_data[:signature_block]
|
|
171
|
+
|
|
172
|
+
unless manifest && signature_file && signature_block
|
|
173
|
+
result.add_error("Incomplete JAR signature (missing META-INF files)")
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Verify PKCS#7 signature over the .SF file
|
|
178
|
+
sig_result = signature_checker.verify_detached_pkcs7(signature_block, signature_file, trust_store)
|
|
179
|
+
|
|
180
|
+
result.signature_valid = sig_result.signature_valid
|
|
181
|
+
result.signer_certificate = sig_result.signer_certificate
|
|
182
|
+
result.certificate_chain = sig_result.certificates
|
|
183
|
+
result.signature_algorithm = sig_result.signature_algorithm
|
|
184
|
+
|
|
185
|
+
sig_result.errors.each { |e| result.add_error(e) }
|
|
186
|
+
|
|
187
|
+
if result.signer_certificate
|
|
188
|
+
extract_signer_info(result)
|
|
189
|
+
verify_certificate_chain(result)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Verify manifest integrity
|
|
193
|
+
verify_zip_manifest(signable, manifest, signature_file, result)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def verify_pdf(signable, signature_data, result)
|
|
197
|
+
signature_checker = Verification::SignatureChecker.new
|
|
198
|
+
|
|
199
|
+
# PDF signatures store PKCS#7 in /Contents and signed data via ByteRange
|
|
200
|
+
contents = signature_data[:contents]
|
|
201
|
+
byte_range = signature_data[:byte_range]
|
|
202
|
+
|
|
203
|
+
unless contents && byte_range
|
|
204
|
+
result.add_error("Invalid PDF signature (missing Contents or ByteRange)")
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Read the ByteRange content from the file
|
|
209
|
+
signed_content = read_byte_range_content(signable.file_path, byte_range)
|
|
210
|
+
|
|
211
|
+
# Verify PKCS#7 signature
|
|
212
|
+
sig_result = signature_checker.verify_pkcs7(contents, signed_content, trust_store)
|
|
213
|
+
|
|
214
|
+
result.signature_valid = sig_result.signature_valid
|
|
215
|
+
result.signer_certificate = sig_result.signer_certificate
|
|
216
|
+
result.certificate_chain = sig_result.certificates
|
|
217
|
+
result.signature_algorithm = sig_result.signature_algorithm
|
|
218
|
+
|
|
219
|
+
sig_result.errors.each { |e| result.add_error(e) }
|
|
220
|
+
|
|
221
|
+
if result.signer_certificate
|
|
222
|
+
extract_signer_info(result)
|
|
223
|
+
verify_certificate_chain(result)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Verify PDF integrity (ByteRange covers the document properly)
|
|
227
|
+
verify_pdf_integrity(signable, signature_data, result)
|
|
228
|
+
|
|
229
|
+
# Extract signer info from PDF signature dict
|
|
230
|
+
extract_pdf_signature_info(signature_data, result)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def read_byte_range_content(file_path, byte_range)
|
|
234
|
+
# ByteRange = [offset1, length1, offset2, length2]
|
|
235
|
+
# The signed content is: bytes[offset1..offset1+length1] + bytes[offset2..offset2+length2]
|
|
236
|
+
File.open(file_path, "rb") do |f|
|
|
237
|
+
f.seek(byte_range[0])
|
|
238
|
+
part1 = f.read(byte_range[1])
|
|
239
|
+
f.seek(byte_range[2])
|
|
240
|
+
part2 = f.read(byte_range[3])
|
|
241
|
+
part1 + part2
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def verify_pdf_integrity(signable, signature_data, result)
|
|
246
|
+
result.integrity_valid = true
|
|
247
|
+
|
|
248
|
+
byte_range = signature_data[:byte_range]
|
|
249
|
+
return unless byte_range
|
|
250
|
+
|
|
251
|
+
file_size = File.size(signable.file_path)
|
|
252
|
+
|
|
253
|
+
# Verify ByteRange makes sense:
|
|
254
|
+
# [0, before_sig_offset, after_sig_offset, rest_of_file]
|
|
255
|
+
# The end should match file size
|
|
256
|
+
expected_end = byte_range[2] + byte_range[3]
|
|
257
|
+
|
|
258
|
+
unless expected_end == file_size
|
|
259
|
+
result.integrity_valid = false
|
|
260
|
+
result.add_error("PDF ByteRange does not cover entire document (expected #{file_size}, got #{expected_end})")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Verify no gap exists (signature placeholder should be between the two ranges)
|
|
264
|
+
signature_start = byte_range[0] + byte_range[1]
|
|
265
|
+
signature_end = byte_range[2]
|
|
266
|
+
|
|
267
|
+
if signature_start > signature_end
|
|
268
|
+
result.integrity_valid = false
|
|
269
|
+
result.add_error("Invalid PDF ByteRange (overlapping ranges)")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def extract_pdf_signature_info(signature_data, result)
|
|
274
|
+
# Extract additional info from PDF signature dictionary
|
|
275
|
+
result.signature_reason = signature_data[:reason] if signature_data[:reason]
|
|
276
|
+
result.signature_location = signature_data[:location] if signature_data[:location]
|
|
277
|
+
|
|
278
|
+
# Check for timestamp
|
|
279
|
+
# This would require parsing the unsigned attributes of the PKCS#7
|
|
280
|
+
# For now, we don't extract embedded timestamps from PDF
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def verify_certificate_chain(result)
|
|
284
|
+
return unless result.signer_certificate
|
|
285
|
+
|
|
286
|
+
chain_validator = Verification::CertificateChain.new(
|
|
287
|
+
trust_store,
|
|
288
|
+
check_revocation: @check_revocation,
|
|
289
|
+
network_timeout: @configuration.network_timeout
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Use timestamp time if available for point-in-time verification
|
|
293
|
+
at_time = result.timestamped? ? result.timestamp : nil
|
|
294
|
+
|
|
295
|
+
chain_result = chain_validator.validate(
|
|
296
|
+
result.signer_certificate,
|
|
297
|
+
result.certificate_chain[1..] || [],
|
|
298
|
+
at_time: at_time
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
result.certificate_valid = chain_result.certificate_valid
|
|
302
|
+
result.chain_valid = chain_result.chain_valid
|
|
303
|
+
result.trusted = chain_result.trusted
|
|
304
|
+
result.not_revoked = chain_result.not_revoked
|
|
305
|
+
result.revocation_checked = chain_result.revocation_checked
|
|
306
|
+
|
|
307
|
+
chain_result.errors.each { |e| result.add_error(e) }
|
|
308
|
+
chain_result.warnings.each { |w| result.add_warning(w) }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def extract_signer_info(result)
|
|
312
|
+
cert = result.signer_certificate
|
|
313
|
+
return unless cert
|
|
314
|
+
|
|
315
|
+
subject = cert.subject
|
|
316
|
+
|
|
317
|
+
# Extract CN
|
|
318
|
+
cn = subject.to_a.find { |name, _, _| name == "CN" }
|
|
319
|
+
result.signer_name = cn ? cn[1] : subject.to_s
|
|
320
|
+
|
|
321
|
+
# Extract O
|
|
322
|
+
org = subject.to_a.find { |name, _, _| name == "O" }
|
|
323
|
+
result.signer_organization = org ? org[1] : nil
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def verify_gem_integrity(signable, signature_data, result)
|
|
327
|
+
# For gems, verify all signed files have valid signatures
|
|
328
|
+
# and content matches
|
|
329
|
+
result.integrity_valid = true
|
|
330
|
+
|
|
331
|
+
# The signature covers the gem contents hash
|
|
332
|
+
# If signature verification passed, integrity is verified
|
|
333
|
+
# Additional checks could verify individual file hashes
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def verify_zip_manifest(signable, manifest, signature_file, result)
|
|
337
|
+
result.integrity_valid = true
|
|
338
|
+
|
|
339
|
+
# Parse manifest and verify file hashes
|
|
340
|
+
begin
|
|
341
|
+
# Verify that .SF file hash matches manifest
|
|
342
|
+
sf_manifest_digest = extract_sf_manifest_digest(signature_file)
|
|
343
|
+
actual_manifest_hash = compute_manifest_hash(manifest)
|
|
344
|
+
|
|
345
|
+
unless sf_manifest_digest == actual_manifest_hash
|
|
346
|
+
result.integrity_valid = false
|
|
347
|
+
result.add_error("Manifest digest mismatch")
|
|
348
|
+
return
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Verify individual file hashes from manifest
|
|
352
|
+
verify_zip_file_hashes(signable, manifest, result)
|
|
353
|
+
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
result.integrity_valid = false
|
|
356
|
+
result.add_error("Manifest verification failed: #{e.message}")
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def extract_sf_manifest_digest(signature_file)
|
|
361
|
+
# Look for SHA-256-Digest-Manifest or similar
|
|
362
|
+
match = signature_file.match(/SHA-256-Digest-Manifest:\s*(\S+)/) ||
|
|
363
|
+
signature_file.match(/SHA-384-Digest-Manifest:\s*(\S+)/) ||
|
|
364
|
+
signature_file.match(/SHA-512-Digest-Manifest:\s*(\S+)/)
|
|
365
|
+
|
|
366
|
+
match ? match[1] : nil
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def compute_manifest_hash(manifest)
|
|
370
|
+
require "base64"
|
|
371
|
+
Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(manifest))
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def verify_zip_file_hashes(signable, manifest, result)
|
|
375
|
+
# Parse manifest entries
|
|
376
|
+
entries = parse_manifest(manifest)
|
|
377
|
+
|
|
378
|
+
::Zip::File.open(signable.file_path) do |zip|
|
|
379
|
+
entries.each do |name, expected_hash|
|
|
380
|
+
entry = zip.find_entry(name)
|
|
381
|
+
unless entry
|
|
382
|
+
result.add_warning("File in manifest not found in ZIP: #{name}")
|
|
383
|
+
next
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
actual_hash = compute_file_hash(entry.get_input_stream.read)
|
|
387
|
+
unless actual_hash == expected_hash
|
|
388
|
+
result.integrity_valid = false
|
|
389
|
+
result.add_error("Hash mismatch for file: #{name}")
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def parse_manifest(manifest)
|
|
396
|
+
entries = {}
|
|
397
|
+
current_name = nil
|
|
398
|
+
|
|
399
|
+
manifest.each_line do |line|
|
|
400
|
+
line = line.strip
|
|
401
|
+
if line.start_with?("Name: ")
|
|
402
|
+
current_name = line.sub("Name: ", "")
|
|
403
|
+
elsif current_name && line.match?(/SHA-\d+-Digest:/)
|
|
404
|
+
hash = line.split(": ", 2).last
|
|
405
|
+
entries[current_name] = hash
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
entries
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def compute_file_hash(content)
|
|
413
|
+
require "base64"
|
|
414
|
+
Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(content))
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def verify_timestamp_token(signature_data, result)
|
|
418
|
+
# Look for embedded timestamp in PKCS#7
|
|
419
|
+
# This would require parsing the unsigned attributes
|
|
420
|
+
# For now, mark as not verified if timestamp detection is needed
|
|
421
|
+
|
|
422
|
+
result.timestamp_valid = false
|
|
423
|
+
result.add_warning("Timestamp verification not yet implemented")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|