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,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module EasyCodeSign
6
+ module Timestamp
7
+ # RFC 3161 Timestamp Response parser
8
+ #
9
+ # Parses TimeStampResp ASN.1 structures returned by TSAs.
10
+ #
11
+ # @example
12
+ # response = Timestamp::Response.parse(der_bytes)
13
+ # if response.success?
14
+ # puts response.timestamp
15
+ # puts response.serial_number
16
+ # end
17
+ #
18
+ class Response
19
+ # PKIStatus values
20
+ STATUS_GRANTED = 0
21
+ STATUS_GRANTED_WITH_MODS = 1
22
+ STATUS_REJECTION = 2
23
+ STATUS_WAITING = 3
24
+ STATUS_REVOCATION_WARNING = 4
25
+ STATUS_REVOCATION_NOTIFICATION = 5
26
+
27
+ STATUS_NAMES = {
28
+ STATUS_GRANTED => "granted",
29
+ STATUS_GRANTED_WITH_MODS => "granted_with_mods",
30
+ STATUS_REJECTION => "rejection",
31
+ STATUS_WAITING => "waiting",
32
+ STATUS_REVOCATION_WARNING => "revocation_warning",
33
+ STATUS_REVOCATION_NOTIFICATION => "revocation_notification"
34
+ }.freeze
35
+
36
+ attr_reader :raw_response, :status, :status_string, :failure_info,
37
+ :timestamp_token, :tst_info
38
+
39
+ # Parse a DER-encoded TimeStampResp
40
+ # @param der [String] DER-encoded response
41
+ # @return [Response]
42
+ def self.parse(der)
43
+ new(der)
44
+ end
45
+
46
+ def initialize(der)
47
+ @raw_response = der
48
+ parse_response
49
+ end
50
+
51
+ # Check if the timestamp was granted
52
+ # @return [Boolean]
53
+ def success?
54
+ status == STATUS_GRANTED || status == STATUS_GRANTED_WITH_MODS
55
+ end
56
+
57
+ # Check if there was a failure
58
+ # @return [Boolean]
59
+ def failure?
60
+ !success?
61
+ end
62
+
63
+ # Get the timestamp time
64
+ # @return [Time, nil]
65
+ def timestamp
66
+ return nil unless @tst_info
67
+
68
+ @timestamp ||= extract_gen_time
69
+ end
70
+
71
+ # Get the TSA serial number
72
+ # @return [Integer, nil]
73
+ def serial_number
74
+ return nil unless @tst_info
75
+
76
+ @serial_number ||= extract_serial_number
77
+ end
78
+
79
+ # Get the nonce from the response
80
+ # @return [Integer, nil]
81
+ def nonce
82
+ return nil unless @tst_info
83
+
84
+ @nonce ||= extract_nonce
85
+ end
86
+
87
+ # Get the message imprint hash from the response
88
+ # @return [String, nil]
89
+ def message_imprint_hash
90
+ return nil unless @tst_info
91
+
92
+ @message_imprint_hash ||= extract_message_imprint
93
+ end
94
+
95
+ # Get the TSA policy OID
96
+ # @return [String, nil]
97
+ def policy_oid
98
+ return nil unless @tst_info
99
+
100
+ @policy_oid ||= extract_policy
101
+ end
102
+
103
+ # Get the raw timestamp token (CMS SignedData)
104
+ # @return [String, nil] DER-encoded token
105
+ def token_der
106
+ return nil unless @timestamp_token
107
+
108
+ @timestamp_token.to_der
109
+ end
110
+
111
+ # Verify the nonce matches the request
112
+ # @param request_nonce [Integer]
113
+ # @return [Boolean]
114
+ def nonce_matches?(request_nonce)
115
+ nonce == request_nonce
116
+ end
117
+
118
+ # Get human-readable status
119
+ # @return [String]
120
+ def status_name
121
+ STATUS_NAMES[status] || "unknown(#{status})"
122
+ end
123
+
124
+ # Get error message if failed
125
+ # @return [String, nil]
126
+ def error_message
127
+ return nil if success?
128
+
129
+ msg = "Timestamp request failed: #{status_name}"
130
+ msg += " - #{status_string}" if status_string
131
+ msg += " (failure: #{failure_info})" if failure_info
132
+ msg
133
+ end
134
+
135
+ private
136
+
137
+ def parse_response
138
+ # TimeStampResp ::= SEQUENCE {
139
+ # status PKIStatusInfo,
140
+ # timeStampToken TimeStampToken OPTIONAL
141
+ # }
142
+
143
+ asn1 = OpenSSL::ASN1.decode(raw_response)
144
+ raise InvalidTimestampError, "Invalid response structure" unless asn1.is_a?(OpenSSL::ASN1::Sequence)
145
+
146
+ parse_status_info(asn1.value[0])
147
+
148
+ if asn1.value[1] && success?
149
+ parse_timestamp_token(asn1.value[1])
150
+ end
151
+ rescue OpenSSL::ASN1::ASN1Error => e
152
+ raise InvalidTimestampError, "Failed to parse timestamp response: #{e.message}"
153
+ end
154
+
155
+ def parse_status_info(status_info)
156
+ # PKIStatusInfo ::= SEQUENCE {
157
+ # status PKIStatus,
158
+ # statusString PKIFreeText OPTIONAL,
159
+ # failInfo PKIFailureInfo OPTIONAL
160
+ # }
161
+
162
+ @status = status_info.value[0].value.to_i
163
+
164
+ if status_info.value[1]
165
+ @status_string = extract_status_string(status_info.value[1])
166
+ end
167
+
168
+ if status_info.value[2]
169
+ @failure_info = status_info.value[2].value
170
+ end
171
+ end
172
+
173
+ def extract_status_string(asn1)
174
+ # PKIFreeText is a SEQUENCE of UTF8String
175
+ return asn1.value if asn1.is_a?(OpenSSL::ASN1::UTF8String)
176
+ return asn1.value.map(&:value).join("; ") if asn1.is_a?(OpenSSL::ASN1::Sequence)
177
+
178
+ asn1.value.to_s
179
+ end
180
+
181
+ def parse_timestamp_token(token_asn1)
182
+ # TimeStampToken is ContentInfo containing SignedData
183
+ @timestamp_token = OpenSSL::PKCS7.new(token_asn1.to_der)
184
+
185
+ # Extract TSTInfo from the SignedData content
186
+ signed_data_content = @timestamp_token.data
187
+ @tst_info = OpenSSL::ASN1.decode(signed_data_content) if signed_data_content
188
+ rescue OpenSSL::PKCS7::PKCS7Error => e
189
+ raise InvalidTimestampError, "Invalid timestamp token: #{e.message}"
190
+ end
191
+
192
+ def extract_gen_time
193
+ # TSTInfo.genTime is at a specific position in the sequence
194
+ return nil unless @tst_info.is_a?(OpenSSL::ASN1::Sequence)
195
+
196
+ # genTime is typically the 4th element (index 3) in TSTInfo
197
+ @tst_info.value.each do |elem|
198
+ if elem.is_a?(OpenSSL::ASN1::GeneralizedTime) || elem.is_a?(OpenSSL::ASN1::UTCTime)
199
+ return elem.value
200
+ end
201
+ end
202
+ nil
203
+ end
204
+
205
+ def extract_serial_number
206
+ return nil unless @tst_info.is_a?(OpenSSL::ASN1::Sequence)
207
+
208
+ # serialNumber is the 2nd element (index 1)
209
+ serial = @tst_info.value[1]
210
+ serial.is_a?(OpenSSL::ASN1::Integer) ? serial.value.to_i : nil
211
+ end
212
+
213
+ def extract_nonce
214
+ return nil unless @tst_info.is_a?(OpenSSL::ASN1::Sequence)
215
+
216
+ # nonce is optional, look for it after the mandatory fields
217
+ @tst_info.value.each do |elem|
218
+ next unless elem.is_a?(OpenSSL::ASN1::Integer)
219
+ next if elem == @tst_info.value[1] # Skip serial number
220
+
221
+ return elem.value.to_i
222
+ end
223
+ nil
224
+ end
225
+
226
+ def extract_message_imprint
227
+ return nil unless @tst_info.is_a?(OpenSSL::ASN1::Sequence)
228
+
229
+ # messageImprint is the 3rd element (index 2)
230
+ imprint = @tst_info.value[2]
231
+ return nil unless imprint.is_a?(OpenSSL::ASN1::Sequence)
232
+
233
+ # Get the hash value (2nd element of MessageImprint)
234
+ imprint.value[1]&.value
235
+ end
236
+
237
+ def extract_policy
238
+ return nil unless @tst_info.is_a?(OpenSSL::ASN1::Sequence)
239
+
240
+ # policy is the 1st element (index 0)
241
+ policy = @tst_info.value[0]
242
+ policy.is_a?(OpenSSL::ASN1::ObjectId) ? policy.oid : nil
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module EasyCodeSign
6
+ module Timestamp
7
+ # Verifies RFC 3161 timestamp tokens
8
+ #
9
+ # @example
10
+ # verifier = Timestamp::Verifier.new
11
+ # result = verifier.verify(token_der, original_data)
12
+ #
13
+ class Verifier
14
+ attr_reader :trust_store
15
+
16
+ # Create a new timestamp verifier
17
+ #
18
+ # @param trust_store [OpenSSL::X509::Store, nil] custom trust store
19
+ #
20
+ def initialize(trust_store: nil)
21
+ @trust_store = trust_store || build_default_trust_store
22
+ end
23
+
24
+ # Verify a timestamp token
25
+ #
26
+ # @param token_der [String] DER-encoded timestamp token
27
+ # @param original_data [String] the data that was timestamped
28
+ # @param algorithm [Symbol] hash algorithm used (:sha256, :sha384, :sha512)
29
+ # @return [VerificationResult]
30
+ #
31
+ def verify(token_der, original_data, algorithm: :sha256)
32
+ result = VerificationResult.new
33
+
34
+ begin
35
+ pkcs7 = OpenSSL::PKCS7.new(token_der)
36
+ result.token_parsed = true
37
+
38
+ # Verify PKCS#7 signature
39
+ verify_pkcs7_signature(pkcs7, result)
40
+
41
+ # Parse and verify TSTInfo
42
+ tst_info = parse_tst_info(pkcs7)
43
+ result.tst_info_parsed = true
44
+
45
+ # Verify message imprint
46
+ verify_message_imprint(tst_info, original_data, algorithm, result)
47
+
48
+ # Extract timestamp info
49
+ extract_timestamp_info(tst_info, result)
50
+
51
+ # Verify certificate chain
52
+ verify_certificate_chain(pkcs7, result)
53
+
54
+ result.valid = result.signature_valid && result.imprint_valid && result.chain_valid
55
+ rescue OpenSSL::PKCS7::PKCS7Error => e
56
+ result.errors << "PKCS#7 error: #{e.message}"
57
+ rescue OpenSSL::ASN1::ASN1Error => e
58
+ result.errors << "ASN.1 parsing error: #{e.message}"
59
+ rescue StandardError => e
60
+ result.errors << "Verification error: #{e.message}"
61
+ end
62
+
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ def build_default_trust_store
69
+ store = OpenSSL::X509::Store.new
70
+ store.set_default_paths
71
+ store
72
+ end
73
+
74
+ def verify_pkcs7_signature(pkcs7, result)
75
+ # Verify the PKCS#7 signature using embedded certificates
76
+ if pkcs7.verify(pkcs7.certificates, trust_store, nil, OpenSSL::PKCS7::NOVERIFY)
77
+ result.signature_valid = true
78
+ else
79
+ result.signature_valid = false
80
+ result.errors << "PKCS#7 signature verification failed"
81
+ end
82
+ rescue OpenSSL::PKCS7::PKCS7Error => e
83
+ result.signature_valid = false
84
+ result.errors << "Signature verification error: #{e.message}"
85
+ end
86
+
87
+ def parse_tst_info(pkcs7)
88
+ content = pkcs7.data
89
+ raise InvalidTimestampError, "No content in timestamp token" unless content
90
+
91
+ OpenSSL::ASN1.decode(content)
92
+ end
93
+
94
+ def verify_message_imprint(tst_info, original_data, algorithm, result)
95
+ # Extract message imprint from TSTInfo
96
+ return unless tst_info.is_a?(OpenSSL::ASN1::Sequence)
97
+
98
+ imprint_seq = tst_info.value[2]
99
+ return unless imprint_seq.is_a?(OpenSSL::ASN1::Sequence)
100
+
101
+ # Get the hash from the timestamp
102
+ ts_hash = imprint_seq.value[1]&.value
103
+
104
+ # Compute expected hash
105
+ expected_hash = digest_class(algorithm).digest(original_data)
106
+
107
+ if ts_hash == expected_hash
108
+ result.imprint_valid = true
109
+ else
110
+ result.imprint_valid = false
111
+ result.errors << "Message imprint does not match original data"
112
+ end
113
+ end
114
+
115
+ def extract_timestamp_info(tst_info, result)
116
+ return unless tst_info.is_a?(OpenSSL::ASN1::Sequence)
117
+
118
+ # Extract policy OID (index 0)
119
+ if tst_info.value[0].is_a?(OpenSSL::ASN1::ObjectId)
120
+ result.policy_oid = tst_info.value[0].oid
121
+ end
122
+
123
+ # Extract serial number (index 1)
124
+ if tst_info.value[1].is_a?(OpenSSL::ASN1::Integer)
125
+ result.serial_number = tst_info.value[1].value.to_i
126
+ end
127
+
128
+ # Extract genTime
129
+ tst_info.value.each do |elem|
130
+ if elem.is_a?(OpenSSL::ASN1::GeneralizedTime) || elem.is_a?(OpenSSL::ASN1::UTCTime)
131
+ result.timestamp = elem.value
132
+ break
133
+ end
134
+ end
135
+ end
136
+
137
+ def verify_certificate_chain(pkcs7, result)
138
+ certs = pkcs7.certificates
139
+ return unless certs&.any?
140
+
141
+ result.tsa_certificate = certs.first
142
+
143
+ # Verify the TSA certificate chain
144
+ begin
145
+ if trust_store.verify(certs.first, certs)
146
+ result.chain_valid = true
147
+ else
148
+ result.chain_valid = false
149
+ result.errors << "TSA certificate chain validation failed: #{trust_store.error_string}"
150
+ end
151
+ rescue OpenSSL::X509::StoreError => e
152
+ result.chain_valid = false
153
+ result.errors << "Certificate chain error: #{e.message}"
154
+ end
155
+
156
+ # Check if TSA certificate has extended key usage for timestamping
157
+ verify_tsa_extended_key_usage(certs.first, result)
158
+ end
159
+
160
+ def verify_tsa_extended_key_usage(cert, result)
161
+ return unless cert
162
+
163
+ eku = cert.extensions.find { |e| e.oid == "extendedKeyUsage" }
164
+ return unless eku
165
+
166
+ unless eku.value.include?("Time Stamping")
167
+ result.warnings << "TSA certificate does not have Time Stamping extended key usage"
168
+ end
169
+ end
170
+
171
+ def digest_class(algorithm)
172
+ case algorithm
173
+ when :sha256 then OpenSSL::Digest::SHA256
174
+ when :sha384 then OpenSSL::Digest::SHA384
175
+ when :sha512 then OpenSSL::Digest::SHA512
176
+ else raise ArgumentError, "Unsupported algorithm: #{algorithm}"
177
+ end
178
+ end
179
+ end
180
+
181
+ # Result of timestamp verification
182
+ class VerificationResult
183
+ attr_accessor :valid, :token_parsed, :tst_info_parsed,
184
+ :signature_valid, :imprint_valid, :chain_valid,
185
+ :timestamp, :serial_number, :policy_oid,
186
+ :tsa_certificate, :errors, :warnings
187
+
188
+ def initialize
189
+ @valid = false
190
+ @token_parsed = false
191
+ @tst_info_parsed = false
192
+ @signature_valid = false
193
+ @imprint_valid = false
194
+ @chain_valid = false
195
+ @timestamp = nil
196
+ @serial_number = nil
197
+ @policy_oid = nil
198
+ @tsa_certificate = nil
199
+ @errors = []
200
+ @warnings = []
201
+ end
202
+
203
+ def valid?
204
+ valid
205
+ end
206
+
207
+ def tsa_name
208
+ tsa_certificate&.subject&.to_s
209
+ end
210
+
211
+ def to_h
212
+ {
213
+ valid: valid,
214
+ timestamp: timestamp,
215
+ serial_number: serial_number,
216
+ policy_oid: policy_oid,
217
+ tsa_name: tsa_name,
218
+ signature_valid: signature_valid,
219
+ imprint_valid: imprint_valid,
220
+ chain_valid: chain_valid,
221
+ errors: errors,
222
+ warnings: warnings
223
+ }
224
+ end
225
+ end
226
+ end
227
+ end