1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
require 'rubygems/user_interaction'
##
# A Gem::Security::Policy object encapsulates the settings for verifying
# signed gem files. This is the base class. You can either declare an
# instance of this or use one of the preset security policies in
# Gem::Security::Policies.
class Gem::Security::Policy
include Gem::UserInteraction
attr_reader :name
attr_accessor :only_signed
attr_accessor :only_trusted
attr_accessor :verify_chain
attr_accessor :verify_data
attr_accessor :verify_root
attr_accessor :verify_signer
##
# Create a new Gem::Security::Policy object with the given mode and
# options.
def initialize name, policy = {}, opt = {}
require 'openssl'
@name = name
@opt = opt
# Default to security
@only_signed = true
@only_trusted = true
@verify_chain = true
@verify_data = true
@verify_root = true
@verify_signer = true
policy.each_pair do |key, val|
case key
when :verify_data then @verify_data = val
when :verify_signer then @verify_signer = val
when :verify_chain then @verify_chain = val
when :verify_root then @verify_root = val
when :only_trusted then @only_trusted = val
when :only_signed then @only_signed = val
end
end
end
##
# Verifies each certificate in +chain+ has signed the following certificate
# and is valid for the given +time+.
def check_chain chain, time
raise Gem::Security::Exception, 'missing signing chain' unless chain
raise Gem::Security::Exception, 'empty signing chain' if chain.empty?
begin
chain.each_cons 2 do |issuer, cert|
check_cert cert, issuer, time
end
true
rescue Gem::Security::Exception => e
raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
end
end
##
# Verifies that +data+ matches the +signature+ created by +public_key+ and
# the +digest+ algorithm.
def check_data public_key, digest, signature, data
raise Gem::Security::Exception, "invalid signature" unless
public_key.verify digest.new, signature, data.digest
true
end
##
# Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
# If the +issuer+ is +nil+ no verification is performed.
def check_cert signer, issuer, time
raise Gem::Security::Exception, 'missing signing certificate' unless
signer
message = "certificate #{signer.subject}"
if not_before = signer.not_before and not_before > time then
raise Gem::Security::Exception,
"#{message} not valid before #{not_before}"
end
if not_after = signer.not_after and not_after < time then
raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
end
if issuer and not signer.verify issuer.public_key then
raise Gem::Security::Exception,
"#{message} was not issued by #{issuer.subject}"
end
true
end
##
# Ensures the public key of +key+ matches the public key in +signer+
def check_key signer, key
unless signer and key then
return true unless @only_signed
raise Gem::Security::Exception, 'missing key or signature'
end
raise Gem::Security::Exception,
"certificate #{signer.subject} does not match the signing key" unless
signer.public_key.to_pem == key.public_key.to_pem
true
end
##
# Ensures the root certificate in +chain+ is self-signed and valid for
# +time+.
def check_root chain, time
raise Gem::Security::Exception, 'missing signing chain' unless chain
root = chain.first
raise Gem::Security::Exception, 'missing root certificate' unless root
raise Gem::Security::Exception,
"root certificate #{root.subject} is not self-signed " +
"(issuer #{root.issuer})" if
root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8
check_cert root, root, time
end
##
# Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
# the digests of the two certificates match according to +digester+
def check_trust chain, digester, trust_dir
raise Gem::Security::Exception, 'missing signing chain' unless chain
root = chain.first
raise Gem::Security::Exception, 'missing root certificate' unless root
path = Gem::Security.trust_dir.cert_path root
unless File.exist? path then
message = "root cert #{root.subject} is not trusted"
message << " (root of signing cert #{chain.last.subject})" if
chain.length > 1
raise Gem::Security::Exception, message
end
save_cert = OpenSSL::X509::Certificate.new File.read path
save_dgst = digester.digest save_cert.public_key.to_s
pkey_str = root.public_key.to_s
cert_dgst = digester.digest pkey_str
raise Gem::Security::Exception,
"trusted root certificate #{root.subject} checksum " +
"does not match signing root certificate checksum" unless
save_dgst == cert_dgst
true
end
##
# Extracts the email or subject from +certificate+
def subject certificate # :nodoc:
certificate.extensions.each do |extension|
next unless extension.oid == 'subjectAltName'
return extension.value
end
certificate.subject.to_s
end
def inspect # :nodoc:
("[Policy: %s - data: %p signer: %p chain: %p root: %p " +
"signed-only: %p trusted-only: %p]") % [
@name, @verify_chain, @verify_data, @verify_root, @verify_signer,
@only_signed, @only_trusted,
]
end
##
# For +full_name+, verifies the certificate +chain+ is valid, the +digests+
# match the signatures +signatures+ created by the signer depending on the
# +policy+ settings.
#
# If +key+ is given it is used to validate the signing certificate.
def verify chain, key = nil, digests = {}, signatures = {},
full_name = '(unknown)'
if signatures.empty? then
if @only_signed then
raise Gem::Security::Exception,
"unsigned gems are not allowed by the #{name} policy"
elsif digests.empty? then
# lack of signatures is irrelevant if there is nothing to check
# against
else
alert_warning "#{full_name} is not signed"
return
end
end
opt = @opt
digester = Gem::Security::DIGEST_ALGORITHM
trust_dir = opt[:trust_dir]
time = Time.now
_, signer_digests = digests.find do |algorithm, file_digests|
file_digests.values.first.name == Gem::Security::DIGEST_NAME
end
if @verify_data then
raise Gem::Security::Exception, 'no digests provided (probable bug)' if
signer_digests.nil? or signer_digests.empty?
else
signer_digests = {}
end
signer = chain.last
check_key signer, key if key
check_cert signer, nil, time if @verify_signer
check_chain chain, time if @verify_chain
check_root chain, time if @verify_root
if @only_trusted then
check_trust chain, digester, trust_dir
elsif signatures.empty? and digests.empty? then
# trust is irrelevant if there's no signatures to verify
else
alert_warning "#{subject signer} is not trusted for #{full_name}"
end
signatures.each do |file, _|
digest = signer_digests[file]
raise Gem::Security::Exception, "missing digest for #{file}" unless
digest
end
signer_digests.each do |file, digest|
signature = signatures[file]
raise Gem::Security::Exception, "missing signature for #{file}" unless
signature
check_data signer.public_key, digester, signature, digest if @verify_data
end
true
end
##
# Extracts the certificate chain from the +spec+ and calls #verify to ensure
# the signatures and certificate chain is valid according to the policy..
def verify_signatures spec, digests, signatures
chain = spec.cert_chain.map do |cert_pem|
OpenSSL::X509::Certificate.new cert_pem
end
verify chain, nil, digests, signatures, spec.full_name
true
end
alias to_s name # :nodoc:
end
|