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,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module EasyCodeSign
|
|
8
|
+
module Verification
|
|
9
|
+
# Validates certificate chains and checks revocation status
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# validator = CertificateChain.new(trust_store)
|
|
13
|
+
# result = validator.validate(cert, intermediates)
|
|
14
|
+
#
|
|
15
|
+
class CertificateChain
|
|
16
|
+
attr_reader :trust_store
|
|
17
|
+
|
|
18
|
+
def initialize(trust_store, check_revocation: false, network_timeout: 10)
|
|
19
|
+
@trust_store = trust_store
|
|
20
|
+
@check_revocation = check_revocation
|
|
21
|
+
@network_timeout = network_timeout
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Validate a certificate chain
|
|
25
|
+
#
|
|
26
|
+
# @param certificate [OpenSSL::X509::Certificate] end-entity certificate
|
|
27
|
+
# @param intermediates [Array<OpenSSL::X509::Certificate>] intermediate certs
|
|
28
|
+
# @param at_time [Time, nil] verify at specific time (nil = now)
|
|
29
|
+
# @return [ChainValidationResult]
|
|
30
|
+
#
|
|
31
|
+
def validate(certificate, intermediates = [], at_time: nil)
|
|
32
|
+
result = ChainValidationResult.new
|
|
33
|
+
result.certificate = certificate
|
|
34
|
+
|
|
35
|
+
# Set verification time if specified
|
|
36
|
+
if at_time
|
|
37
|
+
trust_store.at_time(at_time)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Build the chain
|
|
41
|
+
chain = build_chain(certificate, intermediates)
|
|
42
|
+
result.chain = chain
|
|
43
|
+
|
|
44
|
+
# Verify basic certificate validity
|
|
45
|
+
validate_certificate(certificate, result, at_time)
|
|
46
|
+
|
|
47
|
+
# Verify chain to trusted root
|
|
48
|
+
validate_chain_trust(certificate, chain, result)
|
|
49
|
+
|
|
50
|
+
# Check revocation if enabled
|
|
51
|
+
if @check_revocation && result.chain_valid
|
|
52
|
+
check_revocation_status(certificate, chain, result)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check code signing extended key usage
|
|
56
|
+
check_key_usage(certificate, result)
|
|
57
|
+
|
|
58
|
+
result.valid = result.certificate_valid && result.chain_valid &&
|
|
59
|
+
result.trusted && result.not_revoked
|
|
60
|
+
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def build_chain(leaf, intermediates)
|
|
67
|
+
chain = [leaf]
|
|
68
|
+
current = leaf
|
|
69
|
+
remaining = intermediates.dup
|
|
70
|
+
|
|
71
|
+
# Build chain by matching issuer -> subject
|
|
72
|
+
loop do
|
|
73
|
+
issuer = remaining.find { |c| c.subject.to_s == current.issuer.to_s }
|
|
74
|
+
break unless issuer
|
|
75
|
+
|
|
76
|
+
chain << issuer
|
|
77
|
+
remaining.delete(issuer)
|
|
78
|
+
current = issuer
|
|
79
|
+
|
|
80
|
+
# Stop if self-signed (root)
|
|
81
|
+
break if current.subject.to_s == current.issuer.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
chain
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_certificate(cert, result, at_time)
|
|
88
|
+
check_time = at_time || Time.now
|
|
89
|
+
|
|
90
|
+
# Check not before
|
|
91
|
+
if check_time < cert.not_before
|
|
92
|
+
result.add_error("Certificate not yet valid (starts #{cert.not_before})")
|
|
93
|
+
result.certificate_valid = false
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check not after
|
|
98
|
+
if check_time > cert.not_after
|
|
99
|
+
result.add_error("Certificate expired (#{cert.not_after})")
|
|
100
|
+
result.expired = true
|
|
101
|
+
result.certificate_valid = false
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
result.certificate_valid = true
|
|
106
|
+
result.expires_at = cert.not_after
|
|
107
|
+
|
|
108
|
+
# Warn if expiring soon (30 days)
|
|
109
|
+
days_until_expiry = (cert.not_after - Time.now) / 86_400
|
|
110
|
+
if days_until_expiry < 30 && days_until_expiry > 0
|
|
111
|
+
result.add_warning("Certificate expires in #{days_until_expiry.to_i} days")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_chain_trust(cert, chain, result)
|
|
116
|
+
# Remove leaf cert from chain for verification
|
|
117
|
+
intermediates = chain[1..] || []
|
|
118
|
+
|
|
119
|
+
verification = trust_store.verify(cert, intermediates)
|
|
120
|
+
|
|
121
|
+
if verification[:trusted]
|
|
122
|
+
result.chain_valid = true
|
|
123
|
+
result.trusted = true
|
|
124
|
+
else
|
|
125
|
+
result.chain_valid = false
|
|
126
|
+
result.trusted = false
|
|
127
|
+
result.add_error("Certificate chain validation failed: #{verification[:error]}")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def check_revocation_status(cert, chain, result)
|
|
132
|
+
result.revocation_checked = true
|
|
133
|
+
|
|
134
|
+
# Try OCSP first (faster, real-time)
|
|
135
|
+
ocsp_result = check_ocsp(cert, chain)
|
|
136
|
+
if ocsp_result
|
|
137
|
+
if ocsp_result[:revoked]
|
|
138
|
+
result.not_revoked = false
|
|
139
|
+
result.add_error("Certificate has been revoked (OCSP)")
|
|
140
|
+
else
|
|
141
|
+
result.not_revoked = true
|
|
142
|
+
end
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Fall back to CRL if OCSP unavailable
|
|
147
|
+
crl_result = check_crl(cert, chain)
|
|
148
|
+
if crl_result
|
|
149
|
+
if crl_result[:revoked]
|
|
150
|
+
result.not_revoked = false
|
|
151
|
+
result.add_error("Certificate has been revoked (CRL)")
|
|
152
|
+
else
|
|
153
|
+
result.not_revoked = true
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
result.add_warning("Could not check revocation status")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def check_ocsp(cert, chain)
|
|
161
|
+
# Find OCSP responder URL from certificate
|
|
162
|
+
ocsp_uri = extract_ocsp_uri(cert)
|
|
163
|
+
return nil unless ocsp_uri
|
|
164
|
+
|
|
165
|
+
issuer = chain[1] # Issuer is second in chain
|
|
166
|
+
return nil unless issuer
|
|
167
|
+
|
|
168
|
+
begin
|
|
169
|
+
# Build OCSP request
|
|
170
|
+
digest = OpenSSL::Digest.new("SHA256")
|
|
171
|
+
cert_id = OpenSSL::OCSP::CertificateId.new(cert, issuer, digest)
|
|
172
|
+
request = OpenSSL::OCSP::Request.new
|
|
173
|
+
request.add_certid(cert_id)
|
|
174
|
+
request.add_nonce
|
|
175
|
+
|
|
176
|
+
# Send request
|
|
177
|
+
uri = URI.parse(ocsp_uri)
|
|
178
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
179
|
+
http.use_ssl = (uri.scheme == "https")
|
|
180
|
+
http.open_timeout = @network_timeout
|
|
181
|
+
http.read_timeout = @network_timeout
|
|
182
|
+
|
|
183
|
+
http_request = Net::HTTP::Post.new(uri.path)
|
|
184
|
+
http_request["Content-Type"] = "application/ocsp-request"
|
|
185
|
+
http_request.body = request.to_der
|
|
186
|
+
|
|
187
|
+
response = http.request(http_request)
|
|
188
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
189
|
+
|
|
190
|
+
ocsp_response = OpenSSL::OCSP::Response.new(response.body)
|
|
191
|
+
return nil unless ocsp_response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
|
|
192
|
+
|
|
193
|
+
basic = ocsp_response.basic
|
|
194
|
+
return nil unless basic
|
|
195
|
+
|
|
196
|
+
# Check certificate status
|
|
197
|
+
status, = basic.status.find { |s| s[0].cmp(cert_id) }
|
|
198
|
+
return nil unless status
|
|
199
|
+
|
|
200
|
+
{ revoked: status[1] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED }
|
|
201
|
+
rescue StandardError
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def check_crl(cert, chain)
|
|
207
|
+
crl_uri = extract_crl_uri(cert)
|
|
208
|
+
return nil unless crl_uri
|
|
209
|
+
|
|
210
|
+
begin
|
|
211
|
+
uri = URI.parse(crl_uri)
|
|
212
|
+
response = Net::HTTP.get_response(uri)
|
|
213
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
214
|
+
|
|
215
|
+
crl = OpenSSL::X509::CRL.new(response.body)
|
|
216
|
+
|
|
217
|
+
# Check if certificate is in CRL
|
|
218
|
+
revoked = crl.revoked.any? { |r| r.serial == cert.serial }
|
|
219
|
+
{ revoked: revoked }
|
|
220
|
+
rescue StandardError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def extract_ocsp_uri(cert)
|
|
226
|
+
aia = cert.extensions.find { |e| e.oid == "authorityInfoAccess" }
|
|
227
|
+
return nil unless aia
|
|
228
|
+
|
|
229
|
+
match = aia.value.match(/OCSP - URI:(\S+)/)
|
|
230
|
+
match ? match[1] : nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def extract_crl_uri(cert)
|
|
234
|
+
cdp = cert.extensions.find { |e| e.oid == "crlDistributionPoints" }
|
|
235
|
+
return nil unless cdp
|
|
236
|
+
|
|
237
|
+
match = cdp.value.match(/URI:(\S+)/)
|
|
238
|
+
match ? match[1] : nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def check_key_usage(cert, result)
|
|
242
|
+
# Check for code signing EKU
|
|
243
|
+
eku = cert.extensions.find { |e| e.oid == "extendedKeyUsage" }
|
|
244
|
+
|
|
245
|
+
if eku
|
|
246
|
+
unless eku.value.include?("Code Signing")
|
|
247
|
+
result.add_warning("Certificate does not have Code Signing extended key usage")
|
|
248
|
+
end
|
|
249
|
+
else
|
|
250
|
+
result.add_warning("Certificate has no extended key usage extension")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Result of certificate chain validation
|
|
256
|
+
class ChainValidationResult
|
|
257
|
+
attr_accessor :valid, :certificate, :chain,
|
|
258
|
+
:certificate_valid, :chain_valid, :trusted,
|
|
259
|
+
:not_revoked, :revocation_checked, :expired,
|
|
260
|
+
:expires_at, :errors, :warnings
|
|
261
|
+
|
|
262
|
+
def initialize
|
|
263
|
+
@valid = false
|
|
264
|
+
@certificate_valid = false
|
|
265
|
+
@chain_valid = false
|
|
266
|
+
@trusted = false
|
|
267
|
+
@not_revoked = true
|
|
268
|
+
@revocation_checked = false
|
|
269
|
+
@expired = false
|
|
270
|
+
@errors = []
|
|
271
|
+
@warnings = []
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def add_error(msg)
|
|
275
|
+
@errors << msg
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def add_warning(msg)
|
|
279
|
+
@warnings << msg
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def to_h
|
|
283
|
+
{
|
|
284
|
+
valid: valid,
|
|
285
|
+
certificate_valid: certificate_valid,
|
|
286
|
+
chain_valid: chain_valid,
|
|
287
|
+
trusted: trusted,
|
|
288
|
+
not_revoked: not_revoked,
|
|
289
|
+
revocation_checked: revocation_checked,
|
|
290
|
+
expired: expired,
|
|
291
|
+
expires_at: expires_at&.iso8601,
|
|
292
|
+
errors: errors,
|
|
293
|
+
warnings: warnings
|
|
294
|
+
}
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyCodeSign
|
|
4
|
+
module Verification
|
|
5
|
+
# Comprehensive result of signature verification
|
|
6
|
+
#
|
|
7
|
+
# Provides detailed information about each aspect of verification:
|
|
8
|
+
# - Signature validity (cryptographic check)
|
|
9
|
+
# - Certificate chain validity
|
|
10
|
+
# - Trust status (chains to trusted root)
|
|
11
|
+
# - Timestamp validity (if present)
|
|
12
|
+
# - File integrity (content hasn't been modified)
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# result = EasyCodeSign.verify("signed.gem")
|
|
16
|
+
# if result.valid?
|
|
17
|
+
# puts "Signed by: #{result.signer_name}"
|
|
18
|
+
# puts "Signed at: #{result.timestamp}" if result.timestamped?
|
|
19
|
+
# else
|
|
20
|
+
# puts "Verification failed:"
|
|
21
|
+
# result.errors.each { |e| puts " - #{e}" }
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class Result
|
|
25
|
+
# Overall verification status
|
|
26
|
+
attr_accessor :valid
|
|
27
|
+
|
|
28
|
+
# Individual check results
|
|
29
|
+
attr_accessor :signature_valid, # Cryptographic signature is valid
|
|
30
|
+
:integrity_valid, # File content matches signature
|
|
31
|
+
:certificate_valid, # Certificate is valid (not expired, etc.)
|
|
32
|
+
:chain_valid, # Certificate chain is complete
|
|
33
|
+
:trusted, # Chain leads to trusted root
|
|
34
|
+
:timestamp_valid, # Timestamp is valid (if present)
|
|
35
|
+
:not_revoked # Certificate not revoked (if checked)
|
|
36
|
+
|
|
37
|
+
# Signer information
|
|
38
|
+
attr_accessor :signer_certificate, # The signing certificate
|
|
39
|
+
:certificate_chain, # Full chain from signer to root
|
|
40
|
+
:signer_name, # CN from certificate
|
|
41
|
+
:signer_organization # O from certificate
|
|
42
|
+
|
|
43
|
+
# Timestamp information
|
|
44
|
+
attr_accessor :timestamped, # Whether timestamp is present
|
|
45
|
+
:timestamp, # Time from timestamp token
|
|
46
|
+
:timestamp_authority # TSA name/URL
|
|
47
|
+
|
|
48
|
+
# Detailed messages
|
|
49
|
+
attr_accessor :errors, # Array of error messages
|
|
50
|
+
:warnings # Array of warning messages
|
|
51
|
+
|
|
52
|
+
# File information
|
|
53
|
+
attr_accessor :file_path,
|
|
54
|
+
:file_type, # :gem, :zip, etc.
|
|
55
|
+
:signature_algorithm
|
|
56
|
+
|
|
57
|
+
def initialize
|
|
58
|
+
@valid = false
|
|
59
|
+
@signature_valid = false
|
|
60
|
+
@integrity_valid = false
|
|
61
|
+
@certificate_valid = false
|
|
62
|
+
@chain_valid = false
|
|
63
|
+
@trusted = false
|
|
64
|
+
@timestamp_valid = false
|
|
65
|
+
@not_revoked = true # Assume not revoked unless checked
|
|
66
|
+
@timestamped = false
|
|
67
|
+
@errors = []
|
|
68
|
+
@warnings = []
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Overall validity check
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def valid?
|
|
74
|
+
valid
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check if signature is cryptographically valid
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def signature_valid?
|
|
80
|
+
signature_valid
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if file integrity is intact
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def integrity_valid?
|
|
86
|
+
integrity_valid
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if certificate is valid
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def certificate_valid?
|
|
92
|
+
certificate_valid
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if certificate chain is valid
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def chain_valid?
|
|
98
|
+
chain_valid
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check if signing certificate is trusted
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def trusted?
|
|
104
|
+
trusted
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if timestamp is present
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def timestamped?
|
|
110
|
+
timestamped
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if timestamp is valid
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def timestamp_valid?
|
|
116
|
+
timestamp_valid
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if certificate revocation was verified
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def revocation_checked?
|
|
122
|
+
@revocation_checked || false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
attr_writer :revocation_checked
|
|
126
|
+
|
|
127
|
+
# Get certificate expiration date
|
|
128
|
+
# @return [Time, nil]
|
|
129
|
+
def certificate_expires_at
|
|
130
|
+
signer_certificate&.not_after
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if certificate is currently expired
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def certificate_expired?
|
|
136
|
+
return false unless signer_certificate
|
|
137
|
+
|
|
138
|
+
signer_certificate.not_after < Time.now
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Check if certificate was valid at signing time (requires timestamp)
|
|
142
|
+
# @return [Boolean, nil] nil if no timestamp
|
|
143
|
+
def certificate_valid_at_signing?
|
|
144
|
+
return nil unless timestamped? && timestamp && signer_certificate
|
|
145
|
+
|
|
146
|
+
timestamp >= signer_certificate.not_before &&
|
|
147
|
+
timestamp <= signer_certificate.not_after
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Add an error message
|
|
151
|
+
# @param message [String]
|
|
152
|
+
def add_error(message)
|
|
153
|
+
errors << message
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Add a warning message
|
|
157
|
+
# @param message [String]
|
|
158
|
+
def add_warning(message)
|
|
159
|
+
warnings << message
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Convert to hash for serialization
|
|
163
|
+
# @return [Hash]
|
|
164
|
+
def to_h
|
|
165
|
+
{
|
|
166
|
+
valid: valid,
|
|
167
|
+
file_path: file_path,
|
|
168
|
+
file_type: file_type,
|
|
169
|
+
checks: {
|
|
170
|
+
signature_valid: signature_valid,
|
|
171
|
+
integrity_valid: integrity_valid,
|
|
172
|
+
certificate_valid: certificate_valid,
|
|
173
|
+
chain_valid: chain_valid,
|
|
174
|
+
trusted: trusted,
|
|
175
|
+
timestamp_valid: timestamp_valid,
|
|
176
|
+
not_revoked: not_revoked
|
|
177
|
+
},
|
|
178
|
+
signer: {
|
|
179
|
+
name: signer_name,
|
|
180
|
+
organization: signer_organization,
|
|
181
|
+
certificate_expires: certificate_expires_at&.iso8601
|
|
182
|
+
},
|
|
183
|
+
timestamp: timestamped ? {
|
|
184
|
+
time: timestamp&.iso8601,
|
|
185
|
+
authority: timestamp_authority,
|
|
186
|
+
valid: timestamp_valid
|
|
187
|
+
} : nil,
|
|
188
|
+
errors: errors,
|
|
189
|
+
warnings: warnings
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Human-readable summary
|
|
194
|
+
# @return [String]
|
|
195
|
+
def summary
|
|
196
|
+
status = valid? ? "VALID" : "INVALID"
|
|
197
|
+
lines = ["Signature: #{status}"]
|
|
198
|
+
|
|
199
|
+
if signer_name
|
|
200
|
+
lines << "Signer: #{signer_name}"
|
|
201
|
+
lines << "Organization: #{signer_organization}" if signer_organization
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if timestamped?
|
|
205
|
+
lines << "Timestamp: #{timestamp&.iso8601} (#{timestamp_valid? ? 'valid' : 'invalid'})"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
unless errors.empty?
|
|
209
|
+
lines << "Errors:"
|
|
210
|
+
errors.each { |e| lines << " - #{e}" }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
unless warnings.empty?
|
|
214
|
+
lines << "Warnings:"
|
|
215
|
+
warnings.each { |w| lines << " - #{w}" }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
lines.join("\n")
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module EasyCodeSign
|
|
6
|
+
module Verification
|
|
7
|
+
# Performs cryptographic signature verification
|
|
8
|
+
#
|
|
9
|
+
# Handles PKCS#7 signature verification for both gem and ZIP files.
|
|
10
|
+
#
|
|
11
|
+
class SignatureChecker
|
|
12
|
+
# Verify a PKCS#7 signature
|
|
13
|
+
#
|
|
14
|
+
# @param pkcs7_der [String] DER-encoded PKCS#7 signature
|
|
15
|
+
# @param content [String] the signed content
|
|
16
|
+
# @param trust_store [TrustStore] trust store for verification
|
|
17
|
+
# @return [SignatureCheckResult]
|
|
18
|
+
#
|
|
19
|
+
def verify_pkcs7(pkcs7_der, content, trust_store)
|
|
20
|
+
result = SignatureCheckResult.new
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
pkcs7 = OpenSSL::PKCS7.new(pkcs7_der)
|
|
24
|
+
result.signature_parsed = true
|
|
25
|
+
|
|
26
|
+
# Extract certificates
|
|
27
|
+
result.certificates = pkcs7.certificates || []
|
|
28
|
+
result.signer_certificate = result.certificates.first
|
|
29
|
+
|
|
30
|
+
# Verify the signature
|
|
31
|
+
# NOVERIFY flag skips certificate chain verification (we do that separately)
|
|
32
|
+
flags = OpenSSL::PKCS7::NOVERIFY
|
|
33
|
+
|
|
34
|
+
if pkcs7.verify(result.certificates, trust_store.store, content, flags)
|
|
35
|
+
result.signature_valid = true
|
|
36
|
+
else
|
|
37
|
+
result.signature_valid = false
|
|
38
|
+
result.add_error("PKCS#7 signature verification failed")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Extract signature algorithm info
|
|
42
|
+
extract_signature_info(pkcs7, result)
|
|
43
|
+
|
|
44
|
+
rescue OpenSSL::PKCS7::PKCS7Error => e
|
|
45
|
+
result.signature_valid = false
|
|
46
|
+
result.add_error("PKCS#7 error: #{e.message}")
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
result.signature_valid = false
|
|
49
|
+
result.add_error("Signature verification error: #{e.message}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Verify a detached PKCS#7 signature (signature separate from content)
|
|
56
|
+
#
|
|
57
|
+
# @param pkcs7_der [String] DER-encoded PKCS#7 signature
|
|
58
|
+
# @param content [String] the original content that was signed
|
|
59
|
+
# @param trust_store [TrustStore] trust store for verification
|
|
60
|
+
# @return [SignatureCheckResult]
|
|
61
|
+
#
|
|
62
|
+
def verify_detached_pkcs7(pkcs7_der, content, trust_store)
|
|
63
|
+
result = SignatureCheckResult.new
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
pkcs7 = OpenSSL::PKCS7.new(pkcs7_der)
|
|
67
|
+
result.signature_parsed = true
|
|
68
|
+
|
|
69
|
+
result.certificates = pkcs7.certificates || []
|
|
70
|
+
result.signer_certificate = result.certificates.first
|
|
71
|
+
|
|
72
|
+
# For detached signatures, we need to provide the content
|
|
73
|
+
flags = OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::DETACHED
|
|
74
|
+
|
|
75
|
+
store = trust_store.store
|
|
76
|
+
|
|
77
|
+
if pkcs7.verify(result.certificates, store, content, flags)
|
|
78
|
+
result.signature_valid = true
|
|
79
|
+
else
|
|
80
|
+
result.signature_valid = false
|
|
81
|
+
result.add_error("Detached PKCS#7 signature verification failed")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
extract_signature_info(pkcs7, result)
|
|
85
|
+
|
|
86
|
+
rescue OpenSSL::PKCS7::PKCS7Error => e
|
|
87
|
+
result.signature_valid = false
|
|
88
|
+
result.add_error("PKCS#7 error: #{e.message}")
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
result.signature_valid = false
|
|
91
|
+
result.add_error("Signature verification error: #{e.message}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Verify a raw signature (not PKCS#7 wrapped)
|
|
98
|
+
#
|
|
99
|
+
# @param signature [String] raw signature bytes
|
|
100
|
+
# @param content [String] the signed content
|
|
101
|
+
# @param certificate [OpenSSL::X509::Certificate] signer's certificate
|
|
102
|
+
# @param algorithm [Symbol] signature algorithm used
|
|
103
|
+
# @return [SignatureCheckResult]
|
|
104
|
+
#
|
|
105
|
+
def verify_raw(signature, content, certificate, algorithm: :sha256)
|
|
106
|
+
result = SignatureCheckResult.new
|
|
107
|
+
result.signer_certificate = certificate
|
|
108
|
+
result.certificates = [certificate]
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
public_key = certificate.public_key
|
|
112
|
+
digest = digest_for_algorithm(algorithm)
|
|
113
|
+
|
|
114
|
+
if public_key.verify(digest, signature, content)
|
|
115
|
+
result.signature_valid = true
|
|
116
|
+
else
|
|
117
|
+
result.signature_valid = false
|
|
118
|
+
result.add_error("Raw signature verification failed")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
result.signature_algorithm = algorithm
|
|
122
|
+
|
|
123
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
124
|
+
result.signature_valid = false
|
|
125
|
+
result.add_error("Public key error: #{e.message}")
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
result.signature_valid = false
|
|
128
|
+
result.add_error("Signature verification error: #{e.message}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
result
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def extract_signature_info(pkcs7, result)
|
|
137
|
+
# Try to extract algorithm info from signers
|
|
138
|
+
signers = pkcs7.signers rescue []
|
|
139
|
+
if signers.any?
|
|
140
|
+
signer = signers.first
|
|
141
|
+
result.signature_algorithm = signer.digest_algorithm.name rescue nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def digest_for_algorithm(algorithm)
|
|
146
|
+
case algorithm
|
|
147
|
+
when :sha256, :sha256_rsa, :sha256_ecdsa
|
|
148
|
+
OpenSSL::Digest::SHA256.new
|
|
149
|
+
when :sha384, :sha384_rsa, :sha384_ecdsa
|
|
150
|
+
OpenSSL::Digest::SHA384.new
|
|
151
|
+
when :sha512, :sha512_rsa, :sha512_ecdsa
|
|
152
|
+
OpenSSL::Digest::SHA512.new
|
|
153
|
+
else
|
|
154
|
+
OpenSSL::Digest::SHA256.new
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Result of signature verification
|
|
160
|
+
class SignatureCheckResult
|
|
161
|
+
attr_accessor :signature_valid, :signature_parsed,
|
|
162
|
+
:signer_certificate, :certificates,
|
|
163
|
+
:signature_algorithm, :errors
|
|
164
|
+
|
|
165
|
+
def initialize
|
|
166
|
+
@signature_valid = false
|
|
167
|
+
@signature_parsed = false
|
|
168
|
+
@certificates = []
|
|
169
|
+
@errors = []
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def valid?
|
|
173
|
+
signature_valid
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def add_error(msg)
|
|
177
|
+
@errors << msg
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def signer_name
|
|
181
|
+
signer_certificate&.subject&.to_s
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def to_h
|
|
185
|
+
{
|
|
186
|
+
valid: signature_valid,
|
|
187
|
+
parsed: signature_parsed,
|
|
188
|
+
algorithm: signature_algorithm,
|
|
189
|
+
signer: signer_name,
|
|
190
|
+
certificate_count: certificates.size,
|
|
191
|
+
errors: errors
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|