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