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,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